# HubSpot Account Types @ Learn about the different types of HubSpot accounts and what each one is used for. $ getting-started & --- # HubSpot account types 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. A standard HubSpot account will have access to all the tools and features included with your plan. ## Developer accounts Developer accounts are free accounts intended for creating and managing apps, integrations, and developer test accounts. They're also where you can create and manage App Marketplace listings. Developer accounts and their associated test accounts aren’t connected to a standard HubSpot account, and they can’t sync data or assets with another HubSpot account. Get started by creating a [developer account](https://app.hubspot.com/signup-hubspot/developers). ### Developer test accounts Within developer 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: - **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 developer account. This type of account cannot sync data with other accounts, and they can't be connected to a standard HubSpot account. ### Create a developer test account To create a developer test account: - In the main navigation of your developer account, click **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 the main navigation of your developer account, click **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**. ## 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. These accounts can be identified by a yellow banner at the top of the page that includes the label: _You're in \[name of sandbox\], which is a standard sandbox account_. The banner will also include a link back to your production account for easy switching. 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 (BETA) If you have a **_Sales Hub_** or **_Service Hub_** _Enterprise_ subscription, you can create a development sandbox account through the CLI for local development. You can gain access to development sandbox accounts by [opting into the CRM developer tools beta](https://knowledge.hubspot.com/account-management/opt-your-hubspot-account-into-a-public-beta-feature) in your standard HubSpot account. Development sandboxes can be identified by a yellow banner at the top of the page that reads: _You're in \[name of sandbox\], which is a development sandbox account_. Learn more about creating and using [development sandbox accounts](/guides/crm/setup#create-and-use-development-sandboxes). ## Marketplace provider accounts Marketplace Provider accounts are intended for creating and managing [Template Marketplace listings](/guides/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. # Getting started overview @ Learn how to get started developing on HubSpot. $ getting-started & --- # Getting started overview Whether you’re a new HubSpot developer or looking to expand your skills, this section will guide you through the process of getting up and running. You’ll explore 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 First, it’s helpful to consider what makes HubSpot a powerful platform for developers. The foundation of every HubSpot account is the [CRM](/guides/api/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 integrations and/or extensions with HubSpot’s APIs to sync data between HubSpot and external platforms. - Building custom websites using the HubSpot CMS (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. - Customizing the CRM with UI extensions. Developers can use React to build flexible custom cards that integrate seamlessly with HubSpot. In the next section, you’ll get an overview of the tooling that’s available for HubSpot developers. Then, you'll choose a route to continue on your HubSpot developer journey. ## 1. Install tools to help you build There are a wide range of tools available for HubSpot developers. The mix of tools you’ll need will depend on your specific goals. - **HubSpot CLI:** installing the HubSpot CLI for local development is a great starting point for many development tasks. The CLI is used when building UI Extensions and CMS assets, and can be used to create and manage secrets, HubDB tables, and custom object data from your account. You'll need to have [Node.js](https://nodejs.org/en/) installed (version 18 or higher is required, and the LTS version is recommended). To install the CLI globally, run the following command: ```shell npm i -g @hubspot/cli ``` - **Visual studio code:** installing [Visual Studio Code](https://code.visualstudio.com/) means you can take advantage of the [HubSpot Visual Studio Code extension](/guides/cms/setup/install-and-use-hubspot-code-extension), which allows you to centralize your development workflow. - **Client libraries**: HubSpot provides client libraries for [Node](https://github.com/HubSpot/hubspot-api-nodejs), [PHP](https://github.com/HubSpot/hubspot-api-php), [Ruby](https://github.com/HubSpot/hubspot-api-ruby), and [Python](https://github.com/HubSpot/hubspot-api-python). - **Postman:** use [Postman](https://www.postman.com/) to access [HubSpot's Postman Collections](https://www.postman.com/hubspot). Throughout HubSpot's API documentation, you'll find buttons for running calls using the collection. Learn more about [HubSpot's developer tooling](/getting-started/tools-to-help-you-build). ## 2. Decide what to build There’s no one-size-fits-all route to developing with HubSpot. It’s a flexible platform that can meet a wide range of needs. One of the four broad categories listed below should cover most of these needs. You can choose the one that’s most relevant for you right now for a brief introduction, along with some immediate steps to get you started. You can also get a sense of the best tools for the job, and whether a specific [type of HubSpot account](/getting-started/account-types) is required. - [Use APIs and build integrations](/getting-started/what-to-build#use-apis-and-build-integrations) - [Build apps for the marketplace](/getting-started/what-to-build#build-for-the-app-marketplace) - [Customize the HubSpot UI](/getting-started/what-to-build#customize-the-hubspot-ui) - [Build custom websites](/getting-started/what-to-build#build-custom-websites) ## 3. Join your peers Connect with your peers that are already building on HubSpot. Ask questions, share knowledge, build community, and maybe even make some friends along the way. - [Developer Slack](/getting-started/slack/developer-slack) - [Developer Forums](https://community.hubspot.com/t5/HubSpot-Developers/ct-p/developers) - [Developer Advocacy GitHub](https://github.com/hubspotdev) ## 4. Learn more, your way Once you’re up and running, there are plenty of resources available to help you dig deeper. You can access in-depth guides, detailed reference docs or [HubSpot academy courses](https://academy.hubspot.com/courses), depending on your preferred learning style. # The HubSpot Developer Slack Community Code of Conduct @ HubSpot's Developer Slack is a dedicated community for HubSpot's most engaged developers to gather in the name of developer-to-developer support and collaboration around the HubSpot platform. $ getting-started & slack --- # Developer Slack Code of Conduct ## Preamble HubSpot's Developer Slack is a dedicated community for HubSpot's developers to gather in the name of developer-to-developer support and collaboration around the HubSpot platform. By using our Developer Slack, you agree to abide by the following Developer Slack Code of Conduct (“Code of Conduct”), [HubSpots Acceptable Use Policy](https://legal.hubspot.com/acceptable-use) as applicable, and [Slack’s Acceptable Use Policy](https://slack.com/acceptable-use-policy). ## Updates to this Code of Conduct HubSpot reserves the right to update this Code of Conduct at any time. When updates do occur, we are committed to communicating these changes via the #releases-and-updates channel on the Developer Slack, but because this Code of Conduct is written anticipating common scenarios, and we cannot anticipate every possible scenario that could occur, we may need to update it and take action on violations that may not be explicitly described here without prior notification of the community. HubSpot reserves the right to downgrade or deactivate the Developer Slack at any time, but are committed to doing so with plenty of advanced notice to the community. ## HubSpot Support Expectations The Developer Slack is not an official support channel of HubSpot. While this Slack may contain HubSpot Employees who are happy to assist when they can, **please refrain from mentioning or messaging HubSpot employees with direct questions**. If you have a question in which you require direct support from HubSpot, please go to [help.hubspot.com](http://help.hubspot.com/) or the [community forums](https://community.hubspot.com/). HubSpot reserves the right to enforce, interpret, and extend these guidelines for the betterment of the community. ## Expected Behavior At HubSpot we don’t have pages of policies or procedures and instead have a three-word policy of Use Good Judgement and have HEART. We encourage our communities to also use good judgment and to share in the following core values of HubSpot: ### Be Humble Nothing is perfect. Be self-aware and always be respectful. ### Be Empathetic Don’t just be understanding of someone's comments, also act with compassion and respect for all. ### Be Adaptable Be curious, and always be ready to change. Be a life-long learner. ### Be Remarkable In the words of the wise Bill S. Preston, Esquire “Be excellent to each other”. Share your knowledge and don’t be afraid to be awesome. ### Be Transparent Be open and honest with others, most importantly, yourself. To learn more about our values, please see [HubSpot’s Culture Code](https://blog.hubspot.com/blog/tabid/6307/bid/34234/the-hubspot-culture-code-creating-a-company-we-love.aspx). ## Unacceptable Behavior The following behaviors are considered to be unacceptable and will not be tolerated by any users in the Slack workspace. ### Threats of Violence Threats of violence towards another user is not acceptable. This includes encouraging violence towards a user(s) or posting/threatening to post other peoples personally identifying information (known as “Doxxing”). ### Personal Attacks There will always be times when two parties will have a difference of opinion. While this can be frustrating, this should never turn into a personal attack on someone. Attacking people for their opinions, personal beliefs, ideas, or suggestions is not acceptable. Please be professional and respectful at all times. ### Impersonation of HubSpot Employment Any users impersonating a HubSpot Employee (or employment by HubSpot) will be banned from the Slack Workspace. This is a zero tolerance rule. ### Derogatory/Harmful Language and Harassment Harmful language towards a users: - Background - Family - Gender Identity or Expression - Sexual Orientation - Race/Ethnicity - Physical traits (body size, shape, scars, etc.) - Age - Native Language - Abilities - National Origin - Location - Religion - Political affiliation - Other personal attributes is not acceptable. Use Good Judgment at all times. ### Unwanted Solicitation of Services It is unacceptable to message users directly offering your services unless the user has specifically requested this information. This also includes direct messages regarding employment opportunities or freelance projects. We have provided a #jobs channel inside of our Slack workspace where these kinds of messages can be posted to, and a #showcase channel to highlight any HubSpot work you are proud of with the community. These are the only two channels where any form of solicitation or self-promotion may be permitted. ### Abuse of Voice/Video Calling Slack allows the use of voice/video calling within its platform. It is unacceptable to abuse this feature by calling users directly without their permission. We understand sometimes mistakes happen and buttons are clicked on accident. If this happens, be professional and simply send an apology message to the user. ### Undisclosed use of AI in Questions, Responses, or Conversations If you are going to use an AI-tool to generate content that will be used on the Developer Slack, you must clearly disclose the use of such tools in each and every instance of using them. You must also be mindful of the information that you provide an AI-tool and not share any HubSpot confidential or sensitive information. ### Channel and User Spamming The Developer Slack has many channels tailored toward certain functionality of our platform and many users. When posting your message, please use the appropriate channel for posting your message. It is unacceptable to simply copy/paste your message across multiple channels for added visibility. It is unacceptable to post the same message or similar message frequently within the same channel and/or directly spam users. ### Influencing of Unacceptable Behavior It’s unacceptable to influence any user(s) in participating in any of the unacceptable behaviors listed above. ## Use of the HubSpot Developer Slack for Private Team Conversations By joining the HubSpot Developer Slack, you are making a commitment to contribute to the broader HubSpot Developer Community. No conversation topics are discouraged (that abide by this Code of Conduct), but we ask that you do not use the Developer Slack to conduct private company business through Direct Messaging or other means. If such conduct is discovered, we reserve the right to remove you and your team from the Developer Slack. ## Voice/Video Calls + Screen Sharing Slack provides the ability for users to use Voice/Video Calling with the added ability to have screen sharing. When using these features, be mindful and respectful of others. Use good judgment when sharing your screen and be mindful of other windows that may be open on your computer containing sensitive or personal information. ## Slack “Apps” Usage Slack has the ability to add Applications to workspace to help expand on the functionality of Slack. We do not allow users to install apps on their own to the Developer Slack Workspace. All app installs to the workspace will be done by HubSpot. ## Keeping Topics Together (Threaded Messages) One of the great features of Slack is the ability for threaded comments. To help conversations together and be easily searchable/scannable, we recommend using this feature. It helps keep the main channels clean and groups the topic of conversations into one place. ## Gifs and Emojis When using gifs inside of the channels or threaded comments, please be mindful of the content/context inside the gif. Likewise, Slack offers the ability for emojis to express emotions in regards to comments. Please be mindful of the emojis you are using when communicating with others. ## Surveys and Surveying Developer Slack Users The Developer Slack Workspace is intended to be used for conversations around developing on the web and developing on HubSpot. Creating surveys are allowed and we have provided the Polly Slack Application in our workspace for use by anyone. When using Polly, we require the following: - The poll must be related to development on the web (such as CSS or JS Libraries) or development on HubSpot. - "Show Responses" must always remain checked. The following is not allowed when surveying individuals on the Developer Slack: - Solicitation of private information including demographic, medical, or other personal information of users.\* - Linking to surveys hosted on 3rd party sites (such as google forms, survey monkey forms, or personal websites).\* - Directly messagings users asking them to fill in surveys. - \* = HubSpot employees retain the right to survey the HubSpot Developer Slack in service of improvements to the HubSpot product. HubSpot Employees will make an effort to make it clear their survey is an official HubSpot survey if it is hosted elsewhere. ## Reporting Code of Conduct Violations To report a violation of this Code of Conduct, or anything that may have made you or others uncomfortable in the HubSpot Developer Community, you may reach out directly to any of our Admins. All current Admins will be indicated in a pinned post in the #general channel. If you prefer to report your violation anonymously or would prefer not to DM an Admin, you [may fill out this form](https://form.asana.com/) and we will be immediately notified. # Join the HubSpot Developer Slack Community @ HubSpot's Developer Slack is a dedicated community for HubSpot's developers to gather for developer-to-developer support and collaboration around the HubSpot platform. $ getting-started & slack --- # Join the HubSpot Developer Slack Community HubSpot's Developer Slack is a dedicated community for HubSpot's developers to gather for developer-to-developer support and collaboration around the HubSpot platform. Started in 2016, the developer Slack community has grown to hundreds of active developers from around the world gathered together to share their knowledge and expertise. ## Who is the Developer Slack Community for? Whether you are just starting out developing on the HubSpot Platform or you are a seasoned expert, the developer slack is for you. We have channels dedicated to multiple aspects of our platform from APIs, Email Development, Local Development using our [CLI](/guides/cms/tools/local-development-cli), [Templates](/guides/cms/content/templates/overview), [Themes](/guides/cms/content/themes/overview) and more. ## What can I expect once I join the community? You can expect to find an active community of developers who support and teach each other. This is first and foremost a community, the more you put into it the more you'll get out of it. This community is global, but the majority of folks in it are in the western hemisphere, that means if you live in the eastern hemisphere you may not see fast responses. It's not that anyone is ignoring or not seeing your posts, it's probably that folks are sleeping. The way to change that is to be one of the folks in your time zone that's active and helps others. Through that the community will grow and be more useful to you. We at HubSpot care deeply about this community and are a part of the community. Treat others with respect, treat HubSpotters with respect. [Review our slack community code of conduct](/getting-started/slack/code-of-conduct). Let's grow better together. ## Join our Developer Slack by signing up below {' '} # Tools to help you build @ Learn about the different tools and resources available to help you build on HubSpot. $ getting-started & --- # Tools to help you build HubSpot provides a range of tooling to help you as you build on its developer platform. HubSpot’s APIs are flexible and you can always use your preferred development environment to build an integration, but the tools listed below are available to all developers to enhance productivity and help streamline your development workflow. ## HubSpot CLI If you’re building on the HubSpot CMS, you can install the HubSpot CLI to build on HubSpot locally. Once installed, you can use your preferred tools in your preferred environment. You can also set up continuous integration with a GitHub account to track changes and help you collaborate with other developers in your organization. After installing the CLI, you can use it to: - Create and manage CMS assets, such as themes and modules. Check out the [CMS quickstart guide](/guides/cms/quickstart/react-plus-hubl-quickstart) to get started. - Create and manage UI extensions to customize CRM records and the help desk UI (BETA). Learn how to [get started building UI extensions](/guides/crm/overview#get-started). Learn how to [install the HubSpot CLI](/guides/cms/setup/getting-started-with-local-development). ## HubSpot VS Code extension The HubSpot Visual Studio Code extension allows you to centralize your development workflow without having to leave VS Code. Once installed, you can use the HubSpot CLI directly to authenticate and manage your connected accounts, as well as leverage HubL syntax highlighting and support for autocomplete. You can also sync files with the [design manager](https://knowledge.hubspot.com/design-manager/a-quick-tour-of-the-design-manager) of your default HubSpot account. Learn how to [install and use the HubSpot VS Code extension](/guides/cms/setup/install-and-use-hubspot-code-extension). ## Client libraries HubSpot provides client libraries so you can use your preferred programming language when using HubSpot APIs. Client libraries are available in JavaScript, Ruby, Python, and PHP. Each client library allows you to invoke function calls to read, edit, create, and delete data in your HubSpot account instead of having to craft API requests manually. Learn more about setting up and using the [Node](https://github.com/HubSpot/hubspot-api-nodejs), [PHP](https://github.com/HubSpot/hubspot-api-php), [Ruby](https://github.com/HubSpot/hubspot-api-ruby), and [Python](https://github.com/HubSpot/hubspot-api-python) client libraries. ## Postman Postman is a feature-rich API client you can use to easily make authenticated requests to HubSpot. You can also browse curated collections of all of HubSpot’s public API endpoints. These collections are regularly maintained by HubSpot to keep the specifications for all request and response fields up-to-date. Download the [Postman client](https://www.postman.com/), then browse the official [HubSpot Postman collections](https://www.postman.com/hubspot). # What to build @ Learn about the different ways to develop on HubSpot. $ getting-started & --- # What to build There are several different development routes possible with HubSpot, depending on your goals. You’ll find a high-level overview of each route on this page, along with the tooling and accounts you’ll need for each. You'll also find links to detailed docs to give you more context, as well as relevant quickstart guides. - [Use APIs and build integrations](#use-apis-and-build-integrations) - [Get started authenticating calls with a private app](#get-started-authenticating-calls-with-a-private-app) - [Build for the app marketplace](#build-for-the-app-marketplace) - [Customize the HubSpot UI (BETA)](#customize-the-hubspot-ui) - [Build custom websites](#build-custom-websites) ## Use APIs and build integrations Use HubSpot's APIs to build custom solutions, such as sharing data between HubSpot and external systems, using webhooks to listen for changes in the account, and creating custom objects to store data specific to your business. **Example use cases:** - Use custom objects to customize how the CRM stores data so it best represents your business. - Sync data from external systems to provide a richer picture of go-to-market activities. - Extend the capabilities of the CRM UI to best fit your processes. **Two ways to build apps** There are two [types of apps](/guides/apps/overview), depending on what you’re building. If you’re getting started with APIs and integrations, it's recommended to start with a private app, as they're faster to set up and use for authentication. Once you’re familiar with building apps, you may want to learn more about [public apps](#build-for-the-app-marketplace), which can be installed in multiple accounts and enable you to build other types of extensions. **Example use case for private apps:** _“I want to build an application for my company/team.”_ ### Get started authenticating calls with a private app Most API calls require authentication to interact with the data in your HubSpot account. To get started making calls to your account, create a private app and use its access token for authentication. [Super Admin](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#super-admin) permissions are required to build private apps in your HubSpot account. How to get started: - Create a [private app](/guides/apps/private-apps/overview) - Learn more about [making authenticated API calls with private apps](/guides/apps/private-apps/overview#make-api-calls-with-your-app-s-access-token) - You can also use [client libraries](/getting-started/overview) to make API calls ### Build for the app marketplace Develop integrations for HubSpot's App Marketplace with a public app, to enable HubSpot users to install your integration into their account. Building public apps for the marketplace requires adherence to HubSpot's App Marketplace guidelines, and an app developer account. Public apps authenticate with OAuth. Public apps can be installed on multiple accounts and can be distributed on the marketplace. **Example use cases for public apps:** _“I’m a HubSpot partner who wants to build a reusable app that I can adapt for my clients.”_ _“I’m a HubSpot partner who wants to make an app available on the marketplace to promote our capabilities to as many HubSpot customers as possible.”_ How to get started: - [Create an app developer account](https://app.hubspot.com/signup-hubspot/developers) - **Quickstart:** [Create a public app](/guides/apps/public-apps/overview) - [Follow the OAuth quickstart guide](/guides/apps/authentication/oauth-quickstart-guide) - [Review the App Marketplace listing requirements](/guides/apps/marketplace/app-marketplace-listing-requirements) - [Submit your app to the App Marketplace](/guides/apps/marketplace/listing-your-app) ## Customize the HubSpot UI Build UI extensions to customize the CRM and help desk UI. UI extensions are built locally using the developer projects framework, which enables you to build and deploy to HubSpot using the CLI. The UI extensions SDK provides a toolbox of methods, functionalities, tools, and components to customize your extension. To see examples of what you can build, check out [HubSpot's sample extensions](/guides/crm/ui-extensions/sample-extensions/overview). Projects enable you to locally build and deploy UI extensions for private apps and public apps using the HubSpot CLI. **Example use cases for UI extensions:** _“I want to add a custom form to contact and company records that enables our customer support team to create Jira tickets while on customer calls.”_ _“I need to surface detailed sales pipeline summaries across deal records so that our managing partners can find the information they need at a glance.”_ How to get started: - [Enroll your account](/guides/crm/overview#get-started) into the available betas There are a few ways to use HubSpot's UI customization tools, depending on your HubSpot subscription: - Building UI extensions for private apps in a standard HubSpot account requires a **_Sales Hub_** or **_Service Hub_** _Enterprise_ subscription. However, a paid subscription is not required to create UI extensions for private apps in [developer test accounts](/getting-started/account-types#developer-test-accounts). - Public apps are built in [developer accounts](/getting-started/account-types#app-developer-accounts), which don't require a paid subscription. If you're enrolled in the [UI extensions for public apps beta](https://developers.hubspot.com/build-app-cards), learn more about [building UI extensions for public apps](/guides/crm/public-apps/overview). - Follow the quickstart guides: - [UI extensions for private apps quickstart](/guides/crm/private-apps/quickstart) - [UI extensions for public apps quickstart](/guides/crm/public-apps/quickstart) - Learn more about [creating UI extensions](/guides/crm/ui-extensions/create) and using the [UI extensions SDK](/guides/crm/ui-extensions/sdk) ## Build custom websites Using HubSpot's CMS (Content Management System) software, you can create powerful websites and landing pages that adapt and tailor themselves to the individuals coming to your site. By building on the CMS, every website visitor interaction is directly connected to the account’s CRM, the same tool that the Sales, Marketing, Services, and RevOps teams are using to do their jobs more efficiently. Build HubSpot websites locally in your preferred environment with the HubSpot CLI, using technologies such as: - HTML, CSS, and JavaScript - HubL, which is HubSpot's Jinjava-based templating language - React, which can be used to build JavaScript-rendered modules and partials - GraphQL, which you can use to query HubSpot data And if you prefer to use GitHub for version control, you can [set up an integration using GitHub Actions](/guides/cms/setup/github-integration). The CMS enables you to build websites locally with the HubSpot CLI. You’ll have access to an array of versatile and adaptable tools, such as multi-language support, CTAs, forms, memberships, and more. HubSpot also handles security, server-side performance, CDN, hosting, and uptime assurance for you. **Example use cases for the CMS:** _“I want to build out product listing pages that pull information directly from inventory maintained in the CRM.”_ _“I need a website that features a public blog and a members-only blog.”_ - Create a [CMS Sandbox account](https://app.hubspot.com/signup-hubspot/cms-developers) - Install the [HubSpot CLI](/guides/cms/setup/getting-started-with-local-development). - Install the [HubSpot Visual Studio Code extension](/guides/cms/setup/install-and-use-hubspot-code-extension) - **Quickstart:** [HubSpot CMS quickstart guide (React + HubL)](/guides/cms/quickstart/react-plus-hubl-quickstart) - Check out the [CMS guides](/guides/cms) and [reference docs](/reference/cms) # Cookie banner @ The cookie consent banner enables you to enable/disable different analytics scripts based on user preferences. $ reference & api/analytics-and-events/cookie-banner --- # Cookie consent banner API Super admins and users with [permission to edit website settings](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#marketing) can customize visitor cookie tracking and consent banners to comply with EU cookie laws and the [General Data Protection Regulation (GDPR)](https://www.hubspot.com/data-privacy/gdpr). A cookie consent banner allows visitors to opt in or opt out of being tracked in your HubSpot account with cookies. This feature works for all HubSpot pages as well as any external pages with your [HubSpot tracking code](https://knowledge.hubspot.com/reports/install-the-hubspot-tracking-code) installed. [Customize the cookie tracking settings and cookie consent banner.](https://knowledge.hubspot.com/privacy-and-consent/customize-your-cookie-tracking-settings-and-consent-banner) In this article, learn how to manage the cookies that are added to a visitor's browser through the cookie consent banner. You can also learn how to use a [third party cookie consent banner](#thirdparty) to distribute consent, disable the consent collection, and manage features within HubSpot. --- ## Remove cookies `_hsp.push(['revokeCookieConsent']);` Remove the cookies created by the HubSpot tracking code that are included in the consent banner under GDPR, include the HubSpot cookies related to tracking the visitor. As a result of the cookies being removed, the visitor would see the [cookie consent banner](https://knowledge.hubspot.com/privacy-and-consent/customize-your-cookie-tracking-settings-and-consent-banner) on their next page load, as they would appear as a new visitor. This function does not remove cookies placed by non-HubSpot banners. You can find the specific list of cookies that will be removed on [HubSpot's Knowledge Base](https://knowledge.hubspot.com/privacy-and-consent/what-cookies-does-hubspot-set-in-a-visitor-s-browser#consent-banner-cookies). If cookie blocking is turned on, this function will revoke consent so any third-party cookies will not be updated or dropped during future visits to the website. ```js /* Example code to remove the consent banner cookies when a visitor clicks an element with the 'removeCookies' id. */ var _hsp = (window._hsp = window._hsp || []); document.getElementById('removeCookies').onclick = function () { _hsp.push(['revokeCookieConsent']); }; ``` ## Place do not track cookie `_hsq.push(['doNotTrack']);` Places the `__hs_do_not_track` cookie in the visitors browser, which will prevent the HubSpot tracking code from sending any information for the visitor. You can remove the cookie by calling the function again and including the `{track: true}` argument: `_hsq.push(['doNotTrack', {track: true}]);` This function prevents all information from being collected by the tracking code, including anonymized traffic and [custom event](#events-js-api) data. ```js /* Example code to place the __hs_do_not_track cookie for the visitor when they click an element with the 'doNotTrack' id. */ document.getElementById('doNotTrack').onclick = function () { _hsq.push(['doNotTrack']); }; ``` ## Get privacy consent status `_hsp.push(['addPrivacyConsentListener', callbackFunction]);` Get the privacy consent status of the current visitor. There are 3 categories of consent that can be used to provide more granular control to the user. These each have their own keys within the `consent.categories` object: - `consent.categories.analytics` - `consent.categories.advertisement` - `consent.categories.functionality` The _callbackFunction_ will be called, depending on the state of the page: - If the banner is not enabled, or if the visitor has previously seen the banner and clicked accept or decline: - the _callbackFunction_ will be called immediately if the banner code is already loaded. - the _callbackFunction_ will be called after the tracking code loads if the function is pushed to **\_hsp** before the tracking code loads. - If the banner is enabled, the callback function will be called when the visitor clicks on the accept or decline button. ```js // Log the analytics category consent status of the current visitor to the console var _hsp = (window._hsp = window._hsp || []); // analytics _hsp.push([ 'addPrivacyConsentListener', function (consent) { console.log(consent.categories.analytics); }, ]); // advertisement _hsp.push([ 'addPrivacyConsentListener', function (consent) { console.log(consent.categories.advertisement); }, ]); // functionality _hsp.push([ 'addPrivacyConsentListener', function (consent) { console.log(consent.categories.functionality); }, ]); // or it can all be done in one call _hsp.push([ 'addPrivacyConsentListener', function (consent) { console.log(`analytics: ${consent.categories.analytics}`); console.log(`advertisement: ${consent.categories.advertisement}`); console.log(`functionality: ${consent.categories.functionality}`); }, ]); ``` ## Cookies not by category This is provided for backward compatibility with older scripts. For all new websites you should use the cookies by category method, giving more granular control over cookie activation. `_hsp.push(['addPrivacyConsentListener', callbackFunction]);` Allows you to get the _true_ or _false_ privacy consent status of the current visitor. The _callbackFunction_ will be called, depending on the state of the page: - If the banner is not enabled, or if the visitor has previously seen the banner and clicked accept or decline: - the _callbackFunction_ will be called immediately if the banner code is already loaded. - the _callbackFunction_ will be called after the tracking code loads if the function is pushed to **\_hsp** before the tracking code loads. - If the banner is enabled, the callback function will be called when the visitor clicks on the accept or decline button. ```js // Log the consent status of the current visitor to the console var _hsp = (window._hsp = window._hsp || []); _hsp.push([ 'addPrivacyConsentListener', function (consent) { if (consent.allowed) { console.log('something'); } }, ]); ``` The `callbackFunction` accepts a `consent` object as its only argument. The `consent` object has a single `allowed` property that will be `true` if: - The [cookie consent banner](https://knowledge.hubspot.com/privacy-and-consent/customize-your-cookie-tracking-settings-and-consent-banner) is not enabled, or is enabled in notify-only mode. - The visitor clicks accept on the banner when opt-in mode is enabled. - The visitor has previously clicked accept on the banner when opt-in mode is enabled. The property will be false if the consent banner is enabled in opt-in mode and the visitor clicks or has previously clicked the decline button. ## ​​Enable website visitors to manage their consent Call the `showBanner` function to resurface the banner, enabling website visitors to make changes to their consent preferences. For example: ```js ​​var _hsp = window._hsp = window._hsp || []; ​​_hsp.push(['showBanner']); ``` The behavior of`showBanner`varies by policy and is only available for Opt-In and Cookie-By-Category policies. For Opt-In policies, calling `showBanner` will cause the banner to reappear, as shown in the video below: For Cookies-By-Category policies, calling `showBanner` will cause the modal for selecting each category to reappear, as shown in the video below: ## UI Examples This functionality can be made available to visitors in the form of buttons/links on your website that they can use to re-open the banner and edit their preferences. The following are examples with code. ### Button A button, often placed in the website footer. ```html ``` ```css #hs_show_banner_button { display: inline-block; text-align: center; height: 50px; background-color: #425b76; border: 1px solid #425b76; border-radius: 3px; padding: 10px 16px; text-decoration: none; color: #fff; font-family: inherit; font-size: inherit; font-weight: normal; line-height: inherit; text-shadow: none; } ``` ### Fixed position button A button with fixed positioning on the bottom of the screen. This kind of button has the advantage of being readily available and easy to find, while being somewhat obtrusive UX. ```html ``` ```css button#hs-hud-cookie-settings { position: fixed !important; bottom: 0px; right: 10px; color: white; background-color: #425b76; padding: 5px; border-top-right-radius: 5px; border-top-left-radius: 5px; border-width: 0; appearance: none; } ``` ### Link A link or highlighted text. ```html Cookie Settings ``` ```css #hs-cookie-settings-link { cursor: pointer; } ``` ## Block third party cookies manually The HubSpot Consent Banner supports manual handling of third party tracking technologies and cookies. It's recommended to use manual handling if you have a complicated website and/or a dedicated web developer. If auto-blocking does not work for your site, manual blocking is also a good option. Manual blocking is implemented through the [Cookie Banner Consent Listener API](/reference/api/analytics-and-events/cookie-banner/cookie-banner-api). This API is used to prevent tracking technologies from running until they have consent. To get started, take a look at the examples below. ### General usage If you want to install a tracking script onto your website to display targeted ads to visitors. You could use something like the below: `` When this script is pasted into the head HTML of a page on a website it would run anytime someone visits that page, regardless of their consent status. Visitors will have cookies placed on their browser without consent. To prevent the script from running without consent, you can use the HubSpot Cookie Banner Consent Listener API to install the script when the visitor has consented to its cookies. Consent listeners are functions that run whenever the visitor submits their consent. To use this functionality, a consent listener needs to be created that adds the script to the page if the visitor has consented to advertisement cookies. ```js ``` This script will register the consent listener with the cookie banner. When consent to cookies is submitted, the consent listener will run, adding HubSpot's third party ads script to the page. ### Example: Google Tag [Google Tag or gtag.js](https://developers.google.com/tag-platform/gtagjs) can be used to add Google Analytics. For example: ```js ``` To load Google Analytics when analytics consent has been given, the gtag script needs to be added when consent is given: ```js ``` ### Example: HotJar [HotJar](https://help.hotjar.com/hc/en-us/articles/115009336727-How-to-Install-Your-Hotjar-Tracking-Code#manually) is another example of analytics tracking. For example: [HotJar](https://help.hotjar.com/hc/en-us/articles/115009336727-How-to-Install-Your-Hotjar-Tracking-Code#manually) is another example of analytics tracking. For example: ```js ``` To ensure Hotjar runs when analytics consent is given, the consent listener can be added. ```js ``` ## Third-party cookie consent In the section below, learn how to use a third party cookie consent banner to: - Distribute consent - Disable the consent collection - Manage features within HubSpot ### Disable HubSpot cookie banner To disable rendering of the HubSpot cookie banner, include the code below in a script near the top of your page’s head html. This will also disable any consent management. This flag must be set before the cookie banner is loaded. ```js window.disableHubSpotCookieBanner = true; ``` ### Set consent state of the HubSpot cookie banner Set the consent value for all HubSpot, third party, or custom products integrated with the HubSpot cookie banner. This function takes an object specifying the consent state of the visitor, saves that value as the current consent state, and distributes that consent to all scripts with attached [consent listeners](/reference/api/analytics-and-events/cookie-banner/cookie-banner-api#:~:text=Get%20privacy%20consent%20status). This value isn't saved in consent cookies managed by the cookie banner. Preserving consent state across sessions falls to the caller of this function. The function accepts a consent object with the following fields: | Field name | Type | Description | | --- | --- | --- | | `analytics` | Boolean | Grants consent to use cookies to gather analytics data from the website visitor. | | `advertisement` | Boolean | Grants consent to use cookies to help serve personalized ads to the visitor. | | `functionality` | Boolean | Grants consent to use cookies for required functionality for your website to function (e.g., authentication). | #### Example usage ```js window._hsp = window._hsp || []; window._hsp.push([ 'setHubSpotConsent', { analytics: true, advertisement: true, functionality: true, }, ]); ``` #### Example: Using a third-party cookie banner to control HubSpot cookies The following code snippet and script provide an example of how to send a custom event whenever consent state changes. Note that the functionality that you want to implement for your specific use case may vary from the example code below. Consult any third-party documentation (e.g., Google’s introduction to user consent management) for more information on how to tailor behavior to your website’s needs. ```js CustomEvent('thirdPartyConsentEvent', { detail: { analytics: true, advertisement: true, functionality: true, }, }); ``` The following script should be placed at the top of the head html of the page. ```js ``` # Implement Google Consent Mode @ Google consent mode v2 is a framework designed to integrate website visitor consent preferences with Google's advertising and analytics tools. Learn how to implement Google consent mode manually. $ reference & api/analytics-and-events/cookie-banner --- # Implement Google consent mode [Google Consent Mode v2](https://support.google.com/google-ads/answer/10000067) is a framework designed to integrate website visitor consent preferences with Google's advertising and analytics tools. With it, websites can adjust how these tools behave based on the consent status of website visitors, particularly regarding cookies and data collection. When visitors don’t consent to cookies, with [advanced consent mode](https://developers.google.com/tag-platform/security/concepts/consent-mode#basic-vs-advanced), Google services can operate in a limited mode and collect basic interactions without breaching privacy expectations. With [basic consent mode](https://developers.google.com/tag-platform/security/concepts/consent-mode#basic-vs-advanced), without consent, cookielesss pings and basic interactions will not replace tracking. ## Implement Google consent mode v2 manually If you use HubSpot's native [Google Analytics 4](https://knowledge.hubspot.com/website-pages/integrate-google-analytics-with-hubspot-content) or [Google Tag Manager](https://knowledge.hubspot.com/website-and-landing-pages/add-the-google-tag-manager-code-to-hubspot-content) integrations, learn how to [support Google consent mode v2](https://knowledge.hubspot.com/privacy-and-consent/support-google-consent-mode-v2). You will need to manually integrate Google consent mode v2 if either of the following scenarios are true: - You use a HubSpot cookie banner on an externally-hosted website or CMS. - You use a HubSpot cookie banner on the HubSpot CMS, and you use a code snippet to integrate with either Google Analytics or Google Tag Manager. ### Integrate Google consent mode v2 with Google Analytics 4 - [In HubSpot, set up a cookie consent banner](https://knowledge.hubspot.com/privacy-and-consent/customize-your-cookie-tracking-settings-and-consent-banner#set-up-and-edit-your-cookie-consent-banner) with an opt-in policy type targeting EEA, EU, and UK visitors. - On your website, configure on-page consent default, configure consent updates, and install Google Analytics (see below for an example). - Add your manual implementation immediately after the `` element of your HTML. ```js // Step 2: This snippet sets a default consent state, instructing Google's technologies how to behave if no consent is present. // Step 4: This snippet installs Google Analytics 4 ``` Follow [Google’s instructions for installing Google Analytics 4](https://support.google.com/analytics/answer/9304153#zippy=%2Cadd-the-tag-to-a-website-builder-or-cms-hosted-website-eg-hubspot-shopify-etc)when installing Google Analytics. Remember to replace G-XXXXXXXXXX with your Google Measurement ID. ### Integrate Google consent mode v2 with Google Tag Manager - [In HubSpot, set up a cookie consent banner](https://knowledge.hubspot.com/privacy-and-consent/customize-your-cookie-tracking-settings-and-consent-banner#set-up-and-edit-your-cookie-consent-banner) with an opt-in policy type targeting EEA, EU, and UK visitors. - On your website, configure on-page consent default, configure consent updates, and install Google Tag Manager (see below for an example). - Add this code as high in the `` of the page as possible. ```js // Step 2: This snippet sets a default consent state, instructing Google's technologies how to behave if no consent is present. window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } gtag('consent', 'default', { 'analytics_storage': 'denied', 'ad_storage': 'denied', 'ad_user_data': 'denied', 'ad_personalization': 'denied', 'region': ["AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IS", "IE", "IT", "LV", "LI", "LT", "LU", "MT", "NL", "NO", "PL", "PT", "RO", "SK", "SI", "ES", "SE", "UK", "CH" ] }); // Step 3: This snippet sends consent updates from the HubSpot cookie banner to Google's tags using Consent Mode v2 var _hsp = window._hsp = window._hsp || []; _hsp.push(['addPrivacyConsentListener', function(consent) { var hasAnalyticsConsent = consent && (consent.allowed || (consent.categories && consent.categories.analytics)); var hasAdsConsent = consent && (consent.allowed || (consent.categories && consent.categories.advertisement)); gtag('consent', 'update', { 'ad_storage': hasAdsConsent ? 'granted' : 'denied', 'analytics_storage': hasAnalyticsConsent ? 'granted' : 'denied', 'ad_user_data': hasAdsConsent ? 'granted' : 'denied', 'ad_personalization': hasAdsConsent ? 'granted' : 'denied' }); }]); // Step 4: This snippet installs Google Tag Manager ``` - Add this code immediately after the opening `` tag ```js // Step 4 continued: This snippet installs Google Tag Manager ``` Follow [Google’s instructions for installing Google Tag Manager](https://support.google.com/tagmanager/answer/14842164?visit_id=638761120437592858-4282928482&ref_topic=14841964&rd=2)when installing Google Tag Manager. Remember to replace `G-XXXXXXXXXX` with your Google Measurement ID. ## Implement advanced Google consent mode v2 manually This article provides an overview of how Advanced Google Consent Mode works, and how to implement it for Google Tag Manager or Google Analytics 4 using the HubSpot Cookie Banner. Note that this article assumes you’re familiar with writing HTML and JavaScript. When you implement consent mode on your website or app in advanced mode, Google tags are always loaded. Default consent is set (typically to denied) using the Consent Mode API. While consent is denied, Google tags will send [cookieless pings](https://support.google.com/google-ads/answer/10000067#Pings). When the user interacts with the HubSpot cookie banner, the consent state updates. If consent is granted, Google tags send full measurement data. To implement advanced consent mode via the HubSpot cookie banner, you must write a consent mode configuration script and install it along with the base installation code. This script will: - Set up the default consent state. - Update the consent state when the user interacts with the HubSpot cookie banner. Learn more about the [types of Google consent mode](https://developers.google.com/tag-platform/security/concepts/consent-mode#advanced_consent_mode). ### Integrate Google consent mode v2 with Google Tag Manager The example code below provides one possible way you could implement advanced consent mode on your website. The code consists of two parts: **1\. Setting up the default consent state:** this snippet configures the default consent state for consent mode. Each consent category can be set to _granted_ or _denied_. The default can also be targeted to specific regions (identified by their region code). Learn more about [setting up consent mode](https://developers.google.com/tag-platform/security/guides/consent). Make sure to configure the categories used and the regions based on your specific requirements. **2\. Updating consent when the user interacts with your cookie consent banner:** this snippet sends consent updates from the HubSpot cookie banner to Google. This code extracts the consent state from the HubSpot cookie banner consent object and updates the corresponding categories in consent mode. These mappings are a suggestion, so make sure to update them to include the categories you require. Learn more about the [categories available for the HubSpot cookie banner](/reference/api/analytics-and-events/cookie-banner/cookie-banner-api)and [consent mode categories.](https://support.google.com/tagmanager/answer/10718549) The code below is a recommendation and may not meet your regulatory requirements. Make sure to review and update the code to ensure it meets your needs. ```js ``` ### Implement advanced Google consent mode for Google Analytics 4 First, you need to [find the installation code for Google Analytics 4 in your Google Analytics account](https://support.google.com/analytics/answer/9304153#zippy=%2Cadd-the-google-tag-directly-to-your-web-pages). This code may change as Google updates its product. Typically, you must paste two scripts in the page’s `` html (similar to the scripts below). If copying these scripts, replace `MEASUREMENT_ID` with [your measurement ID](https://support.google.com/analytics/answer/12270356). To implement Advanced Consent Mode, add your consent mode configuration script before the installation code in your `` html. ```js // GA4 installation code ``` ### Implement advanced Google consent mode for Google Tag Manager The base installation code for Google Tag Manager can be found in your [Google Tag Manager account](https://support.google.com/tagmanager/answer/14842164?visit_id=638761120437592858-4282928482&ref_topic=14841964&rd=2). The Google Tag Manager installation code can change as Google updates its products. Typically, you must install Google Tag Manager with two snippets. If copying these scripts, replace `CONTAINER_ID` with [your container ID](https://support.google.com/tagmanager/answer/14842164?visit_id=638761120437592858-4282928482&ref_topic=14841964&rd=2). **1\. Head code**: Google instructs that this code be pasted into the `` tag. ```js ``` **2\. Body code**: Google instructs that this code be pasted into the `` tag. ```js ``` #### Support basic consent mode for tags without built-in consent checks If a tag does not feature built-in consent checks, it is not compatible with advanced consent mode, and you must use basic consent mode instead. Under [basic consent mode](https://developers.google.com/tag-platform/security/concepts/consent-mode#advanced_consent_mode), tags only fire when they have consent. If a tag does not have consent, it is blocked. You can implement this functionality using Google Tag Manager’s [_Additional Consent Checks_](https://support.google.com/tagmanager/answer/10718549) feature, which allows you to specify consent mode categories that must be granted for the tag to fire. This implementation requires you to: 1. **Send events when consent is updated**: To properly configure your tags to work with the _Additional Consent Checks_ feature, you need to fire [a custom event](https://support.google.com/tagmanager/answer/7679219) when consent updates. In the next step, you will use this event as a trigger for your tags, ensuring that they fire when consent is updated by the user. To send this custom event, you need to add a new line to your consent mode configuration script, in the consent update section (see below for an example). 2. **Configure additional consent checks**: For each of the tags you identified as requiring consent but lacking built-in consent checks, make the following updates to that tag’s settings: 1. [Configure additional consent checks for that tag](https://support.google.com/tagmanager/answer/10718549). Select all the consent categories that apply to your tag. 2. [Add a custom event trigger to your tag](https://support.google.com/tagmanager/answer/7679219). Make sure the event name matches the event name in your consent mode configuration script. ```js var _hsp = (window._hsp = window._hsp || []); _hsp.push([ 'addPrivacyConsentListener', function (consent) { var hasAnalyticsConsent = consent && (consent.allowed || (consent.categories && consent.categories.analytics)); var hasAdsConsent = consent && (consent.allowed || (consent.categories && consent.categories.advertisement)); gtag('consent', 'update', { ad_storage: hasAdsConsent ? 'granted' : 'denied', analytics_storage: hasAnalyticsConsent ? 'granted' : 'denied', ad_user_data: hasAdsConsent ? 'granted' : 'denied', ad_personalization: hasAdsConsent ? 'granted' : 'denied', }); // Send a custom event when consent updates gtag('event', 'hubspotConsentUpdate'); }, ]); ``` The Cookie Banner Consent Listener API, allows scripts running on a webpage to receive consent updates from the Cookie Banner. Learn more about [how to get privacy consent status](/reference/api/analytics-and-events/cookie-banner/cookie-banner-api#:~:text=Get%20privacy%20consent%20status). # Tracking code reference @ Web Analytics endpoint allows you to find and filter events associated with a CRM object of any type. $ reference & api/analytics-and-events --- # Tracking code API overview This page has been updated to document working with new custom behavioral events. For legacy custom events, please [see the legacy documentation](/reference/api/analytics-and-events/tracking-code/v1). In addition to tracking page views, the HubSpot tracking code allows you to identify visitors, track events, and manually track page views without reloading the page. The tracking code API allows you to dynamically create events and track event data in HubSpot. If your site uses the [privacy consent banner](https://knowledge.hubspot.com/privacy-and-consent/customize-your-cookie-tracking-settings-and-consent-banner), learn how to manage the cookies that are added to a visitor's browser with the [cookie banner API](/reference/api/analytics-and-events/cookie-banner/cookie-banner-api). Function calls are pushed into the `_hsq` array. For example: ```js var _hsq = window._hsq = window._hsq || []; _hsq.push(['setPath', { path string }]) ``` In this article, learn about how you can add functions to the tracking code to identify visitors, track page views, send event data, and more. ## Identifying contacts The HubSpot analytics tool identifies contacts using two pieces of data: - The usertoken, which is stored in the visitor's `hubspotutk` browser cookie. - The contact's email address. When the HubSpot tracking code tracks a visitor's action, such as a page view, it automatically associates that action with the visitor's usertoken. When you use the tracking code API to identify a visitor by email address, the analytics system will tie that email to the usertoken, allowing HubSpot to update an existing contact record or create a new one. Analytics data associated with the usertoken, such as page views and original source, will then appear on the contact record. When you know a visitor's identity (e.g., email address), you can use the [identify](#identify-a-visitor) function to set identities in the tracker. You can then send the identity to HubSpot by making a separate [trackPageView](#track-page-view) or [trackCustomBehavioralEvent](#trackcustombehavioralevent) call. When using this function keep in mind: - Avoid using a placeholder email for unknown visitors, as HubSpot will create a contact record with that placeholder email, and the visitor's `usertoken` cookie will be associated with that record. This leads to all unknown visitors being associated with your placeholder contact. - If you set an identity to a contact record and have any properties that are unique, we will drop any properties from the identity that violate uniqueness. ## Identify a visitor If your account was created before September 8, 2021 and is set up to [allow contact properties to be updated through the tracking code](https://knowledge.hubspot.com/account/prevent-contact-properties-update-through-tracking-code-api), you can also include other contact properties to be updated with this function. For accounts created after September 8, 2021, this functionality is deprecated. `_hsq.push(["identify", { {identity details} }]);` Use this endpoint to identify website visitors and contacts. - **Visitor:** refers to any website visitor, regardless of whether they’re a HubSpot contact. HubSpot does not create records for visitors like it does for contacts. - **Contact:** refers to a visitor or offline contact that has a record in HubSpot. A contact can be identified by its unique email address. To manually identify a visitor or contact, you can use either an email address or unique external ID: - **email:** identify a visitor by email address when you want to update an existing contact or create a new one. If a contact with that email exists in HubSpot, their contact record will update with the analytics data. If no contact exists at that email address, a new record will be created. - **id:** a custom external ID that identifies the visitor. Identifying a visitor with this ID will associate analytics data to that visitor. However, using the ID alone will not create a contact in HubSpot. Analytics data can only be associated with an existing contact through this method when: - the visitor was previously identified by both ID and email. - the visitor was previously identified by ID and also has a form submission associated with their record. This external ID can only be used with the HubSpot tracking code. This ID cannot be used to retrieve or update any records through any other HubSpot tools or APIs. If you know the visitor’s email address, it’s recommended to use that as the unique identifier. Similarly, you should only identify a visitor with by ID when you don’t know their email address. If you’ve previously sent analytics data to HubSpot using the visitor’s ID only, you can later include both the ID and an email address to associate the data from that ID with a contact. The existing contact will then be updated or created if no contact currently exists. ```js /* The below example gets the value of a query string parameter '?email=' and uses that to identify the visitor */ function getParameterByName(name) { var match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search); return match && decodeURIComponent(match[1].replace(/\+/g, ' ')); } var _hsq = (window._hsq = window._hsq || []); _hsq.push([ 'identify', { email: getParameterByName('email'), }, ]); /* The below example sets the email, as well as a custom property favorite_color */ var _hsq = (window._hsq = window._hsq || []); _hsq.push([ 'identify', { email: getParameterByName('email'), favorite_color: 'orange', }, ]); /* The below example sets the email and a custom external ID for the visitor. This assumes that your site includes a variable named 'user' that includes that user's ID in an 'id' property, and their email in an 'email' property. */ var _hsq = (window._hsq = window._hsq || []); _hsq.push([ 'identify', { email: user.email, id: user.id, }, ]); ``` When using this function, keep the following in mind: - This function call stores the data in the tracker, but the data is not actually passed to HubSpot with this call. The data will only be passed when tracking a page view or an event (with either the [trackPageView](#track-page-view) or [trackEvent](#events-js-api) functions). - A contact can only have one ID and/or email address associated with them. If you try to assign two IDs to one email, only the first ID will be associated with the email address. - You must include an email address to tie the data to a contact. - If your account was created before September 8, 2021 and is set up to [allow contact properties to be updated through the tracking code](https://knowledge.hubspot.com/account/prevent-contact-properties-update-through-tracking-code-api), you can also include other contact properties to be updated with this function. - This function will _not_ restore previously deleted contacts. These contacts must be [restored in HubSpot](https://knowledge.hubspot.com/records/restore-deleted-records). When restored, the cookie association won't be linked to the new contact record. ## Tracking in single-page applications The HubSpot tracking code will automatically record a page view when the code is first loaded, but you can also manually track page views in a single-page application without reloading the tracking code. You can use the [setPath](#set-page-path) and [trackPageView](#track-page-view) functions to update and track the current page. For example: ```js ``` ### Set page path `hsq.push(['setPath', { path string }])` Update the path of the current page stored in the tracker. This function should be used by single-page applications to update the current page whenever a page is loaded. After using this function to update the path, you'll need to call the [trackPageView function](#track-page-view) to track the view of the current page. Single-page applications should push a `setPath` call into `_hsq` before the tracking code loads to set the URL that gets tracked for the first page view. See the [track page view](#track-page-view) section below for an example. When calling `setPath`, you'll include the path of the current page. The set path will be treated as relative to the current domain being viewed. The path should always start with a slash. If your URL also contains parameters, these will need to be included in the path as well. View the above code for examples. When using this function, keep the following in mind: - Any path set using the **setPath** function will override the data in the referrer header. If you call **setPath** once, you'll need to use **setPath** to update the path for each view you want to track. - Repeatedly calling **setPath** will override the referrer, which can impact a contact's original source, depending on when a tracking request is made. - This function can only update the path of the URL. The domain is set automatically based on the URL of the page on load, and the path that is set using this function is always treated as relative to that detected domain. ```js Example usage: // These examples assume that the domain of the site is // www.mydomain.com // Set the path to '/' and track the page var _hsq = window._hsq = window._hsq || []; _hsq.push(['setPath', '/']); _hsq.push(['trackPageView']); // This will result in a view being recorded for // http://www.mydomain.com/ // Set the path to '/contact-us' and track the page var _hsq = window._hsq = window._hsq || []; _hsq.push(['setPath', '/contact-us']); _hsq.push(['trackPageView']); // This will result in a view being recorded for // http://www.mydomain.com/contact-us // Set the path to '/blog/post?utm_campaign=my-campaign' and track the page var _hsq = window._hsq = window._hsq || []; _hsq.push(['setPath', '/blog/post?utm_campaign=my-campaign']); _hsq.push(['trackPageView']); // This will result in a view being recorded for // http://www.mydomain.com/blog/post?utm_campaign=my-campaign // Set the path to '/#/about-us' and track the page var _hsq = window._hsq = window._hsq || []; _hsq.push(['setPath', '/#/about-us']); _hsq.push(['trackPageView']); // This will result in a view being recorded for // http://www.mydomain.com/#/about-us ``` ### Track page view `_hsq.push(['trackPageView']);` Track the page view for the current page. This function is automatically called when the tracking code is loaded on a page, but you can manually call this function to track subsequent views in a single page application. Calling this function manually before or during the initial page load could lead to duplicate views being tracked. This function does not support any arguments. The page title tracked will be the current value of `document.title`. The URL that gets tracked is based on one of the following: - The path set using the [setPath function](/reference/api/analytics-and-events/tracking-code#set-page-path). If your site is built as a single-page application, this function is the preferred method of setting the tracked path. View the [setPath](/reference/api/analytics-and-events/tracking-code#set-page-path) section above for disclaimers about the function. - If `setPath` has never been called, the tracked URL will be the referrer HTTP header in the request being made by the visitor's browser to HubSpot's tracking servers ([modifying the browser history state](https://developer.mozilla.org/en-US/docs/Web/API/History_API#Adding_and_modifying_history_entries) would update the value used for that header). The referrer header will not support URL fragments (anything after the # in the URL), as fragments are not included in the referrer header. ```js Example usage: // Track a new page using setPath: // Update the path stored in the tracker: var _hsq = window._hsq = window._hsq || []; _hsq.push(['setPath', '/#/new-page']); // Track the page view for the new page _hsq.push(['trackPageView']); // Track a new page by updating the browser state: // Update the browser state, showing "updated.html" in the browser address bar var stateObj = { foo: 'updated' }; history.pushState(stateObj, "updated page", "updated.html"); //Track the page view for the new page, '/updated.html' var _hsq = window._hsq = window._hsq || []; _hsq.push(['trackPageView']); ``` When using this function, keep in mind the following: - While you can't prevent this function from being automatically called when the tracking code loads, you can control the URL recorded for the page by pushing a `setPath` call into `_hsq` before the tracking code is loaded. - If your site is a single-page application, you should avoid including a `` tag in your page. If your page uses `` tags, you'll need to use the [setPath](/reference/api/analytics-and-events/tracking-code#set-page-path) function to update the path of your new pages, as the canonical URL set from the link tag will override any detected URL if you're only updating the browser history state. ## Privacy policy in tracking If your site has a [privacy consent banner](https://knowledge.hubspot.com/privacy-and-consent/customize-your-cookie-tracking-settings-and-consent-banner) you can use functions to check and manage cookies placed into the visitor's browser. Learn more about [managing privacy consent banner cookies](/reference/api/analytics-and-events/cookie-banner/cookie-banner-api). ## Get cross-domain linking parameters `_hsq.push(['addIdentityListener', function(hstc, hssc, hsfp) {}])` The HubSpot tracking code can be used across multiple sites with separate domains. This function will allow you to get the query parameters required to create links that will allow you to track your visitors [across those separate domains](https://knowledge.hubspot.com/reports/set-up-sources-tracking#track-multiple-domains-with-cross-domain-linking). These query parameters are used by the HubSpot tracking code to identify a visitor across domains by ensuring that the separate cookies for the separate domains are merged to a single tracked visitor. You can also use cross-domain query parameters in links that are dynamically added to the page after the tracking code is loaded. Cross-domain links are only needed when linking to a distinct domain (e.g., _`domain-one.com`_ and _`domain-two.com`_) that is also being tracked for a single HubSpot account. You do not need cross-domain link parameters when tracking visits between subdomains (e.g., _`www.domain-one.com`_ and _`blog.domain-one.com`_). ```js // Get the cross-domain query parameters and store them in a string, //so that they can be appended to links as needed. _hsq.push([ 'addIdentityListener', function (hstc, hssc, hsfp) { // Add these query parameters to any links that point to a separate tracked domain crossDomainTrackingParams = '&__hstc=' + hstc + '&__hssc=' + hssc + '&__hsfp=' + hsfp; }, ]); ``` ## Reapply analytics event handlers `_hsq.push(['refreshPageHandlers'])` This function reapplies any analytics event handlers that are set up in the analytics settings for the HubSpot account. This would include reapplying any [clicked element events](https://knowledge.hubspot.com/analytics-tools/create-codeless-custom-behavioral-events) that have been set up. You can use this function to automatically reapply click handlers when content on the page is updated, such as updating a section of content or displaying a modal window on the page. This functionality is automatically triggered as part of the [`setPath`](/reference/api/analytics-and-events/tracking-code#set-page-path) function, so you'll only need to use this function when updating the content without updating the tracked page URL. ```js // Reapply event handlers after updating the page content _hsq.push(['refreshPageHandlers']); ``` ## Send custom event data (Enterprise only) If you have an _Enterprise_ subscription, you can create custom events to track visitor interactions on your website through the tracking code. For example, you can create a custom JavaScript event that listens for a button click, then sends event data to HubSpot based on the visitor and interaction. - Custom JavaScript events must be [created in HubSpot's UI](https://knowledge.hubspot.com/reports/create-custom-behavioral-events-with-the-code-wizard#create-events-with-a-javascript-code-snippet) before they can receive data. Once created, they can be updated in HubSpot or via the [custom event definitions API](/reference/api/analytics-and-events/custom-events/custom-event-definitions). - Custom behavioral events can be created either in HubSpot's UI or via the [custom event definitions API](/reference/api/analytics-and-events/custom-events/custom-event-definitions). Once the event is created, you can use the `trackCustomBehavioralEvent` function to send event data to custom JavaScript events and custom behavioral events. ### trackCustomBehavioralEvent `_hsq.push(["trackCustomBehavioralEvent", { {event details} }]);` Use this function to send custom event data via the tracking code. For example, the code below would trigger a custom behavioral event with the internal name `pe123456_course_registration` when a visitor clicks an element with the `signUp` ID. ```js document.getElementById('signUp').onclick = function () { console.log('Button clicked'); if (true) { window._hsq.push([ 'trackCustomBehavioralEvent', { name: 'pe123456_course_registration', properties: { custom_event_property: 'success!', }, }, ]); } }; ``` | Field | Type | Description | | --- | --- | --- | | `name` | String | The internal name of the event that you created in HubSpot (e.g., `pe123456_my_event_name`). You can retrieve an event's internal name either via the [custom event definitions API](/reference/api/analytics-and-events/custom-events/custom-event-definitions#get-%2Fevents%2Fv3%2Fevent-definitions%2F%7Beventname%7D) or in HubSpot's UI, as shown below. | | `properties` | Object | A list of key-value pairs, with one key-value pair per property. You can retrieve an event's property names via the [custom event definitions API](/reference/api/analytics-and-events/custom-events/custom-event-definitions#get-%2Fevents%2Fv3%2Fevent-definitions%2F%7Beventname%7D) or in HubSpot's UI, as shown below. | Because the function is handled by the tracking code, event completions will automatically be associated with the visitor's corresponding contact record via their `usertoken` cookie. To find the internal event name and property names: - In your HubSpot account, navigate to **Data Management** > **Custom Events**. - Click the **name** of the event that you want to track. - At the top, under the event title, you can copy the **Internal name** for targeting the event in your JavaScript code. - Click the **Property info** tab, then view the **Internal Name** column to retrieve the values needed for updating event properties in your JavaScript code. - You can also edit the event's JavaScript code and manage its properties by clicking **Edit event** in the upper right. ### Customizing the tracking code When creating a JavaScript event in HubSpot, HubSpot will provide a code snippet that you can customize and add to your tracking code without needing to navigate into your account settings. ```js // Replace 'false' with your custom logic that returns 'true' when the event should be tracked. if (false) { _hsq.push([ 'trackCustomBehavioralEvent', { name: 'pe123456_event_name', properties: { // Event properties to update event_property: 'string', }, }, ]); } ``` You can also customize your tracking code manually in your account settings: - In your HubSpot account, click the **settings icon** in the top navigation bar. - In the left sidebar menu, navigate to **Tracking & Analytics** > **Tracking Code**. - Under the _Embed code_ section, click **Customize JavaScript**. - To add a new JavaScript snippet, click **Add custom JavaScript**. Or, update an existing snippet by hovering over the **existing row**, then clicking **Edit**. JavaScript added to an event during creation will not appear in your account settings. To update event-specific JavaScript, you'll need to edit the event itself. - In the right sidebar, add your custom JavaScript, then click **Save**. Your tracking code will be updated automatically. # Workflows | Custom Code Actions @ Instructions for using custom code actions in workflows $ reference & api/automation --- # Custom code workflow actions In workflows, use the _Custom code_ action to write and execute JavaScript or Python (_in beta_). With custom code actions, you can extend workflow functionality within and outside of HubSpot. To see examples of common custom code actions, view [HubSpot's Programmable Automation Use Cases](https://www.hubspot.com/programmable-automation-use-cases). Custom code actions support JavaScript using the [Node.js runtime framework](https://docs.aws.amazon.com/lambda/latest/dg/lambda-nodejs.html). If you're using Python for your custom code action, the custom code action will use [Python runtime framework](https://docs.aws.amazon.com/lambda/latest/dg/lambda-python.html). When an action executes, the runtime compute is managed through a serverless function by HubSpot and [AWS Lambda](https://aws.amazon.com/lambda/features/#:~:text=AWS%20Lambda%20is%20a%20serverless,scale%2C%20performance%2C%20and%20security.). If you encounter any general issues implementing your custom code action, you can reach out to [HubSpot support](https://knowledge.hubspot.com/help-and-resources/get-help-with-hubspot). However, if you're facing any issues with your written custom code, it's recommended to search and post on the [HubSpot Developer's Forum](https://community.hubspot.com/t5/HubSpot-Developers/ct-p/developers) to get help with troubleshooting your code. ### Node.js supported libraries If you're using Node.js, the following libraries are available for use within the code action. These libraries can be loaded using the normal `require()` function at the top of your code. - @hubspot/api-client ^10 - async ^3.2.0 - aws-sdk ^2.744.0 - axios ^1.2.0 - lodash ^4.17.20 - mongoose ^6.8.0 - mysql ^2.18.1 - redis" ^4.5.1 - request" ^2.88.2 - bluebird ^3.7.2 - random-number-csprng ^1.0.2 - googleapis ^67.0.0 The v4 Associations API is supported in Version 9.0.0 or later of the NodeJS HubSpot Client and in Version 8 of the NodeJS HubSpot Client. ## Python supported libraries If you're using Python, you can load the following libraries with an import statement at the top of your code. The import statement should be formatted as `from [libraryname] import [item]`, such as `from redis.client import redis`. - requests 2.28.2 - @hubspot/api-client ^8 - google-api-python-client 2.74.0 - mysql-connector-python 8.0.32 - redis 4.4.2 - nltk 3.8.1 If you're using anything from the standard library, you can use `import`, such as `import os`. ## Get started Use the code samples below to begin using custom code workflow actions. ### Code samples ```js const hubspot = require('@hubspot/api-client'); exports.main = async (event, callback) => { /***** How to use secrets Secrets are a way for you to save API keys or private apps and set them as a variable to use anywhere in your code Each secret needs to be defined like the example below *****/ const hubspotClient = new hubspot.Client({ accessToken: process.env.SECRET_NAME }); let phone; try { const ApiResponse = await hubspotClient.crm.contacts.basicApi.getById(event.object.objectId, ["phone"]); phone = ApiResponse.properties.phone; } catch (err) { console.error(err); // We will automatically retry when the code fails because of a rate limiting error from the HubSpot API. throw err; } /***** How to use inputs Inputs are a way for you to take data from any actions in your workflow and use it in your code instead of having to call the HubSpot API to get that same data. Each input needs to be defined like the example below *****/ const email = event.inputFields['email']; /***** How to use outputs Outputs are a way for you to take data from your code and use it in later workflows actions Use the callback function to return data that can be used in later actions. Data won't be returned until after the event loop is empty, so any code after this will still execute. *****/ callback({ outputFields: { email: email, phone: phone } }); } // A sample event may look like: { "origin": { // Your portal ID "portalId": 1, // Your custom action definition ID "actionDefinitionId": 2, }, "object": { // The type of CRM object that is enrolled in the workflow "objectType": "CONTACT", // The ID of the CRM object that is enrolled in the workflow "objectId": 4, }, "inputFields": { // The property name for defined inputs }, // A unique ID for this execution "callbackId": "ap-123-456-7-8" } ``` ```py import os from hubspot import HubSpot from hubspot.crm.contacts import ApiException def main(event): # How to use secrets # Secrets are a way for you to save API keys or private apps and set them as a variable to use anywhere in your code # Each secret needs to be defined like the example below hubspot = HubSpot(access_token=os.getenv('SECRET_NAME')) phone = '' try: ApiResponse = hubspot.crm.contacts.basic_api.get_by_id(event.get('object').get('objectId'), properties=["phone"]) phone = ApiResponse.properties.get('phone') except ApiException as e: print(e) # We will automatically retry when the code fails because of a rate limiting error from the HubSpot API. raise # How to use inputs # Inputs are a way for you to take data from any actions in your workflow and use it in your code instead of having to call the HubSpot API to get that same data. # Each input needs to be defined like the example below email = event.get('inputFields').get('email') # How to use outputs # Outputs are a way for you to take data from your code and use it in later workflows actions # Use the callback function to return data that can be used in later actions. # Data won't be returned until after the event loop is empty, so any code after this will still execute. return { "outputFields": { "email": email, "phone": phone } } # A sample event may look like: # { # "origin": { # # Your portal ID # "portalId": 1, # # Your custom action definition ID # "actionDefinitionId": 2, # }, # "object": { # # The type of CRM object that is enrolled in the workflow # "objectType": "CONTACT", # # The ID of the CRM object that is enrolled in the workflow # "objectId": 4, # }, # "inputFields": { # # The property name for defined inputs # }, # # A unique ID for this execution # "callbackId": "ap-123-456-7-8" # } ``` ## Create a custom code action To add a custom code action to a workflow: - In your HubSpot account, navigate to **Automation** \> **Workflows**. - Click the **name** of a workflow, or [create a new workflow](https://knowledge.hubspot.com/workflows/create-workflows). - Click the **\+** **plus** **icon** to add a workflow action. - In the right panel, select **Custom code**. - In the right panel, set up your action: - By default, custom code actions will use **Node.js**. If you’re in the Python beta and want to build your action with Python, click the **Language** dropdown menu, then select **Python**. - To add a new [secret](#secret), such as a [private app access token](/guides/apps/private-apps/overview), click **Add secret**. The app must include the respective scopes of any data that you're trying to pull from HubSpot, such as `contacts` or `forms`. Learn more about [HubSpot private apps](/guides/apps/private-apps/overview). - In the dialog box, enter the **Secret name** and **Secret value**. - Click **Save**. You can now select this secret in future custom code actions. - To edit or delete existing secrets, click **Manage secrets**. - To include properties in your custom code, click the **Choose property** dropdown menu, then select a **property**. You can use existing properties or [previously formatted property values](https://knowledge.hubspot.com/workflows/format-your-data-with-workflows) in the workflow. After selecting your property, enter a Property **name** to use in your code. Learn how to [reference a property in your custom code](#add-hubspot-properties-to-your-custom-code). - To add another property, click **Add property**. Each property can only be added once and must have a unique _Variable ID_. You can use up to 50 properties with your custom code. - To delete a property, click the **delete** icon. - In the **code field**, enter your JavaScript or Python. - To define data outputs that can be used as inputs later in the workflow, for example with a [_Copy property value_](https://knowledge.hubspot.com/workflows/choose-your-workflow-actions#copy-a-property-value) action: - Under _Data outputs_, click the **Data type** dropdown menu, and select a type of data. - In the **Name** field, enter a name for the data output. - To add multiple outputs, click **Add output**. - Click **Save**. The code field will not display lint errors when using Python. When building custom code actions, keep the following in mind: - The `def main(event):` function is called when the code snippet action is executed. - The event argument is an object containing details for the workflow execution. - The `callback()` function is used to pass data back to the workflow. It should be called in the `exports.main` function. This can only be used with Node.js. The `event` object will contain the following data: ```json //example payload { "origin": { // Your portal ID "portalId": 1, // Your custom action definition ID "actionDefinitionId": 2 }, "object": { // The type of CRM object that is enrolled in the workflow "objectType": "CONTACT", // The ID of the CRM object that is enrolled in the workflow "objectId": 4 }, // A unique ID for this execution. "callbackId": "ap-123-456-7-8" } ``` ## Test the action When adding a custom code action to a workflow, you can test the action to ensure that your code runs as expected before turning the workflow on. When testing a custom code action, you'll start by selecting a record to test the code with, then run the code. This test will run only the code in your custom action, not any of the other actions in the workflow. When the code is finished running, you'll be able to view the code outputs and the log of your test. When testing your custom code, the code will run and any changes will apply to the selected test record. It's recommended to create a dedicated test record if you want to avoid updating your live records. To test a custom code action: - In the workflow timeline, click the **custom code action**. - At the bottom of the right sidebar, click **Test action** to expand the testing section. - Select a record to test your code with by clicking the **\[Object\]** dropdown menu, then selecting a **record**. - If you're using [previously formatted property values](https://developers.hubspot.com/workflows/format-your-data-with-workflows) in the workflow, enter a **test value** for the formatted data. - To run the code, click **Test**. - In the dialog box, confirm that you want to test your code against the selected record by clicking **Test**. - Once your code is done running, the sidebar will display the results of your test: - **Status:** the success or failure status of your custom code action. - **Data outputs:** the values that resulted for your defined data outputs. An alert will display next to any outputs that the code generated which weren't defined either in the _Data outputs_ section or in the code editor. You'll need to add those outputs in order to use them later in the workflow. - **Logs:** information about the test itself, such as how much memory the action took to execute and the total runtime. - To update your custom code action, click **Create** **action** to expand the action editor. Continue to update and test your code as needed. - When you're done testing the action, click **Save** to save your changes. ## Secrets There are times you will want your code to reference something that shouldn't be widely shared. Most often, this is a means of authentication, like a [private app access token](/guides/apps/private-apps/overview). You can manage the secrets your function has access to directly in the workflow action definition. When using multiple secrets within a custom code, the total length of all secret values must not exceed 1000 characters. Once added, the secrets will be available as environment variables, which you can access in the custom code, as shown below: ```js const hubspot = require('@hubspot/api-client'); exports.main = (event, callback) => { return callback(processEvent(event)); }; function processEvent(event) { // secrets can be accessed via environment variables const hubspotClient = new hubspot.Client({ accessToken: process.env.secretName, }); hubspotClient.crm.contacts.basicApi .getById(event['object']['objectId'], ['email', 'phone']) .then((results) => { let email = results.body['properties']['email']; let phone = results.body['properties']['phone']; // ... }) .catch((err) => { console.error(err); }); } ``` ## Add HubSpot properties to your custom code At times, you may need to fetch object properties in your custom code action. Rather than using HubSpot's APIs, you can add these properties directly in the workflow action definition. Add properties and set property names to reference properties in your code. You can add up to 50 properties in each custom code action. Once added, the property can be referenced in the custom code. ```js const email = event.inputFields['email']; ``` ```py email = event.get('inputFields').get('email') ``` ## Logging An important tool for developers is ability to print outputs from their code. It helps you debug issues and provide better support for your end users. To see the output of the logs, you can find them in the "History" tab of the workflow. ## How to Define Outputs In the function, define the output fields you want to use later in the workflow. Then, in the right sidebar, select the data output type (e.g., number, string, boolean, datetime, enum, date phone number) and input the field you want to output. The output fields should be part of a json object formatted accordingly, depending on the language used: ```js callback({ outputFields: { email: email, phone: phone, }, }); ``` ```py return { "outputFields": { "email": email, "phone": phone } } ``` You can then use the output from your code action as in input to the _Copy property value_ action. This removes the need to make another API call to store the value as a property on your object. Do take note of the following when defining your output: - If your data output type is in string format, the limit for string output values is 65,000 characters. Exceeding this limit will result in an `OUTPUT_VALUES_TOO_LARGE` error. - If you're using the _Copy property value_ action, please also take note of [compatible source and target properties](https://knowledge.hubspot.com/workflows/compatible-source-and-target-properties-for-copying-property-values-in-workflows). - When copying to date properties: - If you're copying an output to a datetime property, the output will need to be in [UNIX millisecond format](https://developers.hubspot.com/docs). - If you're copying an output to date property instead of a datetime, the output will need to be in [UNIX millisecond format](https://developers.hubspot.com/docs) and the time on the date will need to be set to midnight UTC. `currentDate.setUTCHours(0,0,0,0)` ## Limitations Custom code actions must finish running within 20 seconds and can only use up to 128 MB of memory. Exceeding either of these limits will result in an error. ## Retries You may need to fetch object properties using the HubSpot API or to call other HubSpot API endpoints in your custom code action. Like any other API call, you'll still need to comply with [HubSpot API rate limits](/guides/apps/api-usage/usage-details). - If you're using Node.js and encounter a rate limiting error but you want HubSpot to retry your call, you'll need to throw the error in the `catch` block of your custom code action. - If you're using Python and encounter a rate limiting error but you want HubSpot to retry your call, you'll need to raise the error in the `except` block of your custom code action. If the call fails due to a rate limiting error, or a 429 or 5XX error from [axios](https://www.npmjs.com/package/axios) or [@hubspot/api-client](https://www.npmjs.com/package/@hubspot/api-client), HubSpot will reattempt to execute your action for up to three days, starting one minute after failure. Subsequent failures will be retried at increasing intervals, with a maximum gap of eight hours between tries. ## Caveats If you're using Node.js for your custom code, take note of the following caveats: - **Generating random numbers:** it's common to use [Math.random](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random) to generate random numbers but users may see the same numbers generated across different executions. This is because Math.random is seeded by the current time. Since HubSpot may enroll many objects into a workflow at the same time and clear the state on every execution, different executions end up seeding Math.random in the same way. Instead, you can use of [random-number-csprng 1.0.2](https://www.npmjs.com/package/random-number-csprng) library which guarantees cryptographically secure pseudo-random number generation. - **Variable re-use:** to save memory, any variables declared outside the `exports.main` function may be re-used for future executions of the custom code action. This is useful when connecting to external services like a database, but any logic or information that needs to be unique to each execution of the custom code action should be inside the `exports.main` function. If you're using Python for your custom code, take note of the following caveats: - **Variable re-use:** similar to the above, any variables declared outside the `def main` function may be re-used for future executions of the custom code action. - If you've declared a variable outside the `def main` function but do not plan on altering it, you can reference the variable directly. - If you plan on altering a variable, you can declare the variable within the `def main` function with a global keyword before referencing it. ```hubl a = 1 def main(event): global a a += 1 ``` # Conversations | Chat Widget SDK @ HubSpot's Live Chat widget allows you to chat with customers on your website. With the Chat Widget SDK, you can customize the behavior of the chat widget. $ reference & api/conversations --- # Conversations SDK To chat with customers and leads on your website using HubSpot's conversation inbox, you can set up a [live chat widget](https://knowledge.hubspot.com/inbox/chat-with-your-website-visitors). With the conversations SDK, you can provide a more tailored experience for visitors by customizing the behavior of the chat widget. At a high level, the conversations SDK enables you to do the following: - [Configure chat widget settings](#configure-conversations-settings) - [Control the widget's behavior](#widget-behavior) - [Clear chat cookies](#clear-chat-cookies) - [Listen for and respond to widget events](#chat-events)[](#widget-status) ## Initializing The API is housed in the `window.HubSpotConversations` object, which provides access to all available methods. The object is created by the [HubSpot tracking code](https://knowledge.hubspot.com/reports/install-the-hubspot-tracking-code), but may not be available immediately on page load. To defer accessing the API until it's initialized, you can use the `window.hsConversationsOnReady` helper. `window.hsConversationsOnReady` is an optional field you can define on the `window` object which enables you to specify code to be executed as soon as the widget becomes available. This field takes an array functions to be executed once the API has been initialized. ```js ``` ## Configure conversations settings `hsConversationsSettings` This optional object enables you to provide some configuration options to the widget before it initializes. ```js window.hsConversationsSettings = { loadImmediately: false, inlineEmbedSelector: '#some-id', enableWidgetCookieBanner: true, disableAttachment: true, }; window.hsConversationsOnReady = [ () => { window.HubSpotConversations.widget.load(); }, ]; ``` | Field | Type | Default | Description | | --- | --- | --- | --- | | `loadImmediately` | Boolean | `true` | Whether the widget should implicitly load or wait until the `widget.load` method is called. | | `inlineEmbedSelector` | String | `""` | Specify a selector (`#some-id`) to embed the chat widget in a specific location on the page. Widget will be embedded inline within that DOM node and will remain open until it is removed with `widget.remove`. Learn more about [styling embedded chat widgets](#inline-embed-styling). | | `enableWidgetCookieBanner` | Enumeration | `false` | Control behavior of the cookie banner for all chat widgets on the page. Options include: | | `disableAttachment` | Boolean | `false` | Whether to hide the upload attachment button in the chat widget. | | `disableInitialInputFocus` | Boolean | `false` | Whether to automatically prevent focusing on the widget's input field after an inline embedded widget is initially loaded. | | `avoidInlineStyles` | Boolean | `false` | When set to `true`, injects a link tag with externally hosted CSS instead of a direct dynamic insertion of a style tag. | | `hideNewThreadLink` | Boolean | `false` | When set to `true`, prevent the _To start a new chat, click here_ link from appearing below the _Your chat has ended_ text when a chat has ended. | ### Inline embed styling When the widget is embedded in a specific location using `inlineEmbedSelector`, several DOM elements are added and can be styled (e.g. height, width, border). For example, if you embed the chat widget using the `#some-id` selector, it would be loaded with the following containers and IDs: ```html ``` ```css #hubspot-conversations-inline-iframe { width: 300px; height: 500px; border: none; } ``` ## Widget behavior `HubSpotConversations.widget` The widget object contains a number of methods that allow you to manipulate the chat widget on your page, including: - [widget.load](#widget-load) - [widget.refresh](#widget-refresh) - [widget.open](#widget-open) - [widget.close](#widget-close) - [widget.remove](#widget-remove) - [widget.status](#widget-status) Below, learn more about each method. ### widget.load The `widget.load` method handles the initial load on the page. This method is only necessary if you set `loadImmediately` to `false`. Otherwise, the widget will load itself automatically. This method is throttled to one call per second. ```js window.HubSpotConversations.widget.load(); /* ... */ // Force the widget to load in an open state window.HubSpotConversations.widget.load({ widgetOpen: true }); ``` | Field | Type | Default | Description | | --- | --- | --- | --- | | `widgetOpen` | Boolean | `false` | Whether the widget should load in an open state. | ### widget.refresh The `widget.refresh` method handles refreshing and re-rendering the widget's information, given the current page URL. This method can be useful for chat widgets embedded in single-page applications when you need to refresh the widget on route changes. This method also enables you to [specify different chat widgets on different page routes](#example). If you call `widget.refresh` on a route where there is no chat widget, and the user isn't engaged in a chat, the widget will be removed. It will not remove the widget when there is a currently active chat. This method is throttled to one call per second. ```js window.HubSpotConversations.widget.refresh(); /* ... */ // Force the widget to open to a specific chat flow window.HubSpotConversations.widget.refresh({ openToNewThread: true }); ``` | Field | Type | Default | Description | | --- | --- | --- | --- | | `openToNewThread` | Boolean | `false` | Whether to force a new thread to be created. | #### Example Using this method, you could create buttons and links to open specific chatflows on a page by adding query parameters to the page URL. For example, you could add the following code to your pages to generate the buttons: ```html ``` Then, in each chat's [target settings](https://knowledge.hubspot.com/chatflows/create-a-live-chat#2-target---decide-where-the-live-chat-should-appear), you would set the chat to display when the query parameter matches the one you've set in your button code. ### widget.open The `widget.open` method opens the widget if it is not already open or isn't currently loaded. ```js window.HubSpotConversations.widget.open(); ``` ### widget.close The `widget.close` method closes the widget if it isn't already closed. ```js window.HubSpotConversations.widget.close(); ``` ### widget.remove The `widget.remove` method removes the widget from the page. If the widget isn't present on the page, this method does nothing. The widget will display again on page refresh or if `widget.load` is invoked. ```js window.HubSpotConversations.widget.remove(); ``` ### widget.status The `widget.status` method returns an object containing properties related to the current status of the widget. ```js const status = window.HubSpotConversations.widget.status(); if (status.loaded) { window.HubSpotConversations.widget.refresh(); } else { window.HubSpotConversations.widget.load(); } ``` | Field | Type | Default | Description | | -------- | ------- | ------- | ------------------------------------- | | `loaded` | Boolean | `false` | Whether the widget iframe has loaded. | ## Clear chat cookies The `clear` method deletes cookies related to the chat widget and returns it to its default state on subsequent load. The chat widget creates several cookies to preserve its state across site visits and page refreshes. These cookies are scoped to the domain of the page hosting the widget, and are used to support the following features: - Referencing historical conversations. - Persisting the open state of the chat widget across page loads. - Persisting the open state of the welcome message across page loads. The following cookies are cleared with this method: - `messagesUtk` - `hs-messages-is-open` - `hs-messages-hide-welcome-message` For more information about these cookies, see [check out HubSpot's Knowledge Base](https://knowledge.hubspot.com/reports/what-cookies-does-hubspot-set-in-a-visitor-s-browser). ```js window.HubSpotConversations.clear(); ``` Additionally, you can pass `{resetWidget:true}` to the `clear()` function to clear all chat related cookies, remove the widget from the page, and create a new instance of the chat widget. ```js window.HubSpotConversations.clear({ resetWidget: true }); ``` ## Chat events The chat widget emits various events you can listen and respond to throughout its lifecycle. These events include: - [conversationStarted](#conversationstarted) - [conversationClosed](#conversationclosed) - [userSelectedThread](#userselectedthread) - [unreadConversationCountChanged](#unreadconversationcountchanged) - [contactAssociated](#contactassociated) - [userInteractedWithWidget](#userinteractedwithwidget) - [widgetLoaded](#widgetloaded) - [quickReplyButtonClick](#quickreplybuttonclick) - [widgetClosed](#widgetclosed) To register and remove event listeners, you'll use `on` and `off`, as shown below. ```js const handleEvent = (eventPayload) => console.log(eventPayload); window.HubSpotConversations.on('conversationStarted', handleEvent); /* ... */ window.HubSpotConversations.off('conversationStarted', handleEvent); ``` Learn more about each event below. ### conversationStarted The `conversationStarted` event triggers when a new conversation has been successfully started. ```js window.HubSpotConversations.on('conversationStarted', (payload) => { console.log( `Started conversation with id ${payload.conversation.conversationId}` ); }); ``` | Field | Type | Description | | --- | --- | --- | | `payload.conversation.conversationId` | Number | The thread ID of the conversation that was started. You can use this ID when making calls to the [conversations API](/guides/api/conversations/inbox-and-messages). | ### conversationClosed The `conversationClosed` event triggers when a new conversation has [marked as closed](https://knowledge.hubspot.com/inbox/use-the-conversations-inbox#reply-and-comment:~:text=To%20mark%20the%20conversation%20as%20closed) from the conversations inbox. Site visitors minimizing or closing the chat widget will not trigger this event. For that event, use [widgetClosed](#widgetclosed) instead. ```js window.HubSpotConversations.on('conversationClosed', (payload) => { console.log( `Conversation with id ${payload.conversation.conversationId} has been closed!` ); }); ``` | Field | Type | Description | | --- | --- | --- | | `payload.conversation.conversationId` | Number | The thread ID of the conversation that was started. You can use this ID when making calls to the [conversations API](/guides/api/conversations/inbox-and-messages). | ### userSelectedThread The `userSelectedThread` event triggers when creating a thread or selecting an existing thread. ```js window.HubSpotConversations.on('userSelectedThread', (payload) => { console.log( `User selected thread with ID ${payload.conversation.conversationId}!` ); }); ``` | Field | Type | Description | | --- | --- | --- | | `payload.conversation.conversationId` | Number | The thread ID of the conversation that was started. You can use this ID when making calls to the [conversations API](/guides/api/conversations/inbox-and-messages). | ### unreadConversationCountChanged The `unreadConversationCountChanged` event is triggered when the number of conversations with unread messages increases or decreases. ```js window.HubSpotConversations.on('unreadConversationCountChanged', (payload) => { console.log(`New unread count is ${payload.unreadCount}!`); }); ``` | Field | Type | Description | | --- | --- | --- | | `unreadCount` | Number | The total number of conversations with at least one unread message. | ### contactAssociated The `contactAssociated` event is triggered when the visitor is associated with a contact in the CRM. ```js window.HubSpotConversations.on('contactAssociated', (payload) => { console.log(payload.message); }); ``` | Field | Type | Description | | --- | --- | --- | | `message` | String | A confirmation message that the visitor has been associated with a contact. | ### userInteractedWithWidget The `userInteractedWithWidget` event is triggered when the visitor interacts with the widget, such as clicking to open the widget or closing the initial welcome message. ```js window.HubSpotConversations.on(‘userInteractedWithWidget’, payload => { console.log(payload.message); }); ``` | Field | Type | Description | | --- | --- | --- | | `message` | String | A confirmation message that the visitor has been interacted with the widget. | ### widgetLoaded The `widgetLoaded` event is triggered when the widget iframe is loaded. ```js window.HubSpotConversations.on(‘widgetLoaded’, payload => { console.log(payload.message); }); ``` | Field | Type | Description | | --- | --- | --- | | `message` | String | A confirmation message that the widget iframe has loaded. | ### quickReplyButtonClick The `quickReplyButtonClick` event is triggered when the visitor clicks a [quick reply](https://knowledge.hubspot.com/chatflows/a-guide-to-bot-actions#ask-a-question) in a bot conversation. | Field | Type | Description | | --- | --- | --- | | `value` | Array | An array containing the text of the quick reply option that was clicked. | ```js window.HubSpotConversations.on('quickReplyButtonClick', (event) => { console.log(`The text content of the clicked button is ${payload.value[0]}`); }); ``` In the example screenshot above, the bot chatflow contains three quick reply options. If the user selects _Learn more_, the resulting event payload would be: ```json // Example event payload when a quick reply option is selected { "name": "QUICK_REPLIES", "multiSelect": false, "value": ["Learn more"] } ``` ### widgetClosed The `widgetClosed` event is triggered when the visitor closes the chat widget. ```js window.HubSpotConversations.on('widgetClosed', (event) => { console.log(event); }); ``` | Field | Type | Description | | --- | --- | --- | | `message` | String | A confirmation message that the visitor has closed the chat widget. | # Access and manage Sensitive Data @ Learn how to gain access to and manage Sensitive Data via API. $ reference & api/crm --- # Access and manage Sensitive Data With [Sensitive Data properties](https://knowledge.hubspot.com/properties/store-sensitive-data), HubSpot users in _Enterprise_ accounts can store Sensitive Data or Highly Sensitive Data and use the data in HubSpot tools. For example, you can create a _Passport Number_ property and filter contacts in a list based on its values, or create an _SSN_ property to store social security numbers. Refer to the [Sensitive Data terms](https://legal.hubspot.com/sensitive-data-terms) page to understand which types of Sensitive Data you can store and which HubSpot tools can access each type of Sensitive Data. Existing HubSpot APIs and apps will continue to work as expected, but if you have an app that needs access to Sensitive Data or Highly Sensitive Data, you must update the app’s scopes and require app users to authorize and install the app. Once your app gains access, you can use [certain APIs](#manage-sensitive-data) to manage your Sensitive Data. ## Sensitive Data scopes For an app to access Sensitive Data, it must have the sensitive scopes for each applicable object. For example, if your app needs to read and update Sensitive Data on companies and contacts, you’ll need to add the company and contact read and write sensitive scopes. If you want to access Highly Sensitive Data, you'll need [additional scopes](#highly-sensitive-scopes-beta). Sensitive and highly sensitive scopes are not required to retrieve schema information for a Sensitive Data or Highly Sensitive Data property, but are required to create or edit Sensitive Data or Highly Sensitive data properties and to access the property values. ### Sensitive scopes The sensitive scopes are: - `crm.objects.contacts.sensitive.read` - `crm.objects.contacts.sensitive.write` - `crm.objects.companies.sensitive.read` - `crm.objects.companies.sensitive.write` - `crm.objects.deals.sensitive.read` - `crm.objects.deals.sensitive.write` - `crm.objects.appointments.sensitive.read` - `crm.objects.appointments.sensitive.write` - `crm.objects.custom.sensitive.read` - `crm.objects.custom.sensitive.write` - `tickets.sensitive` (Both read and write access) ### Highly sensitive scopes The highly sensitive scopes are: - `crm.objects.contacts.highly_sensitive.read` - `crm.objects.contacts.highly_sensitive.write` - `crm.objects.companies.highly_sensitive.read` - `crm.objects.companies.highly_sensitive.write` - `crm.objects.deals.highly_sensitive.read` - `crm.objects.deals.highly_sensitive.write` - `crm.objects.custom.highly_sensitive.read` - `crm.objects.custom.highly_sensitive.write` - `tickets.highly_sensitive` (Both read and write access) ## Turn on Sensitive Data access for your app The process for gaining access to Sensitive Data scopes depends on whether you have a private or public app. ### Private apps For private apps, you can add the [scopes](#sensitive-data-scopes) in your private app settings. - In your HubSpot account, click the **settings icon** in the top navigation bar. - In the left sidebar menu, navigate to **Integrations** > **Private Apps**. - On the private app, click **Edit**. - Click the **Scopes** tab. - Select the **checkboxes** of the [sensitive scopes](#sensitive-data-scopes) you want to add. - In the top right, click **Commit changes**. Your private app's access token will immediately have access to read and/or write Sensitive Data properties on the selected objects. ### Public apps For public apps, you’ll need to request access to specific sensitive scopes from HubSpot’s Ecosystem Quality team. If approved, the team will allowlist the sensitive scopes to test your app and will help you publish your app with the required scopes following a period of testing and compliance checks. You’ll then need to notify app users of the change and [request re-authorization of your app](#request-authorization-from-app-users). Navigate to [this page](https://developers.hubspot.com/sensitive-data#getStarted) to view the full process and fill out the form to request Sensitive Data access for public apps. ### Request authorization from app users Once the sensitive [scopes](#sensitive-data-scopes) have been added to your app, you’ll need to send [authorization URLs](/guides/apps/authentication/working-with-oauth) to app users so they can authorize and install the app, granting access to Sensitive Data scopes. If they’re existing users of the app, they’ll need to reauthorize with the updated scopes for Sensitive Data access to apply. Once you’ve notified your app users, a Super admin in their account will need to: - Click the **authorization URL** with Sensitive Data scopes. - Select the **account** into which to install the app. The HubSpot account must have an _Enterprise_ subscription to access Sensitive Data functionality. - Review the updated list of scopes, then click **Connect App** to install. Once authorized and installed, the app can access Sensitive Data via APIs for the account. ## Manage Sensitive Data Once your app has access to Sensitive Data, you can use the following APIs to manage Sensitive Data: - **Properties API**: create, edit, or archive Sensitive Data and Highly Sensitive Data properties, and retrieve property information. - **Object APIs** [(Contacts](/guides/api/crm/objects/contacts), [Companies](/guides/api/crm/objects/companies)**,** [Deals](/guides/api/crm/objects/deals), [Tickets](/guides/api/crm/objects/tickets)**,** [Custom objects](/guides/api/crm/objects/custom-objects)): retrieve or update Sensitive Data and Highly Sensitive Data property values. - **CRM Search V3 API**: search for records with values for Sensitive Data properties. This API is not available for use with Highly Sensitive Data. - **Forms API**: send form submissions containing Sensitive Data or Highly Sensitive Data. - **Webhooks API**: create `propertyChange` event type webhook subscriptions for contact, company, deal, and ticket Sensitive Data properties. This API is not available for use with Highly Sensitive Data. ### Retrieve Sensitive Data properties You can use the [properties API](/guides/api/crm/properties) to view Sensitive Data property definitions and schema information. To retrieve all Sensitive Data properties for an object, include the `dataSensitivity` query parameter with the value `sensitive`. To retrieve all Highly Sensitive Data properties, use the value `highly_sensitive`. If you don't include this parameter when retrieving properties, only non-sensitive properties will be returned. For example, to retrieve all contact properties that store Sensitive Data, make a `GET` request to `/crm/v3/properties/contacts?dataSensitivity=sensitive`. - To retrieve an individual property, make a `GET` request to `/crm/v3/properties/{objectType}/{propertyName}`. - If a property contains Sensitive Data, in the response, the `dataSensitivity` property will have the value `sensitive`. - If a property contains Highly Sensitive data, the `dataSensitivity` property will have the value `highly_sensitive`. - If a returned Sensitive Data property stores protected health information, the returned `sensitiveDataCategories` field will have a value of `["HIPAA"]`. - If it’s not a Sensitive Data property, the `dataSensitivity` property value will be `non_sensitive`. For example, if you you have a _Passport Number_ Sensitive Data property, when you retrieve it, your response would look like: ```json // Example response for GET /crm/v3/properties/contacts/passport_number { "updatedAt": "2021-11-03T15:24:08.528Z", "createdAt": "2021-04-16T18:17:14.911Z", "name": "passport_number", "label": "Passport Number", "type": "string", "fieldType": "text", "description": "", "groupName": "contactinformation", "options": [], "createdUserId": "9586504", "updatedUserId": "9586504", "displayOrder": -1, "dataSensitivity": "sensitive", "calculated": false, "externalOptions": false, "archived": false, "hasUniqueValue": false, "hidden": false, "showCurrencySymbol": false, "modificationMetadata": { "archivable": true, "readOnlyDefinition": false, "readOnlyValue": false }, "formField": true } ``` ### Create or update Sensitive Data properties You can use the [properties API](/guides/api/crm/properties) to create or update Sensitive Data properties. You must have the `sensitive.write` scope for the given object to create or edit its properties. - To create a property and mark it as sensitive, make a `POST` request to `/crm/v3/properties/{object}` and include the `dataSensitivity` field with the value `sensitive`. - To create a property and mark it as highly sensitive, make a `POST` request to `/crm/v3/properties/{object}` and include the `dataSensitivity` field with the value `highly_sensitive`. Once you've created a property as sensitive, you cannot change the sensitivity setting. For example, to create a Sensitive Data property for contacts, your request would look like: ```json // Example response for POST /crm/v3/properties/contacts { "groupName": "contactinformation", "name": "sensitive_property", "label": "Sensitive Property", "type": "string", "fieldType": "text", "dataSensitivity": "sensitive" } ``` - To edit a Sensitive Data property, make a `PATCH` request to `/crm/v3/properties/{objectType}/{propertyName}`. You cannot edit the `dataSensitivity` field. - To archive a Sensitive Data property, make a `DELETE` request to `crm/v3/properties/{objectType}/{propertyName}`. Once archived, properties will be permanently deleted after 90 days. Learn more about using the [properties API](/guides/api/crm/properties). ### Retrieve Sensitive Data property values You can retrieve a record’s value for a Sensitive Data property. To do this, make a `GET` request to `/crm/v3/objects/{object}/{recordId}` and include the Sensitive Data property in your query parameters. For example, to retrieve a contact’s _Passport Number_ value, your request URL would look like: `https://api.hubspot.com/crm/v3/objects/contacts/1234567?properties=passport_number`. ### Set or update Sensitive Data property values You can use the [object APIs](/guides/api/crm/understanding-the-crm) to create or edit records to set values for Sensitive Data properties. - To create a record and set a value for a Sensitive Data property, make a `POST` request to `/crm/v3/objects/{object}`. In your request body, include the required properties and the Sensitive Data property. - To update a record’s value for a Sensitive Data property, make a `PATCH` request to `/crm/v3/objects/{object}/{objectId}`. In your request body, include the Sensitive Data property with the new value. To clear the value, set the value to an empty string in your request body. For example, if you want to create a contact and set a value for the _Passport Number_ property, your request would look like: ```json // Example request body for POST /crm/v3/objects/contacts { "properties": { "email": "example@hubspot.com", "firstname": "Jane", "lastname": "Doe", "phone": "(555) 555-5555", "company": "HubSpot", "website": "hubspot.com", "lifecyclestage": "marketingqualifiedlead", "passport_number": "1234567" } } ``` If you wanted to update the property’s value later on, your request would look like: ```json // Example request body for PATCH crm/v3/objects/contacts/{contactID} { "properties": { "passport_number": "89101112" } } ``` ### CRM Search API If your app has the Sensitive Data scopes, you can use the [V3 CRM search API](/guides/api/crm/search) to search for records with values for Sensitive Data properties. To do so, make a `POST` request to `/crm/v3/objects/{object}/search` and include the Sensitive Data properties you want to search for. For example, the following search would return contacts with their first name, last name, and passport number values: ```json // Example POST request to /crm/v3/objects/contacts/search { "properties": ["firstname", "lastname", "passport_number"] } ``` ### Forms API To send form submissions containing Sensitive Data to HubSpot, make a `POST` request to `https://api.hsforms.com/submissions/v3/integration/secure/submit/{HubID}/:formGuid`. Learn more about using this [endpoint.](/reference/api/marketing/forms/v3-legacy#submit-data-to-a-form-supporting-authentication). This is currently the only forms submission API endpoint that can be used with Sensitive Data because it supports authentication. Any other requests to the forms API will result in an error or hide Sensitive Data values. ### Webhooks API You can create `propertyChange` [event type webhook subscriptions](/guides/api/app-management/webhooks#webhook-settings) for Sensitive Data properties on contacts, companies, deals, and tickets. For non-sensitive property `propertyChange` events, the webhook payload has the new value in the `propertyValue` field. For Sensitive Data property `propertyChange` events, the webhook payload will have the `propertyValue` set as `"REDACTED"`. To view the value, you'll need to [retrieve the record's value](#retrieve-sensitive-data-property-values) for the Sensitive Data property. For example, a webhook payload for a Sensitive Data property `propertyChange` event contact subscription would look like the following: ```json // Example webhook payload [ { "eventId": 3029365631, "subscriptionId": 686627, "portalId": 891472211, "appId": 7906213, "occurredAt": 1715203101896, "subscriptionType": "contact.propertyChange", "attemptNumber": 0, "objectId": 847297356, "propertyName": "passport_number", "propertyValue": "REDACTED", "changeSource": "CRM_UI", "sourceId": "userId:882761034" } ] ``` ## Errors The following errors and redactions are expected: - If you don’t have the required [scope](#sensitive-data-scopes) to read or update Sensitive Data for a given object, you’ll receive a 403 Forbidden error when you make a request containing a Sensitive Data property to that object's API. - Sensitive Data access is only supported for the V3 properties, object, CRM search, and forms APIs. - If you use legacy versions of the properties and object APIs, your request will appear successful, but Sensitive Data values won't be updated and will be hidden in the response body. - If you use legacy versions of the CRM search API, you'll receive a 403 Forbidden error citing support in version 3 or later. - If you use the unauthorized form submission API, you'll receive a 403 Forbidden error. If you try to retrieve form submissions containing Sensitive Data, the submissions will be returned but the Sensitive Data properties and their values will be hidden. # HubSpot APIs | Deprecated APIs @ A list of deprecated HubSpot APIs $ reference & api --- # Sunsetted and deprecated APIs The APIs listed in the sections below will not receive future updates to functionality, and in some cases, will be fully sunsetted and unavailable for use in your integrations. It's highly recommended that you subscribe to the [HubSpot Developer Changelog](https://developers.hubspot.com/changelog) to follow along with the latest updates, breaking changes, and other significant changes to functionality. ## Sunsetted APIs The following APIs have been fully removed, and will return an error when making a call to their associated endpoints. | API | Sunset date | Notes | | --- | --- | --- | | HubDB v2 | December 16, 2024 | Please use [HubDB v3 API](/reference/api/cms/hubdb) instead. It includes many performance, usability, and API consistency improvements over v2. | | CMS performance | April 9, 2024 | For website analytics data, use the [Analytics API](/guides/api/analytics-and-events/reporting). | | Ecommerce bridge | February 28, 2023 | If you previously built an integration using this API, you can follow [the migration guide](/reference/api/deprecated/migrations/migrate-an-existing-ecommerce-bridge-api-integration) to switch over your integration to use private apps. | | Accounting extension | February 28, 2023 | If you previously built an integration using this API, you can follow [this migration guide](/reference/api/deprecated/migrations/migrate-an-existing-account-extension-integration) to switch over your integration to use private apps. | | Marketing calendar | August 31, 2023 | You can continue to [use the marketing calendar in HubSpot](https://knowledge.hubspot.com/campaigns/use-your-marketing-calendar). | ## Deprecated APIs The legacy endpoints listed below will not be getting a version update. These endpoints are functional and stable, but won’t be updated beyond their current version. HubSpot will continue to support them for the foreseeable future and will announce any future changes with ample notice on HubSpot's [Developer Changelog](https://developers.hubspot.com/changelog).[](/reference/api/deprecated/overview) | API | Notes | | --- | --- | | Social media | Includes the following endpoints: | # Calls to Action widget SDK @ You can use the Calls-to-actions JavaScript API to programmatically control HubSpot CTAs on your website. $ reference & api/marketing --- # Calls-to-action JavaScript API The Calls-to-action JavaScript API allows you to render HubSpot [Calls-To-Actions](https://knowledge.hubspot.com/ctas/create-calls-to-action) on your website. You can use this API to programmatically control when a CTA appears or listen to events triggered when a user interacts with one of your CTAs. ## Initializing The API is housed in the `window.HubSpotCallsToActions` object, which provides access to all available methods. The object is created by the [HubSpot tracking code](https://knowledge.hubspot.com/reports/install-the-hubspot-tracking-code), but may not be available immediately on page load. To defer accessing the API until it's initialized, you can use the `window.hsCallsToActionReady` helper. `window.hsCallsToActionReady` is an optional field you can define on the `window` object which enables you to specify code to be executed as soon as the API becomes available. If used, this field should be set to an array of functions. Once the API has been initialized, this event handler will check for the existence of this array and execute the functions in order. ```js if (window.HubSpotCallsToActions) { console.log('The api is already initialized'); } else { window.hsCallsToActionsReady = [ () => { console.log('Now the api is ready'); }, ]; } ``` ## Refresh CTAs `HubSpotCallsToActions.refresh` Refresh and re-render CTAs, given the current page URL. If you're using CTAs in a single-page application, this function can be useful to refresh the CTAs shown based on a route change (i.e., dynamically show a CTA based on a specific route). If HubSpotCallsToActions.refresh is called on a route where there are no CTAs present, any visible CTAs will be removed. ```js HubSpotCallsToActions.refresh(); ``` ## Close a single CTA `HubSpotCallsToActions.close` Closes a specific CTA based on its ID. Display frequency rules will be applied to a CTA once it's closed. Calling this function will have no effect on embedded CTAs. ```js // Closes a CTA with an ID of 5000 HubSpotCallsToActions.close(5000); ``` ## Close all CTAs on a page `HubSpotCallsToActions.closeAll` Close all CTAs on a page. Calling this function will have no effect on embedded CTAs. ```js HubSpotCallsToActions.closeAll(); ``` ## Register an event listener `HubSpotCallsToActions.on` Register an event listener that will fire based on two arguments: - `event`: the name of the event you want to define the handler for, provided as a string. - `handler`: a function that defines the behavior you want to occur when the `event` fires. See the example snippet below for an example of defining a handler for the onCallToActionReady event. You can also review a full list of events you can define handlers in the table below. ```js HubSpotCallsToActions.on('onCallToActionReady', ({ id }) => { console.log('Call To Action rendered with id', id); }); ``` ## Unregister an event listener `HubSpotCallsToActions.off` Unregister a previously registered event listener - `event`: the name of the event you want to unregister your previously defined handler for. - `handler`: a function that will fire when successfully unregistered. ## Events CTAs will emit various events throughout its lifecycle. These events are defined in the table below. You can define listeners for each of these events using the `HubSpotCallsToAction.on` function, as described in the section above. | Event name | Description | | --- | --- | | `onCallToActionReady` | The CTA has finished loading, and has been fully rendered. | | `onCallToActionViewed` | The CTA has entered the user's viewport. This event will only fire once. | | `onCallToActionNavigated` | A link was clicked within the CTA. | | `onCallToActionFormSubmitted` | A form within the CTA was submitted. When triggered, the event payload will include the `formId`. | Events will also be posted to the parent window, which you can define listeners for using window.addEventListener. For example, the snippet below defines an event listener that checks for the `onCallToActionReady` event being emitted. ```js window.addEventListener('message', function handler({ data }) { if ( data.type === 'hsCallsToActionCallback' && data.eventName === 'onCallToActionReady' ) { console.log('Call To Action rendered'); } }); ``` # Error handling @ Learn how to handle common API errors when developing with HubSpot's APIs. $ reference & api/other-resources --- # Handling errors Unless specified otherwise, most HubSpot endpoints will return a `200` OK response upon success. Any endpoints returning a different status code will specify the returned response in its documentation. In addition, HubSpot has several error responses that are common to multiple APIs: - `207 Multi-Status`: returned when there are different statuses (e.g., errors and successes), which occurs when you've enabled [multi-status error handling](#multi-status-errors) for the object API batch create endpoints. - `401 Unauthorized`: returned when the authentication provided is invalid. See our [Authentication Overview](/guides/api/app-management/oauth-tokens) for details on authenticating API requests. - `403 Forbidden`: returned when the authentication provided does not have the proper permissions to access the specific URL. As an example, an OAuth token that only has content access would get a `403` when accessing the Deals API (which requires contacts access). If you've confirmed that your API key or private app has the necessary permissions, please reach out to [HubSpot support](https://knowledge.hubspot.com/help-and-resources/get-help-with-hubspot) for assistance. - `423 Locked`: returned when attempting to sync a large volume of data (e.g., upserting thousands of company records in a very short period of time). Locks will last for 2 seconds, so if you receive a `423` error, you should include a delay of at least 2 seconds between your API requests. - `429 Too many requests`: returned when your account or app is over its API [rate limits](/guides/apps/api-usage/usage-details). Find suggestions on working within those limits [here](https://developers.hubspot.com/docs). - `477 Migration in Progress`: returned when a HubSpot account is currently being [migrated between data hosting locations](https://www.hubspot.com/data-centers). HubSpot will return a [Retry-After](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) response header indicating how many seconds to wait before retrying the request (typically up to 24 hours). - `502/504 timeouts`: returned when HubSpot's processing limits have been met. These limits are in place to prevent a single client from causing degraded performance. These timeout responses occur when making a large number of requests over a sustained period. If you get one of these responses, you should pause your requests for a few seconds, then retry. - `503 service temporarily unavailable`: returned when HubSpot is temporarily unavailable. If you receive this response, you should pause your requests for a few seconds, then retry. - `521 web server is down`: returned when HubSpot's server is down, this should be a temporary issue. If you receive this response, you should pause your requests for a few seconds, then retry. - `522 connnection timed out`: returned when the connection between HubSpot and your application has timed out. If you've received this response, please reach out to [HubSpot support](https://knowledge.hubspot.com/help-and-resources/get-help-with-hubspot) for assistance. - `523 origin is unreachable`: returned when HubSpot is unable to contact your application. If you receive this response, you should pause your requests for a few seconds, then retry. - `524 timeout`: returned when a response is not received within 100 seconds. This can occur when the HubSpot's server is overloaded, such as with a large data query. If you receive this response, you should pause your requests for a few seconds, then retry. - `525/526 SSL issues`: returned when the SSL certificate is invalid or the SSL handshake fails. If you've received this response, please reach out to [HubSpot support](https://knowledge.hubspot.com/help-and-resources/get-help-with-hubspot) for assistance. Aside from these general errors, HubSpot error responses are intended to be human-readable. Most endpoints don't return error codes, but return a JSON formatted response with details about the error. - For [CRM object APIs](https://developers.hubspot.com/docs/guides/api/crm/understanding-the-crm), error responses will include detailed `message`, `code`, and `context` fields, which will provide additional information on required properties that were not included in your request, as well as any issues with malformed properties in your request. - More details for endpoint-specific errors can be found on the documentation pages for the endpoint. The fields in the example response below should all be treated as optional in any error parsing. The specific fields included can vary between different APIs, so any error parsing should allow for specific fields to be missing from the response. ```json // Structure of an example error from HubSpot { "status": "error", "message": "This will be a human readable message with details about the error.", "errors": [ { "message": "discount was not a valid number", "code": "INVALID_INTEGER", "context": { "propertyName": ["discount"] } } ], "category": "VALIDATION_ERROR", "correlationId": "a43683b0-5717-4ceb-80b4-104d02915d8c" } ``` ## Multi-status errors For the [object APIs'](/guides/api/crm/understanding-the-crm) batch create endpoints, you can enable multi status responses that include partial failures. This means the response will show which records were created and which were not. To do so, include a unique `objectWriteTraceId` value for each input in your request. The `objectWriteTraceId` can be any unique string. For example, a request to create tickets could look like: ```json ///Example request to POST crm/v3/objects/tickets/batch/create { "inputs": [ { "objectWriteTraceId": "549b1c2a9350", "properties": { "hs_pipeline_stage": "1" }, "objectWriteTraceId": "549b1c2a9351", "properties": { "missing": "1" } } ] } ``` In the response, statuses are grouped so you can see which creates were successful and which failed. For the above request, your response would look like: ```json ///Example response { "status": "COMPLETE", "results": [ { "id": "1145814089", "properties": { "createdate": "2024-08-15T17:09:13.648Z", "hs_helpdesk_sort_timestamp": "2024-08-15T17:09:13.648Z", "hs_last_message_from_visitor": "false", "hs_lastmodifieddate": "2024-08-15T17:09:13.648Z", "hs_object_id": "1145814089", "hs_object_source": "API", "hs_object_source_label": "INTERNAL_PROCESSING", "hs_pipeline": "0", "hs_pipeline_stage": "1", "hs_ticket_id": "1145814089" }, "createdAt": "2024-08-15T17:09:13.648Z", "updatedAt": "2024-08-15T17:09:13.648Z", "archived": false } ], "numErrors": 1, "errors": [ { "status": "error", "category": "VALIDATION_ERROR", "message": "Property values were not valid: [{\"isValid\":false,\"message\":\"Property \\\"missing\\\" does not exist\",\"error\":\"PROPERTY_DOESNT_EXIST\",\"name\":\"missing\",\"localizedErrorMessage\":\"Property \\\"missing\\\" does not exist\",\"portalId\":891936587}]", "context": { "objectWriteTraceId": ["549b1c2a9351"] } } ], "startedAt": "2024-08-15T17:09:13.610Z", "completedAt": "2024-08-15T17:09:13.910Z" } ``` ## Retries If your app or integration provides an endpoint that HubSpot will call, such as webhook subscriptions, any errors that your endpoint throws will cause HubSpot to retry the request. ### Webhooks If your service has problems handling notifications at any time, HubSpot will attempt to resend failed notifications up to 10 times. HubSpot will retry in the following cases: - **Connection failed:** HubSpot cannot open an HTTP connection to the provided webhook URL. - **Timeout:** Your service takes longer than 5 seconds to send back a response to a batch of notifications - **Error codes:** Your service responds with any HTTP status code (`4xx` or `5xx`) Workflows **will not** retry after receiving 4XX series response status codes. One exception to this rule is 429 rate limit errors; workflows will automatically retry after receiving a 429 response, and will respect the `Retry-After` header if present. Note that the `Retry-After` value is in miliseconds. Notifications will be retried up to 10 times. These retries will be spread out over the next 24 hours, with varying delays between requests. Individual notifications will have some randomization applied, to prevent a large number of concurrent failures from being retried at the exact same time. ### Custom code workflow actions If you're creating a [custom code action](/reference/api/automation/custom-code-actions) in a workflow, and an API call in your action fails due to a rate limiting error, or a `429` or `5XX` error from `axios` or `@hubspot/api-client`, HubSpot will reattempt to execute your action for up to three days, starting one minute after failure. Subsequent failures will be retried at increasing intervals, with a maximum gap of eight hours between tries. # HubSpot APIs | Getting started @ The HubSpot API allows developers to build custom apps and integrations. Get started by reading our API overview and creating a developer account. $ reference & api --- # HubSpot API reference documentation HubSpot’s developer platform is a core part of our mission to empower organizations to [grow better](https://www.hubspot.com/grow-better). Our APIs are designed to enable teams of any shape or size to build robust integrations that help them customize and get the most value out of HubSpot. All HubSpot APIs are [built using REST conventions](https://en.wikipedia.org/wiki/Representational_state_transfer) and designed to have a predictable URL structure. They use many standard HTTP features, including methods (`POST`, `GET`, `PUT`, `DELETE`) and error response codes. All HubSpot API calls are made under `https://api.hubapi.com` and all responses return standard JSON. ## Setting up There are several ways to build integrations with HubSpot: - To build an internal integration for an individual HubSpot account (e.g., you want to build an app that can access and edit only authorized parts of your account to share or integrate with other parts of your organization), create a [private app](/guides/apps/private-apps/overview). - If you're looking to create a public app that can be installed across multiple HubSpot accounts, you should [create a developer account](https://app.hubspot.com/signup-hubspot/developers). A developer account is where you create HubSpot apps, each authenticated with OAuth and provided with a configurable set of features and permissions. You can also use your developer account to [create test accounts](/getting-started/account-types), monitor app status and performance, or publish apps to the HubSpot App Marketplace. Learn more about the [different types of apps and how to build them](/guides/apps/overview). ## Client libraries Client libraries are designed to help you interact with the HubSpot APIs with less friction. They are written in several different languages and help bridge the gap between your application and HubSpot’s APIs. They take away the need to know the exact URL and HTTP method to use for each API call among other things leaving you more time to focus on making your application. Learn more about our client libraries [here](/getting-started/overview) Language Package Link [Source Code](https://github.com/HubSpot/hubspot-api-nodejs) Node.JsNode.Js npm install @hubspot/api-client [hubspot-api-nodejs](https://github.com/HubSpot/hubspot-api-nodejs) PHPPHP composer require hubspot/api-client [hubspot-api-php](https://github.com/HubSpot/hubspot-api-php) RubyRuby gem install hubspot-api-client [hubspot-api-ruby]([hubspot-api-nodejs](https://github.com/HubSpot/hubspot-api-nodejs)) PythonPython pip install hubspot-api-client [hubspot-api-python]([hubspot-api-nodejs](https://github.com/HubSpot/hubspot-api-nodejs)) ## Using the documentation Each set of API endpoints includes reference documentation that details the available parameters and provides example requests in multiple languages, and a guide that walks you through the functionality of each endpoint. A link to the corresponding guide for an API will be included at the top of each page for easy navigation. In the reference documentation for a given API, several options are available to help you test out a given endpoint: - Above the _Request_ code block for each endpoint, you can click **Test call** to make test calls directly from the page. - You can paste in an OAuth access token if you're building a [public app](/guides/apps/public-apps/overview), or a private app access token if you're building a [private app](/guides/apps/private-apps/overview). - Click **Send request** to make a call to your app with any test parameters that you populate on the left side of the page. - At the top of API reference pages, a **Run in Postman** button is displayed when the API is available in [HubSpot's Postman collection](https://www.postman.com/hubspot/hubspot-public-api-workspace/overview). Learn more about [using Postman](https://developers.hubspot.com/postman) to make API requests to HubSpot. When you're ready to implement an API in your integration, you can consult the documentation about general app development, including [authentication](/guides/apps/authentication/intro-to-auth) and [listing apps on the App Marketplace](/guides/apps/marketplace/listing-your-app). You'll also find guides for [building UI extensions (BETA)](/guides/crm/ui-extensions/overview) and working with the [developer projects framework (BETA)](/guides/crm/setup). ## Building on HubSpot's CMS While the API reference documentation includes information about using HubSpot's CMS APIs, such as the [pages API](/guides/api/cms/pages), you'll find the full set of CMS developer documentation at [https://developers.hubspot.com/docs/cms](/guides/cms/overview). This includes guides that walk through building various CMS assets as well as reference documentation for HubL. ## Support and community resources Get your questions answered, make connections, and share your insights by joining HubSpot’s growing [developer community forums](https://community.hubspot.com/t5/HubSpot-Developers/ct-p/developers) and [Slack community](/getting-started/slack/developer-slack). These forums are a great place to make your voice heard — community feedback is incredibly important to us and our ongoing efforts to improve HubSpot’s developer experience. You can also stay up to date on new features, announcements, and important changes by subscribing to the [Changelog](https://developers.hubspot.com/changelog) blog.[](/guides/api/overview) # CMS APIs @ The HubSpot CMS is powerful and can be extended through APIs. This page is an aggregation of our APIs most relevant to developers building with the CMS. $ reference & --- # CMS APIs The HubSpot CMS is powerful and can be extended through APIs. This page is an aggregation of our APIs that are most relevant to developers building with the CMS. **This is not our** [**full list of APIs**](/reference/api/overview)**.** Our APIs are subject to [rate limits](/guides/apps/api-usage/usage-details#rate-limits) and support [OAuth](/guides/apps/authentication/working-with-oauth). We encourage reviewing our [API Usage Guidelines](/guides/apps/api-usage/usage-details) for best practices. To test your integrations you will need to [create a developer account](https://app.hubspot.com/signup/developers), which will enable you to [create test accounts](/getting-started/account-types) or an [application](/guides/apps/public-apps/overview) to get started with OAuth. This is different from your CMS developer sandbox portal. ## Analytics API The [Analytics API](/reference/api/analytics-and-events/reporting) allows you to export analytics and reporting data from HubSpot. It’s primarily used to connect metrics tracked in HubSpot to those stored in other business intelligence tools. You could use this API to display traffic and leads tracked in another CRM or analytics tool. ## Conversations API The Conversations Live Chat widget allows you to directly engage in conversations on your website. The [Conversations API](/reference/api/conversations/chat-widget-sdk) helps you provide a more tailored visitor experience by giving you more control over this widget. You can use this to customize how and when the conversations widget appears on your website. ## CMS Blog API The [CMS Blog APIs](/reference/api/cms/blogs/blog-details#list-blogs) enable you to sync your blog data with external applications and tools as well as modify blog content. ### CMS Blog Authors API The [CMS Blog Authors API](/guides/api/cms/blogs/blog-authors) enables you to list, search, create, delete, and get information for blog authors. ### CMS Blog Comments API The [CMS Blog Comments API](/reference/api/cms/blogs/blog-comments#list-comments) can be used to list, get, create, and delete comments as well as restore deleted comments. ### CMS Blog Post API The [CMS Blog Post API](/guides/api/cms/blogs/blog-posts) can be used to list, get, create, update, delete, clone, and control publishing of blog posts. The API can also be used to manage revisions of a post. ### CMS Blog Topics API The [CMS Blog Topics API](/guides/api/cms/blogs/blog-tags) can be used to list, search, get, create, update, delete and merge blog topics. ## CMS Domains API The [CMS Domains API](/reference/api/cms/domains/v1#list-domains) can be used to get a list of all the domains and their associated properties for a HubSpot account. ## CMS Files API You can use the [CMS Files API](/reference/api/library/files/v3-legacy#upload-a-new-file) to upload, delete, organize and manage files in the [File Manager](/guides/cms/storage/file-manager). ## Forms API You can use the [Forms API](/guides/api/marketing/forms) to deliver heavily customized forms to users by directly submitting form data to our forms API. This API can also be used to get form information, create, update and delete forms, and their associated fields. Note: when using the Forms API to submit forms data, the [accessibility](/guides/cms/content/accessibility) and validation of your forms is your responsibility. ## CMS HubDB API You can use the [HubDB API](/guides/api/cms/hubdb) to create, modify and delete HubDB tables, and their rows of data. The HubDB API also supports [importing a CSV file](/reference/api/cms/hubdb/v2#import-a-csv-to-the-draft-version-of-a-table) into an existing HubDB table. This API is useful for those storing and displaying complex information. ## CMS Page Publishing API You can use the [CMS Page Publishing API](/reference/api/cms/pages/v2#list-pages) to create, clone, list, update, and delete pages on the CMS. This API can also be used to manage the publishing state and view and revert to revisions of a page. ## CMS Site Search API You can use the [Site Search API](/reference/api/cms/site-search/v2#search-your-site) to search the content (site pages, blog posts, landing pages, blog listing pages, and knowledge articles) of a HubSpot hosted site. This can be used to build a custom [site search](/guides/cms/content/content-search). You can also use the API to access all of the indexed data for a document. ## CMS Templates API You can use the [CMS Templates API](/reference/api/cms/templates#list-templates) to list, create, update, delete, and publish coded HTML+HubL templates. This API can also be used to get and restore previous revisions of a template as well as restore deleted templates. ## CMS URL Mappings API You can use the [CMS URL Mappings API](/guides/api/cms/url-redirects) to list, create, update, delete, and get URL mappings from a HubSpot account. URL Mappings are used for redirects and proxy pages. ## CMS Content Audit API The [CMS Content Audit API](/guides/api/cms/content-audit) allows you to filter and sort on content object changes by type, time period, or HubSpot user ID, so you’re never left wondering what happened. _This API is currently in public beta._ ## Get Privacy Consent Status The [GDPR privacy consent banner](https://knowledge.hubspot.com/reports/customize-your-cookie-tracking-settings-and-privacy-policy-alert) which can control whether different analytics scripts are run. To control script loading you'll need to [get the consent status](/reference/api/analytics-and-events/cookie-banner/cookie-banner-api). # Module and theme fields @ HubSpot has many different field types, you can use to add flexibility and customization to your themes and modules. $ reference & cms/fields --- # Module and theme fields Add fields to modules and themes to allow content creators to control various aspects of a page within the page editor. Below, learn about all of the fields available for modules and themes, along with their available properties. For more information about implementing module and theme fields, including field groups and repeating fields, view the [module and theme fields overview](/guides/cms/content/fields/overview). ## Properties used by all fields All fields share a set of common properties. These are general fields, such as the field's name or the help text that displays for content creators using the field in the module or theme. ```json { "name" : "is_teaser_img", "label" : "Enable Teaser Image", "required" : false, "locked" : false, "type" : "boolean", "inline_help_text" : "Shows Teaser image when toggled on", "help_text" : "Teaser images are used to help provide visual context to the post.", "default" : false "alias_mapping": { "property_aliases_paths": { "is_teaser_img": ["old_boolean_field_name"] } } } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `name` | String | The field's name, which you'll reference when incorporating the field and its values in the module or theme. Cannot contain spaces or special characters. | `richtext_field, date_field, etc.` | | `label` | String | The text the content creator sees describing the field. May contain spaces. | `Rich text field, Date field, etc.` | | `required` | Boolean | Sets whether the field can be left blank in the editor. If `true`, content cannot be published without a value in the field. | `false` | | `locked` | Boolean | Sets whether the field is editable in the content editor. If `true`, the field will not appear in the content editor. | `false` | | `type` | String | The type of field. Field types are unique per field and can be found within the documentation for each field below. | | | `inline_help_text` | String | Text that displays inline below field's label (limit 400 characters). Best used for information required to use the field.You can include the following HTML tags (other tags will be ignored on render):`a`, `b`, `br`, `em`, `i`, `p`, `small`, `strong`, `span`. | | | `help_text` | String | Text that displays in the editor within a tooltip on hover to assist the content creator (limit 300 characters). Best used for information that is supplementary but not required to use the field.You can include the following HTML tags (other tags will be ignored on render):`a`, `b`, `br`, `em`, `i`, `p`, `small`, `strong`, `span`. | | | `id` | String | The field's unique ID, which is set by HubSpot. When building locally you do not need to specify this ID. | | | `visibility` | Array | Sets the field's display conditions. For example, you can set a field to only display when another checkbox field has been selected. Learn more about [visibility](/guides/cms/content/fields/overview#field-visibility). | | | `display_width` | String | By default, fields are full-width in the editor. When two consecutive fields in the `fields.json` file are set to `half_width`, they will instead appear next to each other in the editor. | | | `alias_mapping` | String | An alias for the field, which maps existing field values to a new location without breaking existing content. This can be helpful when needing to update a field that's currently being used in live content, such as moving a field from the _Content_ tab to the _Styles_ tab. Learn more about [alias mapping](/guides/cms/content/fields/alias-mapping). | `False` | ## Alignment ```json { "name": "img_position", "label": "Position Image", "help_text": "Position the image within it's container.", "required": false, "type": "alignment", "default": { "horizontal_align": "CENTER", "vertical_align": "TOP" } } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | Object | Object containing `horizontal_align` and `vertical_align`. | | | `alignment_direction` | String | Determines if only horizontal, only vertical, or both alignment controls should be shown. Can be: | `BOTH` | ## Background image ```json { "name": "bg_image", "label": "Background image", "required": false, "type": "backgroundimage", "default": { "src": "https://example.com/img.png", "background_position": "MIDDLE_CENTER", "background_size": "cover" } } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | Object | Object containing the image src, background position and background size. | `null` | ## Blog ```json { "name": "blog", "label": "Blog", "required": false, "locked": false, "type": "blog", "default": 1234567890 } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | "default" / blog id | Specifies which blog is selected by default. This parameter accepts arguments of either 'default' or a blog ID (available in the URL of the Blog dashboard). | `null` | ## Boolean ```json { "name": "is_teaser_img", "label": "Enable Teaser Image", "required": false, "locked": false, "type": "boolean", "display": "checkbox", "inline_help_text": "Shows Teaser image when toggled on", "help_text": "Teaser images are used to help provide visual context to the post.", "default": false } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | Boolean | Set's whether the default state of this field is `true` or `false`. | `false` | | `display` | String | Choose the visual display style for the field. Can appear as either a `toggle` or a `checkbox`. | `checkbox` | A toggle switch can make sense when the value of the field enables/disables other fields conditionally being shown. Another time a toggle may be useful is when the field represents a major design change for the module. Checkboxes make sense for smaller changes that may not have as drastic of an effect on the module's display, such as hiding or showing individual small elements. ## Border ```json { "id": "styles.border", "name": "border", "label": "border", "required": false, "locked": false, "allow_custom_border_sides": false, "type": "border", "default": { "top": { "width": { "value": 1, "units": "px" }, "opacity": 100, "style": "solid", "color": "#ffffff" }, "bottom": { "width": { "value": 1, "units": "px" }, "opacity": 100, "style": "solid", "color": "#ffffff" }, "left": null, "right": null } } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | Boolean | object with keys for border radius, top, bottom, left, and right sides. | `{}` | ## Choice ```json { "name": "img_position", "label": "Image Position", "required": false, "locked": false, "multiple": "true", "display": "select", "choices": [ ["img--left", "Image Left - Text Right"], ["img--right", "Text Left - Image Right"] ], "type": "choice", "default": "img--left" } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `choices` | Array | Array containing the selectable options, formatted as unique internal value followed by label. | `[ [ "value 1", "Label 1" ], [ "value 2", "Label 2" ] ]` | | `default` | Value | Sets the default selected value from the choice array. | | | `multiple` | Boolean | Optional field that enables multiple options to be selected when set to `true`.Set `display` to `checkbox` or `select` to configure whether the field displays as a list of checkboxes or a dropdown menu. | `false` | | `display` | String | Set the field's appearance using one of the following values: | `"select"` | | `reordering_enabled` | Boolean | When set to `true`, allows content creators to reorder the field's options in the editor. To enable this, `multiple` must also be set to `true`. | `false` | | `preset` | String | Configures the button preset to use when `display` is set to `buttons`. For each preset, you'll need to configure the `choices` labels to match a specific set of values. Learn more about these [preset options](#choice-button-presets) below. | | ### Choice button presets To configure a choice field to display buttons instead of a dropdown menu, checkboxes, or radio selects, you can use any of the presets below. Each preset allows for a specific set of option labels, which you'll need to include in the `choices` array. These labels cannot be customized. ```json { "name": "layout", "type": "choice", "label": "Layout", "required": false, "locked": false, "display": "buttons", "preset": "layout", "choices": [ ["cards_value", "cards"], ["tiles_value", "tiles"], ["minimal_value", "minimal"] ] } ``` | Preset | Choice labels | Example | | --- | --- | --- | | `case` | `none` \| `upper` \| `lower` | | | `expand_icon` | `caret` \| `plus` \| `chevron` | | | `icon_background_shape` | `none` \| `square` \| `rounded` \| `circle` | | | `icon_size` | `small` \| `medium` \| `large` | | | `layout` | `cards` \| `tiles` \| `minimal` | | | `social_icon_background_shape` | `none` \| `square` \| `rounded` \| `circle` | | | `social_icon_size` | `small` \| `medium` \| `large` | | ## Color ```json { "name": "bg_color", "label": "Background color", "required": false, "locked": false, "type": "color", "default": { "color": "#ff0000", "opacity": 100 }, "limited_options": ["#000000", "#ffffff"] } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | Object | Sets the default selected color and opacity. | `{ "color" : "#ffffff", "opacity" : 100 }` | | `show_opacity` | Boolean | Sets whether opacity input is shown. | `undefined` | | `limited_options` | Array | An array of strings that hides the default color grid and overrides the favorite colors list (the "Favorites" label becomes "Available colors"). Values in the array must be either a hex code or a `color` hex value at any inheritance path. | `undefined` | ## CTA ```json { "name": "cta", "label": "CTA", "required": false, "locked": false, "type": "cta", "default": null } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | String | The default selected CTA. Expects a CTA id which can be found in the URL when editing a CTA in the [CTA manager](https://app.hubspot.com/l/ctas). | `null` | ## CRM object ```json { "name": "crmobject_field", "label": "CRM object", "required": false, "locked": false, "object_type": "CONTACT", "properties_to_fetch": [], "type": "crmobject", "default": { "id": 1 } } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `object_type` | String | Type of CRM Object the user can pick from. [Supported CRM Object Types](/guides/cms/content/data-driven-content/crm-objects#supported-crm-object-types) | | | `properties_to_fetch` | Array | Array of property names associated with the object type in string form. Ex: `"date_of_birth"` is a property associated with a contact. Use this to limit the information available to the page to just what you need. | | | `default` | Object | Object with id of default selected object instance. Contact ID, Company ID etc | `null` | ## CRM object property ```json { "name": "crmobjectproperty_field", "label": "CRM object property", "required": true, "locked": false, "object_type": "contact", "type": "crmobjectproperty", "default": { "property": "field_of_study" } } ``` | Parameter | Type | Description | | --- | --- | --- | | `object_type` | String | Type of CRM Object the user can pick from. Learn more about [supported CRM object types](/guides/cms/content/data-driven-content/crm-objects#supported-crm-object-types). | | `default` | Object | Contains the default property to display. | In addition, you can use the following snippets to return other property details: - `{{ module.fieldname.property }}`: returns the property's internal name. - `{{ module.fieldname.property_definition.label }}`: returns the property's label. - `{{ module.fieldname.property_definition.type }}`: returns the property type (e.g., string). ## Date ```json { "name": "event_start_date", "label": "Event Date", "required": false, "locked": false, "type": "date", "default": 1577854800000 } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | Timestamp | The Unix Epoch timestamp for the date and time you want to default to. Leave this null to allow the date and time picker to start the content creator at the current date and time in the picker. | `null` | ## Date and time ```json { "name": "event_start", "label": "Event Start", "required": false, "locked": false, "type": "datetime", "default": 1577854800000 } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | Timestamp | The Unix Epoch timestamp for the date and time you want to default to. Leave this null to allow the date and time picker to start the content creator at the current date and time in the picker. | `null` | ## Email Address ```json { "name": "emails", "label": "Email address", "required": false, "locked": false, "type": "email", "default": null } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | Array | Array of email address strings `["bob@example.com", "dennis@example.com"]` | `null` | ## Embed ```json { "name": "embed_field", "label": "Embed", "required": false, "locked": false, "supported_source_types": ["oembed", "html"], "supported_oembed_types": ["photo", "video", "link", "rich"], "type": "embed", "default": { "source_type": "oembed" } } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `supported_source_types` | Array | Supported source types for either oEmbed URLs (`oembed`), HTML embed code (`html`), or Media Bridge (`media_bridge`). | `["oembed", "html"]` | | `supported_oembed_types` | Array | Supported oEmbed type including `"photo"`, `"video"`, `"link"`, and `"rich"`. Does not apply to the supported_source_types of html | `[ "photo", "video", "link", "rich" ]` | | `supported_media_bridge_providers` | Array | Array of provider IDs that determine which Media Bridge providers are available to select content from.Note: This param will also be populated when installing a [Media Bridge Provider Application](https://ecosystem.hubspot.com/marketplace/apps/apps-for-media). | | | `type` | String | This parameter is always set to `"embed"` | `"embed"` | | `default` | Dict | An array containing the `"source_type"` parameter. This parameter has one string based value from the options provided in the `"supported_source_types"` parameter. | `oembed` | ## File ```json { "name": "file_field", "label": "File", "required": false, "locked": false, "type": "file", "picker": "file", "default": null } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | String | File URL. | `null` | | `picker` | String | Acceptable values: "file", "document", "image".
The picker shows assets uploaded to either the file manager, or the document manager depending on this parameter. | `file` | ## Followup email ```json { "name": "followup_email", "label": "Followup email", "required": false, "locked": false, "type": "followupemail", "default": null } ``` | Parameter | Type | Description | Default | | --------- | ------ | ----------- | ------- | | `default` | String | Email id | `null` | ## Font - Font family is determined by the combination of the `font` and `font_set` properties. You must include both to load the font. When [inheriting fields](/guides/cms/content/fields/overview#inherited-fields), this means you need to inherit both values. - Hiding CSS-related sub fields with `visibility` will not prevent that CSS from being output in the styling returned in the field object. You'll still need to manually include the CSS in the `styles` object. ```json { "name": "font", "label": "Font", "required": false, "locked": false, "load_external_fonts": true, "type": "font", "default": { "size": 12, "font": "Merriweather", "font_set": "GOOGLE", "size_unit": "px", "color": "#000", "styles": {} }, "visibility": { "hidden_subfields": { "font": true, "size": true } } } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | Object | Font object with settings for size, sizing unit, color, and styles for bold, italic, and underline. | `{ "size" : 12, "size_unit" : "px", "color" : "#000", "styles" : { } }` | | `load_external_fonts` | Boolean | HubSpot automatically loads the selected web font to the page if the font is selected and referenced by HubL in a stylesheet or in a module. Set this to false, if you are already loading the font to the page, that way the font won't load twice. | `true` | | `visibility` | Object | Using the `hidden_subfields` nested object, you can set a boolean for which controls of the Font field to hide. Subfields include: `font`, `size`, `bold`, `italic`, `underline`, and `color`. | | | `variant` | String | If using a web font, the variant of the font you want to use. For example, to use the 700-weight version of a font, set this to `"700"`. To use the 400-weight italic version of a font, set this to `"400i"`. | | | `limited_options` | Array | Array of strings that overrides the list of all font options. Values in the array must be a hard-coded font name or a font `name` property at any inheritance path. For example `theme.typography.body_text.name`. | `undefined` | ## Form ```json { "id": "idNumber", "name": "form_field_name", "display_width": null, "label": "Form", "required": false, "locked": false, "type": "form", "disable_inline_form_editing": true, "required_property_types": ["TICKET"], "support_all_webinar_types": true, "default": { "response_type": "inline", "message": "Thanks for submitting the form." } } ``` | Parameter | Type | Description | | --- | --- | --- | | `default` | Object | An object containing the form submission response details. Includes the following parameters: | | `disable_inline_form_editing` | String | Set the `disable_inline_form_editing` property to `true` to hide all inline form editing controls in the form module. This includes the form fields, submit button text, data privacy and consent options, and CAPTCHA. | | `required_property_types` | Array | An array that specifies which forms can be selected based on the property types of the form fields. Values include: `"CONTACT"`, `"COMPANY"`, and `"TICKET"`. | | `support_all_webinar_types` | Boolean | When set to `true`, the form will show a more generic webinar selector that enables selecting Microsoft Teams webinars in addition to GoToWebinar. The form field's values will also change slightly to save `webinar_id` and `webinar_source` instead of `gotowebinar_webinar_key`. You'll need to pass these more generic property values back into the [Form tag](/reference/cms/hubl/tags/standard-tags#form) alongside `gotowebinar_webinar_key`. | ## Gradient ```json { "name": "bg_gradient", "label": "Background gradient", "help_text": "Sets a gradient behind the content", "required": false, "type": "gradient", "default": { "colors": [ { "color": { "r": 0, "g": 0, "b": 0, "a": 1 } }, { "color": { "r": 255, "g": 255, "b": 255, "a": 1 } } ], "side_or_corner": { "verticalSide": "BOTTOM", "horizontalSide": null } } } ``` | Parameter | Type | Description | | --- | --- | --- | | `default` | Object | Object containing directional settings for a gradient ("side_or_corner") and color stops for the gradient as an array of objects. | ## HubDB Row ```json { "name": "hubdbrow_field", "label": "HubDB row", "required": false, "locked": false, "table_name_or_id": "3096859", "columns_to_fetch": ["name", "price", "desc"], "display_columns": ["name", "price", "desc"], "display_format": "%0 - %1 :::: %2", "type": "hubdbrow", "default": { "id": 4450468943 } } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `table_name_or_id` | String | The name or ID of the HubDB table. This field is required. | | | `columns_to_fetch` | Array | An array of column names to fetch from the table. If left blank, will return all columns in the table. | `[]` | | `display_columns` | Array | An array of column names to use in choice label. If left blank, will return only the first column in the table. | `[]` | | `display_format` | String | The format you would like the column data to display in the HubDB row selector using the percent symbol and number to designate a column.
**Ex:** _%0 (%1) would appear as Column0Value (Column1Value)_ | `""` | | `default` | Object | Object containing “id” for setting the default hubdb row. | `{ “id” : null }` | ## HubDB Table ```json { "name": "recipe_table", "label": "Recipe Table", "required": false, "locked": false, "type": "hubdbtable", "default": 2010782 } ``` | Parameter | Type | Description | Default | | --------- | ------ | -------------- | ------- | | `default` | String | HubDB table id | `null` | ## Icon ```json { "name": "icon_field", "label": "Icon", "required": false, "locked": false, "icon_set": "fontawesome-6.4.2", "type": "icon", "default": { "name": "accessible-icon", "unicode": "f368", "type": "REGULAR" } } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | Object | Icon object | | | `icon_set` | String | The FontAwesome icon set to use. Possible values are: | `fontawesome-5.0.10` | ## Image Image fields are supported in modules. Images can be used as [style fields](/guides/cms/content/fields/overview#style-fields). You should only use Image fields as style fields, if the image will be purely presentational, not convey meaning and not a [background image](#background-image). This is to follow best practices for [accessibility](/guides/cms/content/accessibility). ```json { "name": "image_field", "label": "Image", "required": false, "locked": false, "responsive": true, "resizable": true, "show_loading": false, "type": "image", "default": { "size_type": "exact", "src": "", "alt": "image-alt-text", "loading": "lazy", "width": 128, "height": 128, "max_width": 128, "max_height": 128 } } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | Object | Sets properties for image sizing, alt-text, and more. Can contain the following properties: | `{ "size_type" : "auto", "src" : "", "alt" : null, "loading": "disabled" }` | | `responsive` | Boolean | determines if the image is to act responsively or have a fixed height and width. | `true` | | `show_loading` | Boolean | Determines if the controls for choosing to [lazy load](/guides/cms/content/performance/lazy-loading) the image are shown in the page editor. | `false` | ## Link ```json { "name": "link_field", "display_width": null, "label": "Link", "required": false, "locked": false, "supported_types": [ "EXTERNAL", "CONTENT", "FILE", "EMAIL_ADDRESS", "BLOG", "CALL_TO_ACTION", "PHONE_NUMBER", "WHATSAPP_NUMBER", "PAYMENT" ], "show_advanced_rel_options": true, "type": "link", "default": { "url": { "content_id": null, "type": "EXTERNAL", "href": "" }, "open_in_new_tab": false, "no_follow": false } } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | Object | The default URL and link open behavior. This object includes: | `{ "url" : { "content_id" : null, "type" : "EXTERNAL", "href" : "" }, "open_in_new_tab" : false, "no_follow" : false, "sponsored" : false, "user_generated_content" : false }` | | `supported_types` | Array | The types of links that content creators can select. Remove types from the list that you don't want content creators to have access to set. Valid types include: | `[ "EXTERNAL", "CONTENT", "FILE", "EMAIL_ADDRESS", "BLOG", "CALL_TO_ACTION", "PHONE_NUMBER", "WHATSAPP_NUMBER", "PAYMENT" ]` | | `show_advanced_rel_options` | Boolean | By default, content creators will only be able to select the `no_follow` option.When set to `true`, content creators can also select:Learn more about [link attributes](https://moz.com/blog/nofollow-sponsored-ugc). | `false` | ## Logo ```json { "name": "logo", "label": "Logo", "required": false, "locked": false, "type": "logo", "show_loading": true, "default": { "override_inherited_src": false, "src": null, "alt": null, "loading": "lazy" } } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `show_loading` | String | Determines if the controls for choosing to [lazy load](/guides/cms/content/performance/lazy-loading) the image are shown in the page editor. | `false` | | `default` | Object | Logo object. If `show_loading` is set to `true`, you can include a `loading` property to set the image's [lazy loading options](/guides/cms/content/performance/lazy-loading). Options include: | `{ override_inherited_src: false, src: "", alt: null, width: null, height: null, loading: "disabled" suppress_company_name: false }` | ## Menu ```json { "name": "menu", "label": "Menu", "required": false, "locked": false, "type": "menu", "default": 12345678911 } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | Integer | The [menu ID](/guides/cms/content/menus-and-navigation#get-your-menu-id) for the menu. The default value of `null`, defaults to the default menu under navigation. | `null` | ## Number ```json { "name": "number_field", "label": "Number", "required": false, "locked": false, "display": "slider", "min": 1, "max": 10, "step": 1, "type": "number", "prefix": "", "suffix": "", "default": null, "placeholder": "50" } ``` | Parameter | Type | Description | Default | | ------------- | ------ | -------------------------------------- | ------- | | `default` | Number | A default number to be used. | `null` | | `prefix` | String | Added as a prefix to the number field. | | | `suffix` | String | Added as a suffix to the number field. | | | `placeholder` | String | Adds a placeholder value to the field. | | Suffix and prefix parameters are for display purposes in the content editor and have no effect on the numerical value of the field. ## Page ```json { "name": "page_field", "label": "Page", "help_text": "Pulls data from the selected page.", "required": false, "locked": false, "placeholder": "Page to pull from", "type": "page", "default": null } ``` | Parameter | Type | Description | Default | | --------- | ------- | --------------------------------- | ------- | | `default` | Integer | A default page id to be selected. | `null` | ## Rich text ```json { "name": "description", "label": "Description", "required": false, "locked": false, "type": "richtext", "default": null } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | String | string of content to be displayed supports HTML. Note: You cannot use the `get_asset_url` [function](/reference/cms/hubl/functions#get-asset-url) within this default property. | `""` | | `enabled_features` | Array | An array of items that allows you to [configure the Rich Text Editor Toolbar](/reference/cms/fields/rich-text-editor) and what options are available for content editors. | | ## Simple menu ```json { "name": "toc_menu", "label": "Table of Contents", "required": false, "locked": false, "type": "simplemenu", "default": [ { "isPublished": false, "pageLinkId": null, "pageLinkName": null, "isDeleted": null, "categoryId": null, "subCategory": null, "contentType": null, "state": null, "linkLabel": "Why is product marketing important?", "linkUrl": null, "linkParams": null, "linkTarget": null, "type": "NO_LINK", "children": [ { "isPublished": false, "pageLinkId": null, "pageLinkName": null, "isDeleted": null, "categoryId": null, "subCategory": null, "contentType": null, "state": null, "linkLabel": "Product Marketing Responsibilities", "linkUrl": "#product-marketing-responsibilities", "linkParams": null, "linkTarget": null, "type": "URL_LINK", "children": [] }, { "isPublished": false, "pageLinkId": null, "pageLinkName": null, "isDeleted": null, "categoryId": null, "subCategory": null, "contentType": null, "state": null, "linkLabel": "1. Identify the buyer personas and target audience for your product.", "linkUrl": "#step1", "linkParams": null, "linkTarget": null, "type": "URL_LINK", "children": [] }, { "isPublished": false, "pageLinkId": null, "pageLinkName": null, "isDeleted": null, "categoryId": null, "subCategory": null, "contentType": null, "state": null, "linkLabel": "2. Successfully create, manage and carry out your product marketing strategy.", "linkUrl": "#step2", "linkParams": null, "linkTarget": null, "type": "URL_LINK", "children": [] } ] }, { "isPublished": false, "pageLinkId": null, "pageLinkName": null, "isDeleted": null, "categoryId": null, "subCategory": null, "contentType": null, "state": null, "linkLabel": "How HubSpot can help", "linkUrl": "https://hubspot.com", "linkParams": null, "linkTarget": null, "type": "URL_LINK", "children": [] } ] } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | Array of objects | JSON structure for menu and menu children. | `[]` | ## Spacing ```json { "name": "img_spacing", "label": "Spacing around image", "required": false, "type": "spacing", "limits": { "padding": { "top": { "max": 50, "min": 25, "units": ["px", "pt", "em"] }, "left": { "max": 50, "units": ["px", "pt", "em"] }, "bottom": { "max": 50, "units": ["px", "pt", "em"] } }, "margin": { "top": { "max": 50, "min": 25, "units": ["px", "pt", "em"] }, "bottom": { "max": 25, "units": ["Q", "rem", "em"] } } }, "default": { "padding": { "top": { "value": 57, "units": "px" }, "bottom": { "value": 57, "units": "px" }, "left": { "value": 57, "units": "px" }, "right": { "value": 57, "units": "px" } }, "margin": { "top": { "value": 20, "units": "px" }, "bottom": { "value": 20, "units": "px" } } } } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | Object | Sets the default spacing values.Contains `padding` and `margin` objects:Use `units` to set the units that a content creator can use in HubSpot. Learn more about units below. | `{}` | | `limits` | Object | Sets the guidelines for `min` and `max` amount of spacing. Contains `padding` and `margin` objects:Use `units` to set the units that a content creator can use in HubSpot. Learn more about units below. | | When using the spacing field, please note the following: - You must include a `units` list when setting a `min` or `max`. - The `units` property supports the following unit types: `%`, `ch`, `em`, `ex`, `in`, `lh`, `pc`, `pt`, `px`, `Q`, `rem`, `vh`, `vmax`, `vmin`, and `vw`. - When a content creator edits all padding together, HubSpot will use the highest `min` value and the lowest `max` value. In addition, only the units shared by all sides will be available to the content creator. ## Tag ```json { "id": "c3395cd3-8e60-7e47-2f1b-b7ccf4d669c9", "name": "blog_tag", "label": "Blog Tag", "required": false, "locked": false, "tag_value": "SLUG", "type": "tag", "default": null } ``` | Parameter | Type | Description | Default | | --------- | ------ | ----------- | ------- | | `default` | String | Blog tag id | `null` | ## Text ```json { "name": "product_name", "label": "Product Name", "required": false, "locked": false, "validation_regex": "", "allow_new_line": false, "show_emoji_picker": false, "type": "text", "default": "" } ``` | Parameter | Type | Description | Default | | --------- | ------ | ------------ | ------- | | `default` | String | Text string. | `""` | ## Text Alignment ```json { "name": "heading_align", "label": "Heading alignment", "required": false, "type": "textalignment", "default": { "text_align": "LEFT" } } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | Object | Object containing `horizontal_align` and `vertical_align`. | | | `alignment_direction` | String | Determines if only horizontal, only vertical, or both alignment controls should be shown. Can be: | `BOTH` | ## URL ```json { "name": "url", "label": "URL", "required": false, "locked": false, "supported_types": [ "EXTERNAL", "CONTENT", "FILE", "EMAIL_ADDRESS", "BLOG", "PHONE_NUMBER", "WHATSAPP_NUMBER" ], "type": "url", "default": { "content_id": null, "href": "http://example.com", "type": "EXTERNAL" } } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | Object | URL object, with type, href and content id (if content is a page or post on HubSpot) | `{ "content_id" : null, "href" : "", "type" : "EXTERNAL" }` | | `supported_types` | Array | list of the types of links this field allows content creators to select. Remove types from the list that you don't want content creators to have access to set. Types include: | `[ "EXTERNAL", "CONTENT", "FILE", "EMAIL_ADDRESS", "BLOG" ]` | ## Video ```json { "id": "ca4a319e-5b58-422e-47ac-49ce1b51b507", "name": "videoplayer_field", "label": "Video", "required": false, "locked": false, "type": "videoplayer", "show_advanced_options": false, "default": { "player_id": 32173842991, "height": 1224, "width": 1872, "conversion_asset": { "type": "CTA", "id": "c3e4fa03-2c69-461d-b9af-22b2fde86bc7", "position": "POST" }, "loop_video": false, "mute_by_default": false, "autoplay": false, "hide_control": false } } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `default` | Object | Video object with settings for `player_id`, `height`, `width`, `size_type`, `conversion_asset`, `loop_video`, `mute_by_default`, `autoplay`, and `hide_control`. | `[]` | | `show_advanced_options` | Boolean | Whether content creators can see advanced default options. | `false` | ### conversion_asset object parameters | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `type` | String | Accepts either `"FORM"`, `"CTA"`, or `""` | `""` | | `id` | String | The id of the Form or CTA type | `""` | | `position` | String | Whether the conversion asset should be shown before the video starts or after it ends. Accepts either "PRE" or "POST". | `""` | # Configuring the Rich Text Editor @ The Rich Text Editor inside of custom modules now provides the ability for developers to remove components from the configuration toolbar. $ reference & cms/fields --- # Configuring the Rich Text Editor As a developer, there are times when WYSIWYG editors provide functionality that, when used incorrectly, can hinder the goal of a unified brand and cause content design and flow issues. The Rich Text Editor inside of custom modules now provides the ability for developers to remove components from the configuration toolbar through the `enabled_features` property inside of the fields.json file. Note: The following applies to custom modules utilizing the rich text field in local development only. Using this feature will not disable the functionality of options removed from the Rich Text Editor, just the display of the options. This is for backward compatibility reasons so existing content is not affected. ## How to use enabled_features In your fields.json file where you have your rich text field object, you can enable certain features by adding the valid toolbar options in an array to the `enabled_features` property as illustrated below: ```json // Rich text field with only Bold, Link, and Image available in the Toolbar { "name": "description", "label": "Description", "required": false, "locked": false, "type": "richtext", "default": null, "enabled_features": ["bold", "link", "image"] } ``` The content editor would then see the rich text editor with only the included options enabled as illustrated in the image below: Note: Some features, such as the “Clear Styles” button which allows you to revert to the default styling for the editor, will always be enabled and cannot be removed. If the `enabled_features` property is omitted, all features will be shown. ## Feature listings Below is a list of features which can be enabled individually when using the `enabled_features` property. ### Groups of controls | Option | Description | | --- | --- | | `colors` | Text color and background color controls. | | `fonts` | Font family and font size controls. | | `indents` | Outdent and indent controls. | | `lists` | Bulleted and numbered lists controls. | | `standard_emphasis` | Bold, italic, and underline controls. | | `advanced_emphasis` | Strikethrough, superscript, subscript, and code format controls. | | `glyphs` | Emoji, special character, and icon controls. Not supported in email modules. To add the emoji picker to email modules, use [emoji](#insert-buttons) instead. | ### Text Formating | Option | Description | | ------------------ | --------------------------------------- | | `block` | Shows the style switcher dropdown menu. | | `font_family` | Shows the font switcher dropdown menu. | | `font_size` | Shows the font size dropdown menu. | | `bold` | Shows the bold button. | | `italic` | Shows the italics button. | | `underline` | Shows the underline button. | | `text_color` | Shows the text color button. | | `background_color` | Shows the background color button. | | `alignment` | Shows the alignment button. | | `bulleted_list` | Shows the bulleted list button. | | `numbered_list` | Shows the numbered lists button. | | `lineheight` | Shows the line-height button. | | `outdent` | Shows the outdent button. | | `indent` | Shows the indent button. | | `strikethrough` | Shows the strikethrough button. | | `superscript` | Shows the superscript button. | | `subscript` | Shows the subscript button. | | `code_format` | Shows the code format button. | ### Insert Buttons | Option | Description | | --- | --- | | `link` | Shows the link button. | | `image` | Shows the image button. Not supported in email modules. | | `emoji` | Shows the emoji button. | | `personalize` | Shows the personalize toolbar item. | | `cta` | Shows the call-to-action menu item under the insert menu. | | `embed` | Shows the embed menu item under the insert menu. | | `video` | Shows the video menu item under the insert menu. | | `table` | Shows the table menu item under the insert menu. | | `charmap` | Shows the special character menu item under the insert menu. | | `anchor` | Shows the anchor menu item under the insert menu. | | `hr` | Shows the horizontal line menu item under the insert menu. | | `nonbreaking_space` | Shows the non-breaking space menu item under the insert menu. | | `icon` | Shows the icon menu item under the insert menu. | ### Advanced Options | Option | Description | | --------------- | -------------------------------------------------------- | | `source_code` | Shows the source code menu item under the advanced menu. | | `visual_blocks` | Shows the show blocks menu item under the advanced menu. | # Forms reference @ View reference information for forms, including global form events and defining custom styling for embedded forms. $ reference & cms/forms --- # Forms reference [HubSpot forms](https://knowledge.hubspot.com/forms/create-forms) allow you to capture information from website visitors, which you can then access throughout HubSpot. You can share links to forms directly with users, [submit form data via the API](/guides/api/marketing/forms), and embed them on your website pages using the CMS. Below, find reference documentation for forms, including form embed configuration options, internationalization and validation error messages, and form events. ## Global form events Global form events allow you to extend the functionality of your form and define custom behavior for specific events triggered by your form (e.g., display a custom success message when a user submits a form). Learn more about using global form events for forms created in the updated forms editor in [this article](/guides/api/marketing/forms/global-form-events). ## Define custom styling for embedded forms using CSS You can define CSS variables to override the default styling for your embedded forms. This allows you to apply granular styling to specific elements of your forms, as well as define global styling rules to maintain consistency across all embedded forms. To ensure that your custom CSS variables override existing form styling configured within the forms editor, you should target the `.hsfc-FormWrapper` class in your CSS variable declarations. You can then define any custom styling using the variables listed in the tables below. ```js .hsfc-FormWrapper { --hsf-global__font-size: 2em; } ``` The CSS variables you can override are listed in the tables below. The global styles will override top-level styling across all your embedded forms. You can also target individual elements of your forms such as a form element row, submit buttons, or form headings. ### Global | Variable | Description | | --------------------------- | -------------------------------------------- | | `--hsf-global__font-family` | Sets the font family for all elements | | `--hsf-global__font-size` | Sets the font size for all elements | | `--hsf-global__color` | Sets the default text color for all elements | | `--hsf-global-error__color` | Sets the color for errors | ### Row | Variable | Description | | -------------------------------- | ----------------------------------------- | | `--hsf-row__horizontal-spacing` | Sets the horizontal spacing between rows | | `--hsf-row__vertical-spacing` | Sets the vertical spacing between rows | | `--hsf-module__vertical-spacing` | Sets the vertical spacing between modules | ### Button | Variable | Description | | -------------------------------- | ----------------------------------- | | `--hsf-button__font-family` | Sets font family of all buttons | | `--hsf-button__font-size` | Sets font size of all buttons | | `--hsf-button__color` | Sets text color of all buttons | | `--hsf-button__background-color` | Set background color of all buttons | | `--hsf-button__background-image` | Set background image of all buttons | | `--hsf-button__border-radius` | Sets border radius of all buttons | | `--hsf-button__padding` | Sets the padding of all buttons | | `--hsf-button__box-shadow` | Sets the box shadow of all buttons | ### Rich Text | Variable | Description | | ----------------------------- | ------------------------------------------- | | `--hsf-richtext__font-family` | Sets the font family for rich text elements | | `--hsf-richtext__font-size` | Sets the font size for rich text elements | | `--hsf-richtext__color` | Sets the text color for text elements | ### Heading | Variable | Description | | ---------------------------- | -------------------------------------------- | | `--hsf-heading__font-family` | Sets the font family for the form's headings | | `--hsf-heading__color` | Sets the text color for form's headings | | `--hsf-heading__text-shadow` | Sets the text shadow for form's headings | ### Background | Variable | Description | | --- | --- | | `--hsf-background__background-color` | Sets the background color of the form | | `--hsf-background__background-image` | Set the background image of the form | | `--hsf-background__background-size` | Set the background size | | `--hsf-background__background-position` | Set the background position | | `--hsf-background__background-repeat` | Set the background repeat behavior | | `--hsf-background__border-style` | Sets the border style of the form | | `--hsf-background__border-color` | Sets the border color of the form | | `--hsf-background__border-radius` | Sets the border radius of the form | | `--hsf-background__border-width` | Sets the border width of the form | | `--hsf-background__padding` | Sets the padding of the form | ### Progress Bar | Variable | Description | | --- | --- | | `--hsf-progressbar-text__font-family` | Sets the font family for the progress bar text | | `--hsf-progressbar-text__font-size` | Sets the font size for the progress bar text | | `--hsf-progressbar-text__color` | Sets the color for the progress bar text | | `--hsf-progressbar-progressLine__background-color` | Sets the background color of the progress line | | `--hsf-progressbar-progressLine__background-image` | Sets the background image of the progress line | | `--hsf-progressbar-progressLine__border-color` | Sets the border color of the progress line | | `--hsf-progressbar-progressLine__border-style` | Sets the border style of the progress line | | `--hsf-progressbar-progressLine__border-width` | Sets the border width of the progress line | | `--hsf-progressbar-trackLine__background-color` | Sets the background color of the track line | ### Error Alert | Variable | Description | | ------------------------------- | ------------------------------------- | | `--hsf-erroralert__font-family` | Sets the font family for error alerts | | `--hsf-erroralert__font-size` | Sets the font size for error alerts | | `--hsf-erroralert__color` | Sets the text color for error alerts | ### Info Alert | Variable | Description | | ------------------------------ | ------------------------------------ | | `--hsf-infoalert__font-family` | Sets the font family for info alerts | | `--hsf-infoalert__font-size` | Sets the font size for info alerts | | `--hsf-infoalert__color` | Sets the text color for info alerts | ### Field Input | Variable | Description | | --- | --- | | `--hsf-field-label__font-family` | Sets the font family of the field label | | `--hsf-field-label__font-size` | Sets the font size of the field label | | `--hsf-field-label__color` | Sets the text color of the field label | | `--hsf-field-label-requiredindicator__color` | Sets the color of the required indicator for the field label | | `--hsf-field-description__font-family` | Sets the font family of the field description | | `--hsf-field-description__color` | Sets the text color of the field description | | `--hsf-field-footer__font-family` | Sets the font family of the field footer | | `--hsf-field-footer__color` | Sets the text color of the field footer | | `--hsf-field-input__font-family` | Sets the font family of the field input | | `--hsf-field-input__color` | Sets the text color of the field input | | `--hsf-field-input__background-color` | Sets the background color of the field input | | `--hsf-field-input__placeholder-color` | Sets the placeholder color of the field input | | `--hsf-field-input__border-color` | Sets the border color of the field input | | `--hsf-field-input__border-width` | Sets the border width of the field input | | `--hsf-field-input__border-style` | Sets the border style of the field input | | `--hsf-field-input__border-radius` | Sets the border radius of the field input | | `--hsf-field-input__padding` | Sets the padding of the field input | ### Field Textarea | Variable | Description | | --- | --- | | `--hsf-field-textarea__font-family` | Sets the font family for the textarea | | `--hsf-field-textarea__color` | Sets the text color for the textarea | | `--hsf-field-textarea__background-color` | Sets the background color for the textarea | | `--hsf-field-textarea__placeholder-color` | Sets the color for the placeholder text in the textarea | | `--hsf-field-textarea__border-color` | Sets the border color for the textarea | | `--hsf-field-textarea__border-width` | Sets the border width for the textarea | | `--hsf-field-textarea__border-style` | Sets the border style for the textarea | | `--hsf-field-textarea__border-radius` | Sets the border-radius for the textarea | | `--hsf-field-textarea__padding` | Sets the padding for the textarea | ### Field Checkbox | Variable | Description | | --- | --- | | `--hsf-field-checkbox__padding` | Sets the padding within form checkboxes | | `--hsf-field-checkbox__background-color` | Sets the background color of the checkboxes | | `--hsf-field-checkbox__color` | Sets the text color of the checkboxes | | `--hsf-field-checkbox__border-color` | Sets the color of the checkbox's border | | `--hsf-field-checkbox__border-width` | Sets the width of the checkbox's border | | `--hsf-field-checkbox__border-style` | Sets the style of the checkbox's border | ### Field Radio | Variable | Description | | --- | --- | | `--hsf-field-radio__padding` | Sets the padding for radio button | | `--hsf-field-radio__background-color` | Sets the background color for radio button | | `--hsf-field-radio__color` | Sets the text color of radio button | | `--hsf-field-radio__border-color` | Sets the border color for radio button | | `--hsf-field-radio__border-width` | Sets the border width for radio button | | `--hsf-field-radio__border-style` | Sets the border style for radio button | # Legacy forms @ View reference information for legacy forms, including available form events, validation error messages, and internationalization options. $ reference & cms/forms --- # Legacy forms reference [Legacy forms](https://knowledge.hubspot.com/forms/create-forms) allow you to capture information from website visitors, which you can then access throughout HubSpot. You can share links to forms directly with users, [submit form data via the API](/guides/api/marketing/forms), and embed them on your website pages using the CMS. Below, find reference documentation for forms, including form embed configuration options, internationalization and validation error messages, and form events. If you're using jQuery to manipulate the values of form inputs (i.e. using `val()` or `prop()`), you must trigger a change event using `change()` or `trigger('change')` for the change to properly register. ```js // Examples $('input[value="checkbox_1"]').prop('checked', true).change(); $('input[name="firstname"]').val('Brian').change(); // If not using jQuery input.value = value; input.dispatchEvent(new Event('input', { bubbles: true })); // For single checkbox fields input.click(); ``` ## Form embed configuration When embedding a form, you can include the following customization options in the form embed code. - Form customization options are only available for forms created in HubSpot that have been [set as raw HTML](https://knowledge.hubspot.com/forms/how-can-i-share-a-hubspot-form-if-im-using-an-external-site#with-css-in-your-external-stylesheet-marketing-hub-professional-enterprise-or-legacy-marketing-hub-basic-only). The HubSpot account must have a _**Marketing Hub**_ or **_Content Hub_** _Professional_ or _Enterprise_ subscription. - HubSpot forms should only be loaded using the HubSpot-hosted JavaScript file. Making a copy of the form embed code and self-hosting it is not supported. Any improvements that HubSpot makes to improve security, anti-spam, accessibility, and performance will not propagate to your form if you decide to self-host the HubSpot form embed code. ```html ``` | Parameter | Type | Description | | --- | --- | --- | | `portalId` | Number or string | The ID of the HubSpot account that the form was created in. This is used to retrieve the form definition. | | `formId` | String | The form's ID, which is used to retrieve the form definition. | | `region` | String | The [region](https://knowledge.hubspot.com/account-security/hubspot-cloud-infrastructure-and-data-hosting-frequently-asked-questions) of the account where the form was created. This is used to retrieve the form definition. Possible values are `na1` or `eu1`. | | `target` | String | A valid CSS selector string, specifying an existing element on the page into which the form will be embedded.If you're including multiple forms on the page, it is strongly recommended that you include a separate, specific target for each form. It's strongly recommended you specify this property, otherwise the form is injected after the script location. | | `redirectUrl` | String | The URL that the form will redirect to upon successful submission. Cannot be used with `inlineMessage`. | | `inlineMessage` | String | The message to display in place of the form upon successful submission. Cannot be used with `redirectUrl`. | | `pageId` | Number or string | The ID of the HubSpot page that you're embedding the form on. Associates the form submissions with the page. | | `cssRequired` | String | CSS string specific to validation error messages and form styling. Set this property to an empty string if you don't want to apply default HubSpot styling to the form elements. | | `cssClass` | String | The class that will be applied to the form. | | `css` | String | A CSS string that, when defined, is used instead of the built-in CSS theme. This can be used for setting your own CSS styling. | | `submitText` | String | String that overrides the submit button text. | | `submitButtonClass` | String | CSS class that will be applied to the submit button instead of the default `.hs-button.primary.large`. | | `errorClass` | String | CSS class that will be applied to the inputs with validation errors instead of the default `.invalid.error`. | | `errorMessageClass` | String | CSS class that will be applied to error messages instead of the default `.hs-error-msgs.inputs-list`. | | `locale` | String | The form's locale, which is used to customize language for form errors, date pickers, labels, and links. Learn more about [adding internationalized error messages](#internationalization). | | `translations` | Object | An object containing additional translation languages or to override field labels and messages for existing languages. Learn more about [adding internationalization](#internationalization). | | `manuallyBlockedEmailDomain` | Array | Array of domains, specified as strings, to block in email input fields. | | `formInstanceId` | String | Required when embedding the same form on the same page multiple times. This ID can be arbitrary, so long as it's unique. | ### Embed code callbacks The form embed code includes event callbacks that you can use to extend form functionality when the events below occur. These are separate from the [global form events](#form-events) that you can hook into. ```html ``` | Parameter | Type | Description | | --- | --- | --- | | `onBeforeFormInit` | Function | A callback that executes before the form builds. Takes form configuration object as a single argument: `onBeforeFormInit(ctx)`. | | `onFormReady` | Function | A callback that executes just before the form render. This callback should be used for any logic that needs to execute when the form has fully rendered on the page. Takes the jQuery form object as the argument: `onFormReady($form)`. | | `onFormSubmit` | Function | A callback that executes after the form is validated and before the form is submitted (i.e., right before the data is actually sent). This is for any logic that needs to execute before submission is initiated. Any changes made to form fields will not be validated. Takes the jQuery form object as the argument: `onFormSubmit($form)`.

It is not recommended to perform a browser redirect in this callback, as it could prevent the form submission. For any custom redirects, use the `onFormSubmitted` callback.

| | `onBeforeFormSubmit` | Function | A callback that executes after the form is validated, before the form is submitted i.e. before the data is actually sent. This is for any logic that needs to execute before submission is initiated. Any changes made to fields will not be validated.

This callback behaves in the same way as `onFormSubmit`, but is preferred due to the accurate naming of when it is triggered. Takes the jQuery form object as the argument and an array of submission values: `onBeforeFormSubmit($form, submissionValues)`. As in the case of `onFormSubmit`, It is not recommended to perform a browser redirect in this callback as this could prevent before the submission is initiated, thus preventing form submissions. For any custom redirects, please use the `onFormSubmitted` callback. | | `onFormSubmitted` | Function | Callback that executes after form submission. This is for executing actions when the submission is fully complete, such as displaying a confirmation or thank you message, or performing a custom redirect. Takes the jQuery form object as the argument and an array of submission values: `onFormSubmitted($form, data)`.The callback's arguments are the form element and an object containing two properties: | ## Internationalization By default, HubSpot provides a set of translated date picker field labels and [validation messages](#validation-messages) for a set of supported languages. You can also add custom languages or override specific error messages and date picker months/days displayed on the form [using the translation parameter](#custom-internationalization). To include the default translated strings for a supported language, add the `locale` parameter to the embed code, followed by one of the languages in the table below. ```js hbspt.forms.create({ portalId: '', formId: '', locale: 'fi', }); ``` | Value | Language | | ------- | ------------------- | | `en` | English | | `da` | Danish | | `de` | German | | `es` | Spanish (Spain) | | `es-mx` | Spanish (Mexico) | | `fi` | Finnish | | `fr` | French | | `it` | Italian | | `ja` | Japanese | | `nl` | Dutch | | `pl` | Polish | | `pt-br` | Portuguese (Brazil) | | `sv` | Swedish | | `zh-cn` | Chinese | | `zh-hk` | Chinese (Hong Kong) | To add custom languages or override field labels and default translated strings, you can pass language objects into the `translations` parameter that correspond to the desired `locale`. For supported locales, you only need to provide the keys and messages you wish to override. For example, the code below configures the form to replace the `email` field label, required field validation message, and submit button text with custom strings. Learn more about [validation below](#validation). ```js hbspt.forms.create({ portalId: '', formId: '', locale: 'en', translations: { en: { fieldLabels: { email: 'Electronic mail', }, required: "Hey! That's required!", submitText: 'Custom Submit Button Text', }, }, }); ``` | Value | Type | Language | | --- | --- | --- | | `translations` | Object | An object containing additional translation languages or to override field labels and messages for existing languages. | | `en` | Object | An object containing custom translations for the specified `locale`. | | `fieldLabels` | Object | An object containing field label replacement values. In this object, you'll specify the name of the form field followed by your custom label. | | `required` | String | Replaces the validation message for required field errors with a custom message. | | `submitText` | String | Replaces the submit button text with a custom string. | In addition to the supported locales, you can register new locale codes in the `locale` parameter. In this case, make sure to specify messages for _all_ of the keys listed in the table below. Omitted keys will show a "missing translation" message in their place. | Key | English default | | --- | --- | | `learnMore` | Learn more | | `submitText` | Submit | | `previousMonth` | Previous Month | | `nextMonth` | Next Month | | `january` | January | | `february` | February | | `march` | March | | `april` | April | | `may` | May | | `june` | June | | `july` | July | | `august` | August | | `september` | September | | `october` | October | | `november` | November | | `december` | December | | `sunday` | Sunday | | `monday` | Monday | | `tuesday` | Tuesday | | `wednesday` | Wednesday | | `thursday` | Thursday | | `friday` | Friday | | `saturday` | Saturday | | `sundayShort` | Sun | | `mondayShort` | Mon | | `tuesdayShort` | Tue | | `wednesdayShort` | Wed | | `thursdayShort` | Thu | | `fridayShort` | Fri | | `saturdayShort` | Sat | | `fallbackDescription` | We had some trouble loading this form. | | `fallbackCta` | Click here to continue. | | `fallbackGenericDescription` | This form didn't load. Please try again later. | | `fileTooLarge` | Selected file is too large. Maximum allowed size is 100MB. | | `defaultSelectOptionLabel` | Please Select | | `notYou` | Not you? | | `resetTheForm` | Click here to reset. | ## Validation messages HubSpot provides three layers of form validation: - **Live validation:** validation that occurs while a visitor is filling out a form. - **Client-side validation:** validation that occurs after the visitor attempts to submit the form, but before the submission request has been sent to the server. - **Server-side validation:** validation that occurs after form submission. During each step of validation, HubSpot provides a set of default error messages. These messages can be overridden by using the `locale` and `translation` [parameters](#internationalization) in the form embed code. Below, learn more about the default validation error messages and how to override them depending on when they occur. ### Live validation errors Below are the currently supported live validation field error keys for contextual overriding. These error keys can be specified in the language object of the `translations` parameter. ```js hbspt.forms.create({ portalId: '', formId: '', locale: 'en', translations: { en: { required: "Hey! That's required!", invalidEmailFormat: 'Email address formatted incorrectly.', invalidNumber: 'Not a valid number.', }, }, }); ``` | Error key | English default | | --- | --- | | `requiredField` | Please complete this required field. | | `required` | Please complete this required field. | | `missingSelect` | Please complete this required field. | | `missingOptionSelection` | Please complete this required field. | | `fileTooLarge` | Selected file is too large. Maximum allowed size is 100MB. | | `invalidEmailFormat` | Email must be formatted correctly. | | `phoneInvalidCharacters` | A valid phone number may only contain numbers and any of the following characters: `+`, `(`, `)`, `-`, `x`. | | `invalidNumber` | Please enter a valid number. | | `invalidDate` | Please enter a valid date. | | `invalidEmail` | Please enter a valid email address. | | `invalidNumberRangeTooSmall` | Please enter a number that's greater than or equal to . | | `invalidNumberRangeTooLarge` | Please enter a number that's less than or equal to . | | `forbiddenEmailDomain` | Please enter a different email address. This form does not accept addresses from `{domain}`. | | `manuallyBlockedEmailDomain` | Please enter a different email address. This form does not accept addresses from `{domain}`. | | `numberOutOfRange` | The number you entered is not in range. | | `inputTooLarge` | Please use fewer than 65536 characters. | ### Client-side validation errors The following errors may be generated and displayed client-side. They can be contextually overridden using a `submissionErrors` object. ```js hbspt.forms.create({ portalId: '', formId: '', locale: 'en', translations: { en: { submissionErrors: { MISSING_REQUIRED_FIELDS: 'Please fill in all required fields.', }, }, }, }); ``` | Error key | English default | | --- | --- | | `submissionErrors.MISSING_REQUIRED_FIELDS` | Please complete all required fields. | | `submissionErrors.BLOCKED_EMAIL` | Please change your email address to continue. | | `submissionErrors.TOO_MANY_REQUESTS` | There was an issue submitting your form. Please wait a few seconds and try again. | ### Server-side validation errors The following errors may be generated on the server after a submission request has been sent and displayed client-side when a response is received. They can be contextually overridden using a `submissionErrors` object. ```js hbspt.forms.create({ portalId: '', formId: '', locale: 'en', translations: { en: { submissionErrors: { SERVER_ERROR: 'Sorry, something went wrong. Please try again.', }, }, }, }); ``` | Error key | English default | | --- | --- | | `submissionErrors.SERVER_ERROR` | Sorry, something went wrong and the form was not submitted. Please try again later. | | `submissionErrors.RECAPTCHA_VALIDATION_ERROR` | Failed to validate Captcha. Please try again. | | `submissionErrors.OUT_OF_DATE` | This form is no longer current. Please refresh the page and try again. | | `submissionErrors.FORM_NOT_PUBLISHED` | This form is no longer active. Please refresh the page and try again. | | `submissionErrors.MISSING_SCOPE` | This form cannot be submitted. Please contact the site owner. | | `submissionErrors.FORM_NEVER_EXISTED` | This form cannot be submitted. Please contact the site owner. | | `submissionErrors.FORM_TYPE_MISMATCH` | This form cannot be submitted. Please contact the site owner. | | `submissionErrors.FILE_TOO_LARGE` | Selected file is too large. Maximum allowed size is 100MB. | ## Form events Forms allow two ways to bind functionality onto events, including [callbacks in the HubSpot form embed code](#embed-code-callbacks) and global form events. Use these events to trigger custom JavaScript. If you need complete control over the styles and actions of your form, it's recommended that you build your own custom form and submit the data using the [Forms API](/reference/api/marketing/forms/v3-legacy). ```js window.addEventListener('message', (event) => { if ( event.data.type === 'hsFormCallback' && event.data.eventName === 'onFormSubmitted' ) { someAnalyticsLib('formSubmitted'); console.log('Form Submitted! Event data: ${event.data}'); } }); ``` - These events are non-blocking, so it's not possible to prevent a form submission using the `onFormSubmit` callback. - You cannot change form submission data using `onBeforeFormSubmit`. When using `onBeforeFormSubmit`, the form is submitted as the event is emitted to the browser. Any listeners hooked to the events do not block the main thread of the form's execution. For synchronous changes to the form, it is recommended to [customize the form embed code](/guides/cms/content/forms) instead. ### onBeforeFormInit Called before the form has been inserted into DOM. ```js { type: 'hsFormCallback', eventName: 'onBeforeFormInit', id: 'Id of form submitted', data: {} } ``` ### onFormReady Called after the form has been inserted into DOM. ```js { type: 'hsFormCallback', eventName: 'onFormReady', id: 'Id of form submitted', data: {} } ``` ### onBeforeFormSubmit Called at the start of form submission, but before submission has been persisted. Behaves the same as `onFormSubmit`, but is preferred due to more accurate naming indicating when this event is triggered. When performing custom redirects, use [onFormSubmitted](#onformsubmitted) instead, as this event may prevent submissions being initiated, thus preventing form submissions. ```js { type: 'hsFormCallback', eventName: 'onBeforeFormSubmit', id: 'ID of the form to submit', data: [ // Array containing the names and values // of the fields about to be submitted { name: 'email', value: 'test@example.com' }, { name: 'firstName', value: 'Jane' }, { name: 'lastName', value: 'Doe' } ] } ``` ### onFormSubmit Called at the start of form submission, but before submission has been persisted. Please use `onBeforeFormSubmit` instead. When performing custom redirects, use [onFormSubmitted](#onformsubmitted) instead, as this event may prevent submissions being initiated, thus preventing form submissions. ```js { type: 'hsFormCallback', eventName: 'onFormSubmit', id: 'ID of the form to submit', data: [ // Array containing the names and values // of the fields about to be submitted { name: 'email', value: 'test@example.com' }, { name: 'firstName', value: 'Jane' }, { name: 'lastName', value: 'Doe' } ] } ``` ### onFormSubmitted Called after the form has been submitted and the submission has persisted. Recommended for performing custom redirects. ```js { type: 'hsFormCallback', eventName: 'onFormSubmitted', id: 'ID of form submitted', data: { // String containing redirect url, if set redirectUrl: "https://example-url.com", // Object containing key-value pairs of submitted data submissionValues: { 'email': 'test@example.com', 'firstName': 'Jane', 'lastName': 'Doe' } } } ``` ## Define custom styling for embedded forms using CSS You can define CSS variables to override the default styling for your embedded forms. This allows you to apply granular styling to specific elements of your forms, as well as define global styling rules to maintain consistency across all embedded forms. To ensure that your custom CSS variables override existing form styling configured within the forms editor, you should target the `.hsfc-FormWrapper` class in your CSS variable declarations. You can then define any custom styling using the variables listed in the tables below. ```js .hsfc-FormWrapper { --hsf-global__font-size: 2em; } ``` The CSS variables you can override are listed in the tables below. The global styles will override top-level styling across all your embedded forms. You can also target individual elements of your forms such as a form element row, submit buttons, or form headings. ### Global | Variable | Description | | --------------------------- | -------------------------------------------- | | `--hsf-global__font-family` | Sets the font family for all elements | | `--hsf-global__font-size` | Sets the font size for all elements | | `--hsf-global__color` | Sets the default text color for all elements | | `--hsf-global-error__color` | Sets the color for errors | ### Row | Variable | Description | | -------------------------------- | ----------------------------------------- | | `--hsf-row__horizontal-spacing` | Sets the horizontal spacing between rows | | `--hsf-row__vertical-spacing` | Sets the vertical spacing between rows | | `--hsf-module__vertical-spacing` | Sets the vertical spacing between modules | ### Button | Variable | Description | | -------------------------------- | ----------------------------------- | | `--hsf-button__font-family` | Sets font family of all buttons | | `--hsf-button__font-size` | Sets font size of all buttons | | `--hsf-button__color` | Sets text color of all buttons | | `--hsf-button__background-color` | Set background color of all buttons | | `--hsf-button__background-image` | Set background image of all buttons | | `--hsf-button__border-radius` | Sets border radius of all buttons | | `--hsf-button__padding` | Sets the padding of all buttons | | `--hsf-button__box-shadow` | Sets the box shadow of all buttons | ### Rich Text | Variable | Description | | ----------------------------- | ------------------------------------------- | | `--hsf-richtext__font-family` | Sets the font family for rich text elements | | `--hsf-richtext__font-size` | Sets the font size for rich text elements | | `--hsf-richtext__color` | Sets the text color for text elements | ### Heading | Variable | Description | | ---------------------------- | -------------------------------------------- | | `--hsf-heading__font-family` | Sets the font family for the form's headings | | `--hsf-heading__color` | Sets the text color for form's headings | | `--hsf-heading__text-shadow` | Sets the text shadow for form's headings | ### Background | Variable | Description | | --------------------------------- | ---------------------------------- | | `--hsf-background__background` | Sets the background of the form | | `--hsf-background__border-style` | Sets the border style of the form | | `--hsf-background__border-color` | Sets the border color of the form | | `--hsf-background__border-radius` | Sets the border radius of the form | | `--hsf-background__border-width` | Sets the border width of the form | | `--hsf-background__padding` | Sets the padding of the form | ### Progress Bar | Variable | Description | | --- | --- | | `--hsf-progressbar-text__font-family` | Sets the font family for the progress bar text | | `--hsf-progressbar-text__font-size` | Sets the font size for the progress bar text | | `--hsf-progressbar-text__color` | Sets the color for the progress bar text | | `--hsf-progressbar-progressLine__background-color` | Sets the background color of the progress line | | `--hsf-progressbar-progressLine__background-image` | Sets the background image of the progress line | | `--hsf-progressbar-progressLine__border-color` | Sets the border color of the progress line | | `--hsf-progressbar-progressLine__border-style` | Sets the border style of the progress line | | `--hsf-progressbar-progressLine__border-width` | Sets the border width of the progress line | | `--hsf-progressbar-trackLine__background-color` | Sets the background color of the track line | ### Error Alert | Variable | Description | | ------------------------------- | ------------------------------------- | | `--hsf-erroralert__font-family` | Sets the font family for error alerts | | `--hsf-erroralert__font-size` | Sets the font size for error alerts | | `--hsf-erroralert__color` | Sets the text color for error alerts | ### Info Alert | Variable | Description | | ------------------------------ | ------------------------------------ | | `--hsf-infoalert__font-family` | Sets the font family for info alerts | | `--hsf-infoalert__font-size` | Sets the font size for info alerts | | `--hsf-infoalert__color` | Sets the text color for info alerts | ### Field Input | Variable | Description | | --- | --- | | `--hsf-field-label__font-family` | Sets the font family of the field label | | `--hsf-field-label__font-size` | Sets the font size of the field label | | `--hsf-field-label__color` | Sets the text color of the field label | | `--hsf-field-label-requiredindicator__color` | Sets the color of the required indicator for the field label | | `--hsf-field-description__font-family` | Sets the font family of the field description | | `--hsf-field-description__color` | Sets the text color of the field description | | `--hsf-field-footer__font-family` | Sets the font family of the field footer | | `--hsf-field-footer__color` | Sets the text color of the field footer | | `--hsf-field-input__font-family` | Sets the font family of the field input | | `--hsf-field-input__color` | Sets the text color of the field input | | `--hsf-field-input__background-color` | Sets the background color of the field input | | `--hsf-field-input__placeholder-color` | Sets the placeholder color of the field input | | `--hsf-field-input__border-color` | Sets the border color of the field input | | `--hsf-field-input__border-width` | Sets the border width of the field input | | `--hsf-field-input__border-style` | Sets the border style of the field input | | `--hsf-field-input__border-radius` | Sets the border radius of the field input | | `--hsf-field-input__padding` | Sets the padding of the field input | ### Field Textarea | Variable | Description | | --- | --- | | `--hsf-field-textarea__font-family` | Sets the font family for the textarea | | `--hsf-field-textarea__color` | Sets the text color for the textarea | | `--hsf-field-textarea__background-color` | Sets the background color for the textarea | | `--hsf-field-textarea__placeholder-color` | Sets the color for the placeholder text in the textarea | | `--hsf-field-textarea__border-color` | Sets the border color for the textarea | | `--hsf-field-textarea__border-width` | Sets the border width for the textarea | | `--hsf-field-textarea__border-style` | Sets the border style for the textarea | | `--hsf-field-textarea__border-radius` | Sets the border-radius for the textarea | | `--hsf-field-textarea__padding` | Sets the padding for the textarea | ### Field Checkbox | Variable | Description | | --- | --- | | `--hsf-field-checkbox__padding` | Sets the padding within form checkboxes | | `--hsf-field-checkbox__background-color` | Sets the background color of the checkboxes | | `--hsf-field-checkbox__color` | Sets the text color of the checkboxes | | `--hsf-field-checkbox__border-color` | Sets the color of the checkbox's border | | `--hsf-field-checkbox__border-width` | Sets the width of the checkbox's border | | `--hsf-field-checkbox__border-style` | Sets the style of the checkbox's border | ### Field Radio | Variable | Description | | --- | --- | | `--hsf-field-radio__padding` | Sets the padding for radio button | | `--hsf-field-radio__background-color` | Sets the background color for radio button | | `--hsf-field-radio__color` | Sets the text color of radio button | | `--hsf-field-radio__border-color` | Sets the border color for radio button | | `--hsf-field-radio__border-width` | Sets the border width for radio button | | `--hsf-field-radio__border-style` | Sets the border style for radio button | # Deprecated HubL Supported Functions @ All HubL functions that have been deprecated, and the alternatives you can use instead. $ reference & cms/hubl --- # Deprecated HubL filters and functions The following is a list of HubL filters and functions that are deprecated. While these filters and functions still operate as intended, they've been replaced by newer ones that are more streamlined and optimized. For all new and future projects we encourage using our [current HubL functions](/reference/cms/hubl/functions) instead of deprecated ones. ## Deprecated filters The following filters have been deprecated: - [datetimeformat](#datetimeformat) - [format_currency](#format-currency) ### datetimeformat This function has been replaced by [format_datetime](/reference/cms/hubl/filters#format-datetime). ```hubl {{ content.updated|datetimeformat("%B %e, %Y") }} {{ content.publish_date|datetimeformat("%B %e, %Y %l %p") }} {{ content.publish_date|datetimeformat("%B %e, %Y %l %p", "America/Los_Angeles") }} {{ content.publish_date|datetimeformat("%B %e, %Y %l %p", "America/Los_Angeles", "es-US") }} ``` ```html October 17, 2020 October 1, 2020 4 PM October 1, 2020 9 AM octubre 1, 2020 9 a.m. ``` ### format_currency This function has been replaced by [format_currency_value](/reference/cms/hubl/filters#format-currency-value). ```hubl {% set price = 100 %} {{ price|format_currency("en-US") }} {{ price|format_currency("fr-FR") }} {{ price|format_currency("jp-JP", "JPY", true) }} ``` ```html $100
100 $
¥ 100 ``` ## Deprecated functions The following functions have been deprecated: - [blog_post_by_id](#blog-post-by-id) - [blog_topics](#blog-topics) - [blog_recent_topic_posts](#blog-recent-topic-posts) - [datetimeformat](#datetimeformat-nbsp-) - [get_public_template_url](#get-public-template-url) - [include_css](#include-css) - [include_javascript](#include-javascript) - [page_by_id](#page-by-id) ### blog_post_by_id This function has been replaced by [**content_by_id()**](/reference/cms/hubl/functions#content-by-id). ```hubl {% set my_post = blog_post_by_id(4715624297) %} ``` ```html ``` ### blog_topics This function has been renamed to [**blog_tags()**](/reference/cms/hubl/functions#blog-tags). ```hubl {{ blog_topics("default", 250) }} {% set my_tags = blog_topics("default", 250) %} ``` ```html [Insider] ``` ### blog_recent_topic_posts This function has been renamed to [blog_recent_tag_posts()](/reference/cms/hubl/functions#blog-recent-tag-posts). ```hubl {{ blog_recent_topic_posts("default", "culture", 5) }} ``` ### datetimeformat This function has been replaced by [format_datetime()](/reference/cms/hubl/functions#format-datetime). ```hubl {{ datetimeformat(content.publish_date_local_time, "%B %e, %Y") }} ``` ```html February 27, 2020 ``` ### get_public_template_url This function has been replaced by [**get_asset_url()**](/reference/cms/hubl/functions#get-asset-url). ```hubl {{ get_public_template_url("custom/page/Designers_2015/designer-doc-2105.js") }} ``` ```html //cdn2.hubspot.net/hub/327485/hub_generated/style_manager/1431479563436/custom/page/Designers_2015/designer-doc-2105.min.html ``` ### include_css This function has been replaced by [**require_css()**](/reference/cms/hubl/functions#require-css). ```hubl {{ include_css("custom/page/Designers_2015/designers-doc-2015.css") }} ``` ```html ``` ### include_javascript This function has been replaced by [**require_js()**](/reference/cms/hubl/functions#require-js). ```hubl {{ include_javascript("custom/page/Designers_2015/designer-doc-2105.js") }} ``` ```html ``` ### page_by_id This function has been replaced by [**content_by_id()**](/reference/cms/hubl/functions#content-by-id). ```hubl {% set my_page = page_by_id(4715624297) %} ``` ```html ``` # HubL filters @ HubL filter list and examples for HubSpot CMS developers. $ reference & cms/hubl --- # HubL filters Filters affect the ultimate output of your HubL. They can be applied to various HubL statements and expressions to alter the template markup outputted by the server. The basic syntax of a filter is `|filtername`. The filter is added directly following the statement or the expression, within its delimiters. Some filters have additional parameters that can be added in parentheses. The basic syntax of a filter with a string, a number, and a boolean parameters is: `|filtername("stringParameter", 10, true)`. Notice that string parameters should be written in quotes. Also note that HubL filters have an alias that can be used to serve the same purpose as the primary filter. The following article contains all of the supported HubL filters. You can apply HubL filters to [personalization tokens](https://knowledge.hubspot.com/website-pages/personalize-your-content), such as contact and company tokens, on HubSpot CMS and blog pages, but not in emails. ## abs Gets the absolute value of a number. You can use this filter to ensure that a number is positive. ```hubl {% set my_number = -53 %} {{ my_number|abs }} ``` ```html 53 ``` ## add Adds a numeric value to another numeric value. This filter functions the same as the [\+ operator](/reference/cms/hubl/operators-and-expression-tests#math). The parameter in parentheses is the addend that you are combining with your initial numeric value. ```hubl {% set my_num = 40 %} {{ my_num|add(13) }} ``` ```html 53 ``` ## attr Renders the attribute of a dictionary. This filter is the equivalent of printing a variable that exists within a dictionary, such as `content.absolute_url`. ```hubl {{ content|attr("absolute_url") }} ``` ```html https://developers.hubspot.com/docs/cms/hubl/filters ``` | Parameter | Description | | --- | --- | | `attribute_name` | Specifies which attribute to print. | ## batch Groups items within a sequence. In the example below, there is a variable containing a sequence of types of fruits. The `batch` filter is applied to a loop that iterates through the sequence. The nested loop runs three times to print 3 types of fruit per row, before the outer loop runs again. Notice in the final output that since there are only 5 types of fruit, the final item is replaced by a ` ` (the second parameter). ```hubl {% set rows = ["apples", "oranges", "pears", "grapes", "blueberries"] %} {% for row in rows|batch(3, " ") %} {% for column in row %} {% endfor %} {% endfor %}
{{ column }}
``` ```html
apples oranges pears
grapes blueberries  
``` | Parameter | Type | Description | | --- | --- | --- | | `linecount` | Number | The number of items to include in the batch. | | `fill_with` | String | Specifies what to include in order to fill up any missing items. | ## between_times Calculates the time between two datetime objects in a specified time unit. You should use this filter only with variables that return a date. Starting September 30, 2024, this filter will no longer return the current date when a null value is passed. After that date, a null value in the filter will return September 30, 2024. ```hubl {% set begin = "2018-07-14T14:31:30+0530"|strtotime("yyyy-MM-dd'T'HH:mm:ssZ") %} {% set end = "2018-07-20T14:31:30+0530"|strtotime("yyyy-MM-dd'T'HH:mm:ssZ") %} {{ begin|between_times(end, "days") }} ``` ```html 6 ``` | Parameter | Type | Description | | --- | --- | --- | | `end` | datetime object | The ending datetime object. | | `timeunit` | String | Valid time units are `nanos` , `micros` , `millis` , `seconds` , `minutes` , `hours` , `half_days` , `days` , `weeks` , `months` , `years` , `decades` , `centuries` , `millennia` , and `eras` . | ## bool Converts a text string value to a boolean. ```hubl {% if "true"|bool == true %}hello world{% endif %} ``` ```html hello world ``` ## capitalize Capitalizes the first letter of a variable value. The first character will be uppercase, all others letters will be lowercased. Subsequent words separated by spaces or hyphens will not have their first letter uppercased. ```hubl {% set sentence = "the first letter of a sentence should always be capitalized." %} {{ sentence|capitalize }} ``` ```html The first letter of a sentence should always be capitalized. ``` ## center Centers text within a given field length using whitespace. This filter is not recommended or particularly useful since HubSpot's HTML compiler will automatically strip out the white space; however, it is included here for the sake of comprehensiveness. The example below shows this filter being applied to a variable in a pre tag, so the whitespace isn't stripped out. ```hubl
{% set var = "string to center" %}
before{{ var|center(80) }}after
``` ```html
before                                string to center                                after
``` | Parameter | Type | Description | | --- | --- | --- | | `width` | Number | Specifies the length of whitespace to center the text in. | ## convert_rgb Converts a HEX value to an RGB string. This is useful if you need to convert color variables to RGB to be used with a RGBA CSS declaration. In the example below, the value set by a color module is converted to an RGB value and used in an RGBA CSS declaration. ```hubl {% set my_color = "#FFFFFF" %} {{ my_color|convert_rgb }} {% set my_color2="#000000" %} ``` ```html 255, 255, 255 ``` ## cut Removes a string from a value. This filter can be used to match and cut out a specific part of a string. The parameter specifies the part of the string that should be removed. The example below removes the space and the word world from the original variable value. ```hubl {% set my_string = "Hello world." %} {{ my_string|cut(" world") }} ``` ```html Hello. ``` | Parameter | Type | Description | | --- | --- | --- | | `characters_to_cut` | String | The part of the string that should be removed. | ## datetimeformat (deprecated) This filter has been [deprecated](/reference/cms/hubl/deprecated#datetimeformat). Instead, use the [format_datetime](#format_datetime) filter, which has a more standardized syntax. ## default If the value is undefined it will return the first parameter, otherwise the value of the variable will be printed. If you want to use default with variables that evaluate to false, you have to set the second parameter to `true`. The first example below would print the message if the variable is not defined. The second example applies the filter to an empty string, which is not undefined, but it prints a message due to the second parameter. ```hubl {{ my_variable|default("my_variable is not defined") }} {{ ""|default("the string was empty", true) }} ``` ```html my_variable is not defined the string was empty ``` | Parameter | Type | Description | | --- | --- | --- | | `default_value` | String | The value to return if the variable is undefined. If the variable is defined, the value of the variable will be returned instead. | | `truthy` | Boolean | Set to `true` to use with variables which evaluate to `false`. | ## dictsort Sort a dict and yield (key, value) pairs. Dictionaries are unsorted by default, but you can print a dictionary, sorted by key or value. The first parameter is a boolean to determine, whether or not the sorting is case sensitive. The second parameter determines whether to sort the dict by key or value. The example below prints a sorted contact dictionary, with all the known details about the contact. ```hubl {% for item in contact|dictsort(false, "value") %} {{item}} {% endfor %} ``` ```html A sorted contact dictionary ``` | Parameter | Type | Description | | --- | --- | --- | | `case_sensitive` | Boolean | Determines if sorting is case sensitive. | | `sort_by` | `"key"` | `"value"` | Determines whether to sort by `key` or `value`. | ## difference Returns the difference of two sets or lists. The list returned from the filter contains all unique elements that are in the first list but not the second. ```hubl {{ [1, 2, 3]|difference([2, 3, 4, 5]) }} ``` ```html [1] ``` | Parameter | Type | Description | | --- | --- | --- | | `list` | Array | The second list to compare to for use in finding differences from the original list. | ## divide Divides the current value by a divisor. The parameter passed is the divisor. This filter is an alternative to the / operator. ```hubl {% set numerator = 106 %} {{ numerator|divide(2) }} ``` ```html 53 ``` | Parameter | Type | Description | | --- | --- | --- | | `divisor` | Number | The number to divide the variable by. | ## divisible An alternative to the `divisibleby` expression test, this filter will evaluate to true if the value is divisible by the given number. ```hubl {% set num = 10 %} {% if num|divisible(2) %} The number is divisible by 2 {% endif %} ``` ```html The number is divisible by 2 ``` | Parameter | Type | Description | | --- | --- | --- | | `divisor` | Number | The number to use when evaluating if the value is divisible. | ## escape_html Escapes the content of an HTML input. Accepts a string and converts the characters `&`, `<`, `>`, `‘`, `”`, and `escape_jinjava` into HTML-safe sequences. Use this filter for HubL variables that are used in HTML but should not allow any HTML. ```hubl " %} {{ escape_string|escape_html }} ``` ```html ``` ## escape_attr Escapes the content of an HTML attribute input. Accepts a string and converts the characters `&`, `<`, `‘`, `”`, and `escape_jinjava` into HTML-safe sequences. Use this filter for HubL variables that are being added to HTML attributes. Note that when escaping values of attributes that accept URLs, such as `href`, you should use the `escape_url` filter instead. ```hubl {% set escape_string = "This
markup is printed as text" %} {{escape_string|escape_attr}} ``` ```html This <br> markup is printed as text ``` ## escape_jinjava Converts the characters `{` and `}` in strings to Jinjava-safe sequences. Use this filter if you need to display text that might contain such characters in Jinjava. ```hubl {% set escape_string = "{{This markup is printed as text}}" %} {{ escape_string|escape_jinjava }} ``` ```html {{This markup is printed as text}} ``` ## escape_js Escapes strings, including `escape_jinjava`, so that they can be safely inserted into a JavaScript variable declaration. Use this filter for HubL variables that are used inside HTML script elements. ```hubl {% set escape_string = "\tThey said 'This string can safely be inserted into JavaScript.'" %} {{ escape_string|escape_js }} ``` ```html \tThey said \x27This string can safely be inserted into JavaScript.\x27 ``` ## escape_url Escapes the content of a URL input, enforcing specified protocols, stripping invalid and dangerous characters, and encoding HTML entities. Returns empty if a URL is valid. Use this filter for HubL variables that are used within HTML attributes that should be valid URLs. ```hubl {% set escape_string = "http://example.com/with space/" %} ``` ```html ``` ## escapejson Escapes strings so that they can be used as JSON values. ```hubl {% set escape_string = "" %} {% require_js position="head" %} {% end_require_js %} ``` ```html ``` ## filesizeformat Formats a number value into a human-readable file size (i.e. 13 kB, 4.1 MB, 102 Bytes, etc). By default, decimal prefixes are used (e.g., MB and GB), but you can set the `binary` parameter to `true` to use binary prefixes such as Mebi (MiB) and Gibi (GiB). ```hubl {% set bytes = 10000 %} {{ bytes|filesizeformat(binary=true) }} ``` ```html 9.8 KiB ``` | Parameter | Type | Description | | --- | --- | --- | | `binary` | Boolean | If set to `true`, binary prefixes are used, such as Mebi (MiB) and Gibi (GiB). | ## first Returns the first item in a sequence. ```hubl {% set my_sequence = ["Item 1", "Item 2", "Item 3"] %} {{ my_sequence|first }} ``` ```html Item 1 ``` ## float Converts a value into a floating point number. If the conversion doesn’t work it will return `0.0`. You can override this default using the first parameter. ```hubl {% set my_text="25" %} {{ my_text|float + 17 }} ``` ```html 42.0 ``` | Parameter | Type| Description | | --- | --- | ---| | `default` | Number | Integer to return if the conversion doesn't work. | ## forceescape Strictly enforces HTML escaping. In HubSpot's environment there isn't really a use case for double escaping, so this is generally behaves the same as the escape filter. ```hubl " %} {{ escape_string|forceescape }} ``` ```html ``` ## format Applies Python string formatting to an object. `%s` can be replaced with another variable. ```hubl {{ "Hi %s %s"|format(contact.firstname, contact.lastname) }} ``` ```html Hi Monty Python ``` ## format_currency (deprecated) This filter has been [deprecated](/reference/cms/hubl/deprecated). Instead, use the [format_currency_value](#format_currency_value) filter. ## format_currency_value Formats a given number as a currency based on portal's default currency and locale passed in as a parameter. Replaces the deprecated [format_currency filter](/reference/cms/hubl/deprecated#format_currency). ```hubl {{ 100 | format_currency_value(locale='en-GB', currency='EUR', maxDecimalDigits=6, minDecimalDigits=1) }} ``` ```html €100.0 ``` | Parameter | Type | Description | | --- | --- | --- | | `locale` | String | [The Java local Language tag](https://www.oracle.com/java/technologies/javase/jdk8-jre8-suported-locales.html). The default is the page's `locale.Format : ISO639LanguageCodeInLowercase-ISO3166CountryCodeInUppercase`. | | `currency` | String | the [alphabetic ISO 4217 code](https://en.wikipedia.org/wiki/ISO_4217) of the currency, default is the portals default currency. Numeric codes are not accepted. | | `minDecimalDigits` | Number | The minimum number of decimal digits to include in the output. Defaults to the currency's default number of decimal digits. | | `maxDecimalDigits` | Number | The maximum number of decimal digits to include in the output. Defaults to the currency's default number of decimal digits. | ## format_date Formats the date component of a date object. You should use this filter only with variables that return a date. Starting September 30, 2024, this filter will no longer return the current date when a null value is passed. After that date, a null value in the filter will return September 30, 2024. ```hubl {{ content.publish_date | format_date('long') }} {{ content.publish_date | format_date('yyyy.MMMM.dd') }} {{ content.publish_date | format_date('medium', 'America/New_York', 'de-DE') }} ``` ```html November 28, 2022 02022.November.28 28.11.2022 ``` | Parameter | Type | Description | | --- | --- | --- | | `format` | `'short'` | `'medium'` | `'long'` | `'full'` | custom pattern | The format to use. Can be a custom pattern following [Unicode LDML](https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns). | | `timeZone` | String | The time zone of the output date in [IANA TZDB format](https://data.iana.org/time-zones/tzdb/). | | `locale` | String | The locale to use for locale-aware formats. See [list of supported locales](https://www.oracle.com/java/technologies/javase/java8locales.html). | ## format_datetime Formats both the date and time components of a date object. This filter replaces the deprecated [datetimeformat](/reference/cms/hubl/deprecated#datetimeformat-nbsp-) filter. By default, returns a datetime in the UTC-00:00 time zone. You should use this filter only with variables that return a date. Starting September 30, 2024, this filter will no longer return the current date when a null value is passed. After that date, a null value in the filter will return September 30, 2024. ```hubl {{ content.publish_date | format_datetime('medium', 'America/New_York', 'de-DE') }} ``` ```html 12/31/69 7:00 PM ``` | Parameter | Type | Description | | --- | --- | --- | | `format` | `'short'` | `'medium'` | `'long'` | `'full'` | custom pattern | The format to use. Can be one a custom pattern following [Unicode LDML](https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns). When using `long` or `full`, timestamp will include a Z to denote zero offset UTC time (i.e., `2:23:00 PM Z`). To remove the Z designator, specify a `timeZone`. | | `timeZone` | String | The time zone of the output date in [IANA TZDB format](https://data.iana.org/time-zones/tzdb/). By default, returns UTC time. | | `locale` | String | The locale to use for locale-aware formats. See [list of supported locales](https://www.oracle.com/java/technologies/javase/java8locales.html). | ## format_number Formats a given number based on a specified locale. Includes a second parameter that sets the maximum decimal precision. ```hubl {{ 1000|format_number('en-US') }} {{ 1000.333|format_number('fr') }} {{ 1000.333|format_number('en-US', 2) }} ``` ```hubl 1,000 1 000,333 1,000.33 ``` | Parameter | Type | Description | | --- | --- | --- | | `locale` | String | The locale to use for formatting. See [list of supported locales](https://www.oracle.com/java/technologies/javase/java8locales.html). | | `maxDecimalDigits` | Number | The maximum number of decimal digits to include in the output. By default, will use the number of decimal digits from the input value. | ## format_time Formats the time component of a date object. You should use this filter only with variables that return a date. Starting September 30, 2024, this filter will no longer return the current date when a null value is passed. After that date, a null value in the filter will return September 30, 2024. ```hubl {{ content.updated | format_time('long') }} {{ content.updated | format_time('hh:mm a') }} {{ content.updated | format_time('medium', 'America/New_York', 'de-DE') }} ``` ```html 3:25:06 PM Z 03:25 PM 10:25:44 ``` | Parameter | Type | Description | | --- | --- | --- | | `format` | `'short'` | `'medium'` | `'long'` | `'full'` | custom pattern | The format to use. Can be one a custom pattern following [Unicode LDML](https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns). When using `long` or `full`, timestamp will include a Z to denote zero offset UTC time (i.e., `2:23:00 PM Z`). To remove the Z designator, specify a `timeZone`. | | `timeZone` | String | The time zone of the output date in [IANA TZDB format](https://data.iana.org/time-zones/tzdb/). By default, returns UTC time. | | `locale` | String | The locale to use for locale-aware formats. See [list of supported locales](https://www.oracle.com/java/technologies/javase/java8locales.html). | ## fromjson Converts a JSON string to an object. ```hubl {% set obj ='{ "name":"Brian","role":"Owner" }' %} {{ obj|fromjson }} ``` ```html {role=Owner, name=Brian} ``` ## geo_distance Calculates the ellipsoidal 2D distance between two points on Earth. ```hubl {{ row.location | geo_distance(37.3435, -122.0344, "mi") }} MI ``` ```html 861.1655563461395 MI ``` ## groupby Groups a sequence of objects by a common attribute. The parameter sets the common attribute to group by. ```hubl ``` ```html ``` | Parameter | Description | | ------------------------------------------- | -------------------------- | | `attribute` | The attribute to group by. | ## indent Indents text within a given field length using whitespace. This filter is not recommended or particularly useful because HubSpot's HTML compiler will automatically strip out the white space. However, it is included here for the sake of comprehensiveness. The example below shows an `indent` filter being applied to a variable in a `
` tag, so the whitespace isn't stripped out. The first parameter controls the amount of whitespace and the second boolean toggles whether to indent the first line.
```hubl
{% set var = "string to indent" %}
{{ var|indent(2, true) }}
``` ```html string to indent ``` | Parameter | Type | Description | | --- | --- | --- | | `width` | Number | The amount of whitespace to be applied. | | `indent-first` | Boolean | When set to `true`, the first line will be indented. | ## int Converts the value into an integer. If the conversion doesn’t work it will return `0`. You can override this default using the first parameter. ```hubl {% set string="25" %} {{ string|int + 17 }} ``` ```html 42 ``` | Parameter | Type | Description | | --- | --- | --- | | `default` | Number | Integer to return if the conversion doesn't work. | ## intersect Returns the intersection of two sets or lists. The list returned from the filter contains all unique elements that are contained in both lists. ```hubl {{ [1, 2, 3]|intersect([2, 3, 4, 5]) }} ``` ```html [2, 3] ``` | Parameter | Type | Description | | --- | --- | --- | | `list` | Array | The second list to compare to for use in finding where the list intersects with the original list. | ## ipaddr Evaluates to `true` if the value is a valid IPv4 or IPv6 address. ```hubl {% set ip = "1.0.0.1" %} {% if ip|ipaddr %} The string is a valid IP address {% endif %} ``` ```html The string is a valid IP address ``` ## join Returns a string which is the concatenation of the strings in the sequence. The separator between elements is an empty string per default, you can define it with the optional parameter. The second parameter can be used to specify an attribute to join. ```hubl {% set my_list = [1, 2, 3] %} {% set sep = "---" %} {{ my_list|join }} {{ my_list|join("|") }} {{ my_list|join(sep) }} ``` ```html 123 1|2|3 1---2---3 ``` | Parameter | Type | Description | | --- | --- | --- | | `delimiter` | String | The delimiter to use when concatenating strings. | | `attribute` | HubL Variable | Attribute of value to join in an object. | ## last Returns the last item of a sequence. ```hubl {% set my_sequence = ["Item 1", "Item 2", "Item 3"] %} {% my_sequence|last %} ``` ```html Item 3 ``` ## length Returns the number of items of a sequence or mapping. ```hubl {% set services = ["Web design", "SEO", "Inbound Marketing", "PPC"] %} {{ services|length }} ``` ```html 4 ``` ## list Converts values into a list. Strings will be returned as separate characters unless contained in square bracket sequence delimiters `[ ]`. ```hubl {% set one = 1 %} {% set two = 2 %} {% set three = "three" %} {% set four = ["four"] %} {% set list_num = one|list + two|list + three|list + four|list %} {{ list_num }} ``` ```html [1, 2, t, h, r, e, e, four] ``` ## log Calculates the natural logarithm of a number. ```hubl {{ 10|log }} {{ 65536|log(2) }} ``` ```html 2.302585092994046 16.0 ``` | Parameter | Type | Description | | --------- | ------ | ---------------------------------------- | | `base` | Number | The base to use for the log calculation. | ## lower Converts all letters in a value to lowercase. ```hubl {% set text="Text to MAKE LowercaSe" %} {{ text|lower }} ``` ```html text to make lowercase ``` ## map Applies a filter on a sequence of objects or looks up an attribute. This is useful when dealing with a list of objects where you're only interested in a certain value of it. The basic usage is mapping on an attribute. For example, if you want to use conditional logic to check if a value is present in a particular attribute of a dict. Alternatively, you can let it invoke a filter by passing the name of the filter and the arguments afterwards. ```hubl {# Usage 1 #} Apply a filter to a sequence: {% set seq = ["item1", "item2", "item3"] %} {{ seq|map("upper") }} {# Usage 2 #} Look up an attribute: {{ content|map("currentState")}} ``` ```hubl Apply a filter to a sequence: [ITEM1, ITEM2, ITEM3] Look up an attribute: [DRAFT] ``` | Parameter | Type | Description | | --------- | ------ | ------------------------------------------- | | `filter` | String | Filter to apply to the sequence of objects. | ## md5 Calculates the [md5 hash](https://en.wikipedia.org/wiki/MD5) of the given object. ```hubl {{ content.absolute_url|md5 }} ``` ```html 923adb4ce05a4c6342c04c80be88d15e ``` ## minus_time Subtracts an amount of time from a datetime object. ```hubl {% set date = "2018-07-14T14:31:30+0530"|strtotime("yyyy-MM-dd'T'HH:mm:ssZ") %} {{ date }} {{ date|minus_time(2, "months") }} ``` ```html 2018-07-14 14:31:30 2018-05-14 14:31:30 ``` | Parameter | Type | Description | | --- | --- | --- | | `diff` | Number | Amount to subtract. | | `timeunit` | String | Valid time units are `nanos` , `micros` , `millis` , `seconds` , `minutes` , `hours` , `half_days` , `days` , `weeks` , `months` , `years` , `decades` , `centuries` , `millennia` , and `eras` . | ## multiply Multiplies a value with a number. Functions the same as the [\* operator](/reference/cms/hubl/operators-and-expression-tests). ```hubl {% set n = 20 %} {{ n|multiply(3) }} ``` ```html 60 ``` ## plus_time Adds an amount of time to a datetime object. ```hubl {% set date = "2018-07-14T14:31:30+0530"|strtotime("yyyy-MM-dd'T'HH:mm:ssZ") %} {{ date }} {{ date|plus_time(5, "days") }} ``` ```html 2018-07-14 14:31:30 2018-07-19 14:31:30 ``` | Parameter | Type | Description | | --- | --- | --- | | `diff` | Number | Amount to subtract. | | `timeunit` | String | Valid time units are `nanos` , `micros` , `millis` , `seconds` , `minutes` , `hours` , `half_days` , `days` , `weeks` , `months` , `years` , `decades` , `centuries` , `millennia` , and `eras` . | ## pprint Pretty print a variable. This prints the type of variable and other info that is useful for debugging. ```hubl {% set this_var ="Variable that I want to debug" %} {{ this_var|pprint }} ``` ```html (String: Variable that I want to debug) ``` ## random Return a random item from the sequence. When using this filter, the page will be [prerendered](/guides/cms/content/performance/prerendering) periodically rather than every time the page content is updated. This means that the filtered content will not be updated on every page reload. This may not be an issue for certain types of content, such as displaying a random list of blog posts. However, if you need content to change randomly on every page load, you should instead use JavaScript to randomize the content client-side. ```hubl {% for content in contents|random %} {% endfor %} ``` ```html ``` ## regex_replace Searches for a regex pattern and replaces with a sequence of characters. The first argument is a RE2-style regex pattern, the second is the replacement string. Learn more about [RE2 regex syntax](https://github.com/google/re2/wiki/Syntax). ```hubl {{ "contact-us-2"|regex_replace("[^a-zA-Z]", "") }} ``` ```html contactus ``` ## reject Filters a sequence of objects by applying an [expression test](/reference/cms/hubl/operators-and-expression-tests) to the object and rejecting the ones with the test succeeding. ```hubl {% set some_numbers = [10, 12, 13, 3, 5, 17, 22] %} {{ some_numbers|reject("even") }} ``` ```html [13, 3, 5, 17] ``` | Parameter | Type | Description | | --- | --- | --- | | `exp_text` | String | The name of the [expression test](/reference/cms/hubl/operators-and-expression-tests#expression-tests) to apply to the object. | ## rejectattr Filters a sequence of objects by applying a test to an attribute of an object and rejecting the ones with the test succeeding. ```hubl {% for content in contents|rejectattr("post_list_summary_featured_image") %} {% endif %} {{ content.post_list_content|safe }} {% endfor %} ``` ```html ``` | Parameter | Type | Description | | --- | --- | --- | | `attribute_name` | String | Specifies the attribute to select. You can access nested attributes using dot notation. | | `exp_test` | String | The name of the [expression test](/reference/cms/hubl/operators-and-expression-tests#expression-tests) to apply to the object. | ## render Renders strings containing HubL early so that the output can be passed into other filters. ```hubl {{ personalization_token("contact.lastname", "default value")|render|lower }} ``` ```html mclaren ``` ## replace Replaces all instances of a substring with a new one. ```hubl {% if topic %}

Posts about {{ page_meta.html_title|replace("Blog | ", "") }}

{% endif %} ``` ```html

Posts about topic name

``` | Parameter | Type | Description | | --- | --- | --- | | `old` | String | The substring that should be replaced. | | `new` | String | Replacement string. | | `count` | Number | If provided, only the firstcount occurrences are replaced. | ## reverse Reverses the object or return an iterator the iterates over it the other way round. To reverse a list use [.reverse()](/reference/cms/hubl/functions#reverse) ```hubl {% set nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] %} {% for num in nums|reverse %} {{ num }} {% endfor %} ``` ```html 10 9 8 7 6 5 4 3 2 1 ``` ## root Calculates the square root of a value. ```hubl {{ 16|root }} {{ 625|root(4) }} ``` ```html 4 5 ``` | Parameter | Type | Description | | ---------- | ------ | ---------------------------------------- | | `nth_root` | Number | The nth root to use for the calculation. | ## round Rounds a number to a given precision. ```hubl {{ 52.5|round }} {{ 52.5|round(0, "floor") }} ``` ```html 53 52 ``` | Parameter | Type | Description | | --- | --- | --- | | `precision` | Number | Specifies the precision of the rounding. | | `rounding_method` | `'common'` (default) | `'ceil'` | `'floor'` | Options include `common` round either up or down (default); `ceil` always rounds up; `floor` always rounds down. | ## safe Mark a value as safe which means that in an environment with automatic escaping enabled this variable will not be escaped. ```hubl {{ content.post_list_content|safe }} ``` ```html

HTML post content that is not escaped.

``` ## sanitize_html Sanitizes the content of an HTML input for the output of rich text content. Accepts a string, then strips HTML tags that are not allowed. Use this filter for HubL variables that are used in HTML that should allow safe HTML. You can include the following parameters to allow specific types of HTML tags: `FORMATTING`, `BLOCKS`, `STYLES`, `LINKS`, `TABLES`, `IMAGES`. For example, `sanitize_html(IMAGES)`. Using `sanitize_html` will include all parameters in the filter. You can also include a `STRIP` parameter to strip all HTML. All content is run through `escape_jinjava` as well to prevent nested interpretation. ```hubl " %} {{ escape_string|sanitize_html("IMAGES") }} ``` ```html This markup is printed as text. ``` ## select Filters a sequence of objects by applying a test to the objects and only selecting the ones with the test succeeding. ```hubl {% set some_numbers = [10, 12, 13, 3, 5, 17, 22] %} {{ some_numbers|select("even") }} ``` ```html [10, 12, 22] ``` | Parameter | Type | Description | | ---------- | ------ | ------------------------------------------- | | `exp_text` | String | The expression test to apply to the object. | ## selectattr Filters a sequence of objects by applying a test to an attribute of the objects and only selecting the ones with the test succeeding. ```hubl {% for content in contents|selectattr("post_list_summary_featured_image") %} {% endif %} {{ content.post_list_content|safe }} {% endfor %} ``` ```html Post with featured image ``` | Parameter | Type | Description | | --- | --- | --- | | `attribute_name` | String | The attribute to test for. You can access nested attributes using dot notation. | | `exp_test` | String | The name of the [expression test](/reference/cms/hubl/operators-and-expression-tests#expression-tests) to apply to the object. | | `val` | String | Value to test against. | ## shuffle Randomizes the order of iteration through a sequence. The example below shuffles a standard blog loop. When using this filter, the page will be [prerendered](/guides/cms/content/performance/prerendering) periodically rather than every time the page content is updated. This means that the filtered content will not be updated on every page reload. This may not be an issue for certain types of content, such as displaying a random list of blog posts. However, if you need content to change randomly on every page load, you should instead use JavaScript to randomize the content client-side. ```hubl {% for content in contents|shuffle %} {% endfor %} ``` ```html ``` ## slice Slices an iterator and returns a list of lists containing those items. The first parameter specifies how many items will be sliced, and the second parameter specifies characters to fill in empty slices. ```hubl {% set items = ["laptops", "tablets", "smartphones", "smart watches", "TVs"] %} ``` ```html ``` | Parameter | Type | Description | | --- | --- | --- | | `slices` | Number | How many items will be sliced. | | `filler` | String | Specifies characters to fill in empty slices. | ## sort Sorts an iterable. This filter requires all parameters to sort by an attribute in HubSpot. The first parameter is a boolean to reverse the sort order. The second parameter determines whether or not the sorting is case sensitive. And the final parameter specifies an attribute to sort by. In the example below, posts from a blog are rendered and alphabetized by name. ```hubl {% set my_posts = blog_recent_posts("default", limit=5) %} {% for item in my_posts|sort(False, False, "name") %} {{ item.name }}
{% endfor %} ``` ```html A post
B post
C post
D post
E post
``` | Parameter | Type | Description | | --- | --- | --- | | `reverse` | Boolean | Set to `true` to reverse the sort order. | | `case_sensitive` | Boolean | Set to `true` to make sorting case sensitive. | | `attribute` | String | Attribute to sort by. Omit when sorting a list. | ## split Splits the input string into a list on the given separator. The first parameter specifies the separator to split the variable by. The second parameter determines how many times the variable should be split. Any remaining items would remained group. In the example below, a string of names is split at the `;` for the first four names. ```hubl {% set string_to_split = "Mark; Irving; Helly; Dylan; Milchick; Harmony;" %} {% set names = string_to_split|split(";", 4) %} ``` ```html ``` | Parameter | Type | Description | | --- | --- | --- | | `character_to_split_by` | String | Specifies the separator to split the variable by. | | `number_of_splits` | Number | Determines how many times the variable should be split. Any remaining items would remain grouped. | ## string Converts a different variable type to a string. In the example below, a integer is converted into a string (`pprint` is used to confirm the change in variable type). ```hubl {% set number_to_string = 45 %} {{ number_to_string|string|pprint }} ``` ```html (String: 45) ``` ## striptags Strips SGML/XML tags and replace adjacent whitespace by one space. This filter can be used to remove any HTML tags from a variable. ```hubl " %} {{ some_html|striptags }} ``` ```html some text ``` ## strtodate Converts a date string and date format to a date object. ```hubl {{ '3/3/21'|strtodate('M/d/yy') }} ``` ```html 2021-03-03 00:00:00 ``` | Parameter | Type | Description | | --- | --- | --- | | `dateFormat` | String | The [date format](https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html) to use. | ## strtotime Converts a datetime string and a datetime format into a datetime object. ```hubl {{ "2018-07-14T14:31:30+0530"|strtotime("yyyy-MM-dd'T'HH:mm:ssZ")|unixtimestamp }} ``` ```html 1531558890000 ``` | Parameter | Type | Description | | --- | --- | --- | | `datetimeFormat` | String | The [Date and time format](https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html) to use. | ## sum Adds numeric values in a sequence. The first parameter can specify an optional attribute and the second parameter sets a value to return if there is nothing in the variable to sum. ```hubl // Simple sum {% set sum_this = [1, 2, 3, 4, 5] %} {{ sum_this|sum }} // Sum of attribute {% set items = [15, 10] %} {% set dict_var = [{"name": "Item1", "price": "20"}, {"name": "Item2", "price": "10"}] %} Total: {{ dict_var|sum(attribute="price") }} ``` ```html 15 Total: 30 ``` | Parameter | Type | Description | | --- | --- | --- | | `attribute` | String | Attribute to sum. | | `return_if_nothing` | String | Value to return if there is nothing in the variable to sum. | ## symmetric_difference Returns the symmetric difference of two sets or lists. The list returned from the filter contains all unique elements that are in the first list but not the second, or are in the second list but not the first. ```hubl {{ [1, 2, 3]|symmetric_difference([2, 3, 4, 5]) }} ``` ```html [1, 4, 5] ``` | Parameter | Type | Description | | --- | --- | --- | | `list` | Array | The second list to compare to for use in finding the symmetric difference with the original list. | ## title Returns a title case version of the value (i.e., words will start with uppercase letters but all remaining characters are lowercase). ```hubl {% set my_title="my title should be title case" %} {{ my_title|title }} ``` ```html My Title Should Be Title Case ``` ## tojson Writes an object as a JSON string. ```hubl {% for content in contents %} {{ content.blog_post_author|tojson }} {% endfor %} ``` ```json { "portalId": 1234567, "id": 12312253109, "created": 1566413741989, "updated": 1566414012799, "deletedAt": 0, "fullName": "Sample User", "email": "sampleUser@example.com", "userId": null, "username": null, "slug": "sample-user", "jsonBody": { "avatar": "https://app.hubspot.com/settings/avatar/109d6874a0cb066c1c7263ac5df6ce7a", "bio": "Sample Bio", "facebook": "", "linkedin": "", "twitter": "", "website": "https://www.hubspot.com" }, "bio": "Sample Bio", "facebook": "", "linkedin": "", "avatar": "https://app.hubspot.com/settings/avatar/109d6874a0cb066c1c7263ac5df6ce7a", "gravatarUrl": "https://app.hubspot.com/settings/avatar/108bb5ac667ded34796271437dfe8d58", "twitterUsername": "", "hasSocialProfiles": false, "website": "https://www.hubspot.com", "twitter": "", "displayName": "Sample User" } ``` ## trim Strips leading and trailing whitespace. HubSpot already trims whitespace from markup, but this filter is documented for the sake of comprehensiveness. ```hubl {{ " remove whitespace " }} {{ " remove whitespace "|trim }} ``` ```html remove whitespace remove whitespace ``` ## truncate Cuts off text after a certain number of characters. The default is 255. HTML characters are included in this count. Because this filter relies on the spaces between words to shorten strings, it may not work as expected for languages without spaces between characters, such as Japanese. ```hubl {{ "I only want to show the first sentence. Not the second."|truncate(40) }} {{ "I only want to show the first sentence. Not the second."|truncate(35, true, "..........") }} ``` ```html I only want to show the first sente.......... ``` | Parameter | Type | Description | | --- | --- | --- | | `number_of_characters` | Number | Number of characters to allow before truncating the text. Default is 255. | | `breakword` | Boolean | If `true`, the filter will cut the text at length. If `false`, it will discard the last word. | | `end` | String | Override the default '...' trailing characters after the truncation. | ## truncatehtml Truncates a given string, respecting HTML markup (i.e. will properly close all nested tags). This will prevent a tag from remaining open after truncation. HTML characters do not count towards the character total. Because this filter relies on the spaces between words to shorten strings, it may not work as expected for languages without spaces between characters, such as Japanese. ```hubl {% set html_text = "

I want to truncate this text without breaking my HTML

" %} {{ html_text|truncatehtml(28, "..." , false) }} ``` ```html

I want to truncate this..

``` | Parameter | Type | Description | | --- | --- | --- | | `number_of_characters` | Number | Number of characters to allow before truncating the text. Default is 255. | | `end` | String | Override the default '...' trailing characters after the truncation. | | `breakword` | Boolean | Boolean value. If `true`, the filter will cut the text at length. If `false` (default), it will discard the last word. If using only one of the optional parameters, use keyword arguments, such as `truncatehtml(70, breakwords = false)`. | ## unescape_html Converts text with HTML-encoded entities to their Unicode equivalents. ```hubl {% set escape_string = "me & you" %} {{ escape_string|unescape_html }} ``` ```html me & you ``` ## union Returns the union of two sets or lists. The list returned from the filter contains all unique elements that are in either list. ```hubl {{ [1, 2, 3]|union([2, 3, 4, 5]) }} ``` ```html [1, 2, 3, 4, 5] ``` | Parameter | Type | Description | | --- | --- | --- | | `list` | Array | The second list to union with the original list. | ## unique Extracts a unique set from a sequence or dict of objects. When filtering a dict, such as a list of posts returned by a function, you can specify which attribute is used to deduplicate items in the dict. ```hubl {% set my_sequence = ["one", "one", "two", "three" ] %} {{ my_sequence|unique }} ``` ```html [one, two, three] ``` | Parameter | Type | Description | | --- | --- | --- | | `attr` | String | Specifies the attribute that should be used when filtering a dict value. | ## unixtimestamp Converts a datetime object into a Unix timestamp. You should use this filter only with variables that return a date. Starting September 30, 2024, this filter will no longer return the current date when a null value is passed. After that date, a null value in the filter will return `September 30, 2024`. ```hubl {{ local_dt }} {{ local_dt|unixtimestamp }} ``` ```html 2017-01-30 17:11:44 1485814304000 ``` ## upper Converts all letters in a value to uppercase. ```hubl {% set text="text to make uppercase" %} {{ text|upper }} ``` ```html TEXT TO MAKE UPPERCASE ``` ## urlencode Escapes and URL encodes a string using UTF-8 formatting. Accepts both dictionaries and regular strings as well as pairwise iterables. ```hubl {% set encode_value="Escape & URL encode this string" %} {{ encode_value|urlencode }} ``` ```html Escape+%26+URL+encode+this+string ``` ## urldecode Decodes encoded URL strings back to the original URL. Accepts both dictionaries and regular strings as well as pairwise iterables. ```hubl {% set decode_value="Escape+%26+URL+decode+this+string" %} {{ decode_value|urldecode }} ``` ```html Escape & URL decode this string ``` ## urlize Converts URLs in plain text into clickable links. If you pass the filter an additional integer it will shorten the urls to that number. The second parameter is a boolean that dictates whether the link is rel="no follow". The final parameter lets you specify whether the link will open in a new tab. | Parameter | Type | Description | | --- | --- | --- | | `shorten_text` | Number | Integer that will shorten the urls to desired number. | | `no_follow` | Boolean | When set to `true`, the link will include `rel="no follow"`. | | `target="_blank"` | String | Specifies whether the link will open in a new tab. | ```hubl {{ "http://hubspot.com/"|urlize }} {{ "http://hubspot.com/"|urlize(10,true) }} {{ "http://hubspot.com/"|urlize("",true) }} {{ "http://hubspot.com/"|urlize("",false,target="_blank") }} ``` ```html http://hubspot.com/ http://... http://hubspot.com/ http://hubspot.com/ ``` ## wordcount Counts the number of words in a string. If the string contains HTML, use the `striptags` filter to get an accurate count. ```hubl {% set count_words = "Count the number of words in this variable" %} {{ count_words|wordcount }} ``` ```html 8 ``` ## wordwrap Causes words to wrap at a given character count. This works best in a `
` because HubSpot strips whitespace by default.
```hubl
{% set wrap_text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam efficitur, ipsum non sagittis euismod, ex risus rhoncus lectus, vel maximus leo enim sit amet dui. Ut laoreet ultricies quam at fermentum." %}
{{ wrap_text|wordwrap(10) }}
```
```html
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam efficitur, ipsum
non sagittis euismod, ex risus rhoncus lectus, vel maximus leo enim sit amet
dui. Ut laoreet ultricies quam at fermentum.
```
| Parameter | Description |
| --- | --- |
| `character_count` | Number of characters to wrap the content at. |

## xmlattr

Creates an HTML/XML attribute string, based on the items in a dict. All values that are neither none nor undefined are automatically escaped. It automatically prepends a space in front of the item if the filter returned something unless the first parameter is false.
```hubl
{% set html_attributes = {"class": "bold", "id": "sidebar"} %}

```
```html

```
| Parameter | Type | Description |
| --- | --- | --- |
| `autospace` | Boolean | Set to `true` to add a space in front of the item. |

# HubL functions
@ A reference listing of all of the available HubL functions.
$ reference
& cms/hubl
---


# HubL functions

Functions in HubL are similar to filters in that they accept parameters and generate a value. However, not all functions need to be applied to an initial template value, and instead they interact with other areas of your HubSpot environment.

If you maintain an older website, you may also want to check out the [list of deprecated HubL functions](/reference/cms/hubl/deprecated).

Below, learn more about each HubL function and its syntax.

## append

Adds a single item to the end of a list.
```hubl
{% set numbers_under_5 = [1,2,3] %}
{% do numbers_under_5.append(4) %}
{{numbers_under_5}}
```
```html
[1, 2, 3, 4]
```
| Parameter | Type | Description                      |
| --------- | ---- | -------------------------------- |
| `item`    | Any  | Item to be appended to the list. |

## blog_all_posts_url

The `blog_all_posts_url` function returns a full URL to the listing page for all blog posts for the specified blog.

The example below shows how this function can be used as an anchor's `href`.
```hubl
All Marketing blog posts
```
```html
All Marketing blog posts
```
| Parameter | Type | Description |
| --- | --- | --- |
| `selected_blog` | Blog id or "default" | Specifies which blog to use. The blog id is returned by the [module blog field](/reference/cms/fields/module-theme-fields#blog). |

## blog_author_url

The `blog_author_url` function returns a full URL to the specified blog author's listing page.

The example below shows how this function can be used as an anchor's `href`. This can be combined with `blog_authors` as shown in that function's examples.
```hubl
Brian Halligan
```
```html
Brian Halligan
```
| Parameter | Type | Description |
| --- | --- | --- |
| `selected_blog` | Blog ID or "default" | Specifies which blog the author's listing page exists in. You can specify a blog by ID or use `"default"` to select the default blog. The blog ID is returned by the [module blog field](/reference/cms/fields/module-theme-fields#blog). |
| `author_slug` | String or HubL Var | Specifies which author to link to. Can use either `content.blog_post_author.slug` or a lowercase hyphenated name. Example: `"jane-doe"`. |

## blog_authors

The `blog_authors` function returns a sequence of blog author objects for the specified blog, sorted by slug ascending. This sequence can be stored in a variable and iterated through to create custom author post filters.

The number of live posts by each author can be accessed with `author.live_posts`.
This function has a limit of 250 authors. this function also has a limit of 10 calls per page and per email.
The first line of the example below shows how the function returns a sequence of author objects. The rest of the example shows a use case of saving a sequence into a variable and then iterating though the author objects, printing a set of author listing links. The example assumes that the blog has 4 authors.
```hubl
{{ blog_authors("default", 250) }}

{% set my_authors = blog_authors("default", 250) %}

```
```html
[ Brian Halligan, Dharmesh Shah, Katie Burke, Kipp Bodnar]


```
| Parameter | Type | Description |
| --- | --- | --- |
| `selected_blog` | Blog ID or "default" | Specifies which blog to use, either a specific blog by its ID or the default blog by `"default"`. The blog ID is returned by the [module blog field](/reference/cms/fields/module-theme-fields#blog). |
| `limit` | Integer | Sets the limit on the number of authors fetched. |

## blog_by_id

The `blog_by_id` function returns a blog by ID. The below example code shows this function in use to generate a hyperlinked list item.
This function has a limit of 10 calls per page and per email.
```hubl
{% set my_blog = blog_by_id(47104297) %}

```
```html

```
| Parameter | Type | Description |
| --- | --- | --- |
| `selected_blog` | Blog ID or "default" | Specifies which blog to use, either a specific blog by its ID or the default blog by `"default"`. The blog ID is returned by the [module blog field](/reference/cms/fields/module-theme-fields#blog). |

## blog_page_link

The `blog_page_link` function generates the URL of a paginated view of your blog listing. The function takes a numeric parameter, which allows you to generate links for current, next, previous, or a specific page. This function is generally used in the `href` attribute of pagination anchor tags and must be used on your blog listing template.

The examples below show this function in use as an anchor `href`. The first example outputs the current page. The second example takes the number 7 parameter to specify the seventh page. The third example uses the `next_page_num` variable to generate a link that is relative to the current page number (you can also use the `last_page_num` variable for the previous page). The final example uses the `current_page_num` variable and a `+` operator to create a link that is 4 greater than the current page.
```hubl
Current page
Page 7
Next
Page Plus 4
```
```html
Page 1
Page 7
Next
Page Plus 4
```
| Parameter | Type | Description |
| --- | --- | --- |
| `page` | Number or HubL variable | Page number used to generate URL or HubL variable for page number. |

## blog_popular_posts

This function renders a set number of popular posts into a sequence. The sequence can then be saved into a variable and iterated through with a for loop, creating a custom post listing of your most popular posts.

Results from this function are cached for six hours. To retrieve blog posts using HubL in a way that avoids caching, you may want to use [blog_recent_tag_posts](#blog-recent-tag-posts) instead.

In the example code below, the first line shows how the function returns a sequence. The sequence is saved as a variable which is then used in a [for loop](/reference/cms/hubl/loops). [Any blog post variables](/reference/cms/hubl/variables#blog-variables) should use the name of the individual loop item rather than `content.`. In the example, `pop_post.name` is used. This technique can be used on blog templates and website pages.
This function has a limit of 200 posts. This function also has a limit of 10 calls per page and per email.
```hubl
{% set pop_posts = blog_popular_posts("default", 5, ["marketing-tips", "sales-tips"], "popular_past_month", "AND") %}
{% for pop_post in pop_posts %}

{% endfor %}
```
```html
[Popular post title 1, Popular post title 2, Popular post title 3, Popular post
title 4, Popular post title 5]
```
| Parameter | Type | Description |
| --- | --- | --- |
| `selected_blog` | Blog ID or "default" | Specifies which blog to use, either a specific blog by its ID or the default blog by `"default"`. |
| `limit` | Integer | Specifies the number of posts to add to the sequence up to a limit of 200. If not specified, defaults to 10. |
| `tag_slug` | Array | Optional list of tags to filter posts by. |
| `time_frame` | String | Optional timeframe of analytics to filter posts by. Default is `"popular_past_year"`. Must be one of:This parameter is required when including the `logical_operator` parameter. |
| `logical_operator` | String | When `tag_slug` contains multiple tags, use this operator to filter results with `AND` or `OR` logic. By default, this function uses `OR` logic to return posts tagged with any of the specified tags.When including this parameter, `time_frame` is required. |

## blog_post_archive_url

The `blog_post_archive_url` function returns a full URL to the archive listing page for the given date values on the specified blog. This function has two required parameters and two optional parameters. The first parameter is a blog ID or simply the keyword `"default"`. The second is the year of archived posts you'd like to display.

The optional parameters include the month and day of archived posts you'd like to display, respectively.

The example below shows how This function can be used as an anchor's `href`.
This function has a limit of 200 posts. This function also has a limit of 10 calls per page and per email.
```hubl
Posts from July 5th, 2017
```
```html
Posts from July 5th, 2017
```
| Parameter | Type | Description |
| --- | --- | --- |
| `selected_blog` | Blog ID or "default" | Specifies which blog to use, either a specific blog by its ID or the default blog by `"default"`. |
| `year` | Integer | The year. |
| `month` | Integer | The optional month. |
| `day` | Integer | The optional day. |

## blog_recent_author_posts

The `blog_recent_author_posts` function returns a sequence of blog post objects for the specified author, sorted by most recent. This sequence of posts can be saved into a variable and iterated through with a for loop, creating a custom post listing of posts by a particular author.

The function takes three parameters. The first parameter specifies which blog to collect posts by an author from. The value should be `"default"` or the blog ID of a particular blog (available in the URL of the Blog dashboard). The second parameter specifies which author to use. This parameter can use the **`content.blog_post_author.slug`** to use the author of the current post or accepts a lowercase hyphenated name such as `"brian-halligan"`. The third parameter specifies how many posts are retrieved.

The first line of the example below demonstrates how the function returns a sequence of posts by an author. In this example, rather than specifying an exact author name, the current post author is used. The sequence is saved in a variable and looped through. [Any blog post variables](/reference/cms/hubl/variables#blog-variables) should use the name of the individual loop item rather than `content.`. In the example, `author_post.name` is used. This technique can be used on blog and page templates.
This function has a limit of 200 posts and 10 calls per page and per email.
```hubl
{{ blog_recent_author_posts("default", content.blog_post_author.slug, 5 ) }}
{% set author_posts = blog_recent_author_posts("default", content.blog_post_author.slug, 5) %}
{% for author_post in author_posts %}

{% endfor %}
```
```html
[Post by author title 1, Post by author title 2, Post by author title 3, Post by
author title 4, Post by author title 5]
```
| Parameter | Type | Description |
| --- | --- | --- |
| `selected_blog` | Blog ID or "default" | Specifies which blog to use. The blog ID is returned by the [module blog field](/reference/cms/fields/module-theme-fields#blog). |
| `author_slug` | String | Specifies which author to filter on. |
| `limit` | Integer | Specifies the number of posts to add to the sequence up to a limit of 200. |

## blog_recent_posts

The `blog_recent_posts` function returns a sequence of blog post objects for the specified blog, sorted by most recent first. This sequence of posts can be saved into a variable and iterated through with a for loop, creating a custom post listing of your most popular posts.

The function takes two parameters. The first parameter specifies which blog to collect popular posts from. The value should be `"default"` or the blog ID of a particular blog (available in the URL of the Blog dashboard). The second parameter specifies how many posts are retrieved.

The first line of the example below demonstrates how the function returns a sequence. The sequence is saved in a variable and looped through. [Any blog post variables](/reference/cms/hubl/variables#blog-variables) should use the name of the individual loop item rather than `content.`. In the example, `rec_post.name` is used. This technique can be used, not only on blog templates, but also regular pages.
This function has a limit of 200 posts and 10 calls per page and per email.
```hubl
{{ blog_recent_posts("default", 5) }}
{% set rec_posts = blog_recent_posts("default", 5) %}
{% for rec_post in rec_posts %}

{% endfor %}
```
```html
[Recent post title 1, Recent post title 2, Recent post title 3, Recent post
title 4, Recent post title 5]
```
| Parameter | Type | Description |
| --- | --- | --- |
| `selected_blog` | Blog ID or "default" | Specifies which blog to use, either a specific blog by its ID or the default blog by `"default"`. The blog ID is returned by the [module blog field](/reference/cms/fields/module-theme-fields#blog). |
| `limit` | Integer | Specifies the number of posts to add to the sequence, maximum 200. |

## blog_recent_tag_posts

The `blog_recent_tag_posts` function returns a sequence of blog post objects for a specified tag or tags, sorted by most recent first. This sequence of posts can be saved into a variable and iterated through with a for loop, creating a custom post listing of posts by a particular tag or tags.

In the example code below:

- The first line shows how the function returns a sequence of posts by tag.
- The second line shows how to save the function in a sequence variable. The rest of the code then uses a for loop to cycle through the variable values. [Any blog post variables](/reference/cms/hubl/variables#blog-variables) should use the name of the individual loop item rather than `content.`. In the example, `tag_post.name` is used. You can use this technique on both blog and website pages.

Learn more about [creating a related blog post listing](/reference/cms/hubl/tags/related-blog-posts).
This function has a limit of 100 posts and 10 calls per page and per email.
```hubl
{{ blog_recent_tag_posts("default", "marketing-tips", 5) }}
{% set tag_posts = blog_recent_tag_posts("default", ["marketing", "fun", "inbound"], 3, "AND") %}
{% for tag_post in tag_posts %}

{% endfor %}
```
```html
[Post about Marketing 1, Post about Marketing 2, Post about Marketing 3, Post
about Marketing 4, Post about Marketing 5]
```
| Parameter | Type | Description |
| --- | --- | --- |
| `selected_blog` | Blog ID or "default" | Specifies which blog to use, either a specific blog by its ID or the default blog by `"default"`. The blog ID is returned by the [module blog field](/reference/cms/fields/module-theme-fields#blog). |
| `tag_slug` | String | Specifies which tag to filter on. You can include up to 10 tags, comma separated. Tags with multiple words must be lowercase with spaces replaced by hyphens. |
| `limit` | Integer | Specifies the number of posts to add to the sequence. This parameter is required when including a `logical_operator`. |
| `logical_operator` | String | When `tag_slug` contains multiple tags, use this operator to filter results with `AND` or `OR` logic. By default, this function uses `OR` logic to return posts tagged with any of the specified tags.When including this parameter, `limit` is required. |

## blog_tag_url

The `blog_tag_url` function returns a full URL to the specified blog tag's listing page.

This function accepts two parameters. The first parameter specifies which blog the tag's listing page exists in. The second parameter specifies which tag to link. This parameter can use the **`topic.slug`** for a particular tag from `content.topic_list` or accepts a lowercase hyphenated name such as `"marketing-tips"`.

The example below shows how this function can be used as an anchor's href.
```hubl
Inbound Marketing
```
```html
Inbound Marketing
```
| Parameter | Type | Description |
| --- | --- | --- |
| `selected_blog` | Blog ID or "default" | Specifies which blog to use, either a specific blog by its ID or the default blog by `"default"`. |
| `tag_slug` | String | Specifies which tag to link to. |

## blog_tags

The `blog_tags` function returns a sequence of the 250 most blogged-about tags (based on number of associated blog posts) for the specified blog, sorted by blog post count.This sequence can be stored in a variable and iterated through to create custom tag post filters. The number of posts for each tag can be accessed with `tag.live_posts`.

This function accepts two parameters. The first parameter specifies which blog to fetch tags from. The second parameter sets a limit on the number of tags fetched.

The first line of the example below demonstrates how the function returns a sequence of tag objects. The rest of the example demonstrates a use case of saving a sequence into a variable and then iterating though the tag objects, printing a set of tag links. The example assumes that the blog has 4 tags.
This function has a limit of 250 tags.
```hubl
{{ blog_tags("default", 250) }}

{% set my_tags = blog_tags("default", 250) %}

```
```html
[ Blogging, Inbound Marketing, SEO, Social Media]


```
| Parameter | Type | Description |
| --- | --- | --- |
| `selected_blog` | Blog ID or "default" | Specifies which blog to use. The blog ID is returned by the [module blog field](/reference/cms/fields/module-theme-fields#blog). |
| `limit` | Integer | The maximum amount of tags to return. |

## blog_total_post_count

This function returns the total number of published posts in the specified blog. If no parameter is specified, it will count your default blog posts. Alternatively, you can specify `"default"` or a blog ID of a different blog to count. The blog ID is available in the URL of your blog dashboard for a particular blog.
This function has a limit of 10 calls per page and per email.
```hubl
{{ blog_total_post_count }}
{{ blog_total_post_count(359485112) }}
```
```html
16 54
```
| Parameter | Type | Description |
| --- | --- | --- |
| `selected_blog` | Blog ID or "default" | Specifies which blog to count. The blog ID is returned by the [module blog field](/reference/cms/fields/module-theme-fields#blog). |

## clear

Removes all items from a list. Unlike `pop()`, it does not return anything.
```hubl
{% set words = ["phone","home"] %}
{% do words.clear() %}
{{words}}
```
```html
[]
```
## color_contrast

This function validates color contrast based on [WCAG standards](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum). You'll need to include two colors as arguments, and you can include an optional argument to specify the rating (`AA` or `AAA`). Returns `true` or `false` based on whether the colors meet/exceed the standard.
```hubl
{{color_contrast('#FFFFFF','#273749')}}
{{color_contrast('rgb(255, 255, 255)','rgb(229, 237, 245)','AAA')}}

Hey there

``` ```html true false Hey there ``` | Parameter | Type | Description | | --- | --- | --- | | `color_1`Can also use a variable that stores a hex or RGB color code. For example:`theme.primary_color.color` | | `color_2` | Color | The second color to compare against the first. | | `rating` | String | The WCAG standard to use for rating. Can be either `AA` (default) or `AAA`. | ## color_variant This function lightens or darkens a hex value or color variable by a set amount. The first parameter is the hex color (for example ("#FFDDFF") or a variable storing a hex value. The second parameter is the amount to adjust it by, from 0 to 255. This function can be used in CSS files to create a color variation. Another good use case is to use it with a color parameter of a color module, to allow users to specify a primary color that automatically generates a color variation. In the example below, the hex color #3A539B is stored in a variable called `base_color`. The color is modified by -80 resulting in a darker blue (#00034B). ```hubl {% set base_color ="#3A539B" %} {{ color_variant(base_color, -80) }} ``` ```html #00034b ``` | Parameter | Type | Description | | --- | --- | --- | | `base_color` | HEX color string | The starting color to be altered. For example, `#F7F7F7`. | | `brightness_offset` | Integer | A positive or negative number used to lighten or darken the base color. | ## content_by_id The **`content_by_id`** function returns a landing page, website page or blog post by ID. The only parameter accepted by this function is a numeric content ID. The below example code shows this function in use to generate a hyperlinked list item. This function has a limit of 10 calls per page and per email. ```hubl {% set my_content = content_by_id(4715624297) %} ``` ```html ``` | Parameter | Type | Description | | --------- | ---- | --------------------------------- | | `id` | ID | The ID of the content to look up. | ## content_by_ids Given a list of content IDs, returns a dict of landing page, website page or blog posts matching those IDs. This function takes one parameter, a list of page or blog post IDs to look up, placed within an array. Up to 100 content objects can be passed. The below example code shows this function in use to generate a list of hyperlinked list items. This function has a limit of 10 calls per page and per email. ```hubl {% set contents = content_by_ids([4715624297, 4712623297, 5215624284]) %} ``` ```html ``` | Parameter | Type | Description | | --- | --- | --- | | `ids` | List | A list of page or blog post IDs to look up. Up to 100 content objects can be passed. | ## copy Return a shallow copy of the list. Equivalent to `a[:]`. A _shallow copy_ constructs a new compound object and then (to the extent possible) inserts _references_ into it to the objects found in the original. ```hubl {% set a = ["ham"] %} {% set b = a.copy() %} a: {{a}} b: {{b}} After Append {% do a.append("swiss") %} a: {{a}} b: {{b}} ``` ```html a: [ham] b: [ham] After Append a: [ham, swiss] b: [ham] ``` ## count Returns the number of times a variable exists in a list. ```hubl {% set attendees = ["Jack","Jon","Jerry","Jessica"] %} {% set jon_count = attendees.count("Jon") %} There are {{jon_count}} Jon's in the list.

After append:

{% do attendees.append("Jon") %} {% set jon_count_after_append = attendees.count("Jon") %} There are now {{jon_count_after_append}} Jon's in the list. ``` ```html There are 1 Jon's in the list.

After append:

There are now 2 Jon's in the list. ``` ## crm_associations Gets a list of CRM records associated with another record by its record ID, association category, and association definition ID. This function returns an object with the following attributes: `has_more`, `total`, `offset` and `results`. - `has_more` indicates there are more results available beyond this batch (total > offset). - `total` is the total number of results available. - `offset` is the offset to use for the next batch of results. - _`results`_ returns an array of the specified associated objects matching the function's parameters For security purposes, of the [HubSpot standard object types](https://knowledge.hubspot.com/get-started/manage-your-crm-database#standard-objects) only the `product`, and `marketing_event` objects can be retrieved on a publicly accessible page. Any other standard object type must be hosted on a page which is either [password protected](https://knowledge.hubspot.com/website-pages/password-protect-a-page) or requires a [CMS Membership login](https://knowledge.hubspot.com/website-pages/require-member-registration-to-access-private-content). Custom objects do not have this same restriction. ```hubl {% set associated_contacts = crm_associations(847943847, "HUBSPOT_DEFINED", 2, "limit=3&years_at_company__gt=2&orderBy=email", "firstname,email", false) %} {{ associated_contacts }} ``` ```html {has_more=true, offset=3, total=203, results=[{firstname=Aimee, id=905, email=abanks@company.com}, {firstname=Amy, id=1056, email=abroma@company.com}, {firstname=Abigael, id=957, email=adonahu@company.com}]} ``` This function can be called a maximum of 10 times per page and per email. Each `crm_associations` call can return at most 100 objects. The default limit is 10 objects. | Parameter | Type | Description | | --- | --- | --- | | `id` | ID | ID of the record to find associations from. | | `association category` | String | The category of the association definition. Possible values are `HUBSPOT_DEFINED`, `USER_DEFINED`, and `INTEGRATOR_DEFINED`. This parameter can be omitted for hubspot defined built-in association types. | | `association type id` | Integer | The ID of the association definition to use. For standard HubSpot supported objects see [ID of the association type to use](/guides/api/crm/associations).Otherwise, this association ID can found at the [CRM Objects Schema API](/guides/cms/content/data-driven-content/crm-objects#getting-a-custom-object-type-s-details). | | `query` | String | The `id` of the record OR a query string, delimited by `&`. All expressions are ANDed together. Supported operators are:Queries can include the following parameters: | | `properties` | String | Optional. A comma-separated list of properties to return. By default, a small set of common properties are returned. The ID property is always returned.A full list of properties can be found using the [get all contact properties](/reference/api/crm/properties/v1-contacts#get-all-contact-properties) and [get all company properties](/reference/api/crm/properties/v1-companies#get-all-company-properties) endpoints. | | `formatting` | Boolean | Optional. Format values such as dates and currency according to this portal's settings. Omit or pass `false` for raw strings. | ## crm_object Gets a single CRM record by query or by its ID. Records are returned as a dict of properties and values. This function can also be [used with custom and integrator objects](/guides/cms/content/data-driven-content/crm-objects). For security purposes, of the [HubSpot standard object types](https://knowledge.hubspot.com/get-started/manage-your-crm-database#standard-objects) only the `product`, and `marketing_event` objects can be retrieved on a publicly accessible page. Any other standard object type must be hosted on a page which is either [password protected](https://knowledge.hubspot.com/website-pages/password-protect-a-page) or requires a [CMS Membership login](https://knowledge.hubspot.com/website-pages/require-member-registration-to-access-private-content). Custom objects do not have this same restriction. What is the difference between `in` and `contains`? `in` returns if the property value matches with any of the given values. Whereas `contains` returns if the property values for multi-select has all of the given values. ```hubl {% set contact = crm_object("contact", "email=contact@company.com", "firstname,lastname", false) %} {% set contact = crm_object("contact", 123) %} {{ contact.firstname }} {{ contact.lastname }} ``` ```html Brian Halligan ``` This function can only be called a maximum of 10 times on a single page. | Parameter | Type | Description | | --- | --- | --- | | `object_type` | String | The object type name. Object type names are case sensitive.[The supported object types](/guides/cms/content/data-driven-content/crm-objects#supported-crm-object-types). To find the names of the account specific and integrator object types available in your account use the [CRM Objects Schema API](/guides/cms/content/data-driven-content/crm-objects#getting-a-custom-object-type-s-details) to get the type definitions and look for the name property. It contains the internal object type name that should be used in the functionFor integrator and account specific object types with the same name as the built-in objects use the objects [fully qualified name](/guides/cms/content/data-driven-content/crm-objects#getting-a-custom-object-type-s-details) (FQN). | | `query` | String | Optional. The `id` of the record OR a query string, delimited by `&`. All expressions are ANDed together. Supported operators are:Queries can include the following parameters: | | `properties` | String | Optional. A comma-separated list of properties to return. By default, a small set of common properties are returned. The ID property is always returned.A full list of properties can be found using the [get all contact properties](/reference/api/crm/properties/v1-contacts#get-all-contact-properties) and [get all company properties](/reference/api/crm/properties/v1-companies#get-all-company-properties) endpoints. | | `formatting` | Boolean | Optional. Format values such as dates and currency according to this portal's settings. Pass `false` for raw strings. | When building a query, the values of `range`, `distinct`, `ndistinct`, and `startswith` are reserved keywords. To query a property that uses one of those names, you'll need to use the following format: `range__eq=` (rather than `range=`). ## crm_objects Gets a list of records for a specific object type from the HubSpot CRM. This function returns an object with the following attributes: `has_more`, `total`, `offset` and `results`. - `has_more` indicates there are more results available beyond this batch (total > offset). - `total` is the total number of results available. - `offset` is the offset to use for the next batch of results. - `results` returns an array of the specified objects matching the function's parameters. Results can be sorted using at least one order parameter in the query. For example, `crm_objects("contact", "firstname=Bob&order=lastname&order=createdate")` will order contacts with the first name `"Bob"` by last name and then by `createdate`. To reverse a sort, prepend `-` to the property name like `order=-createdate`. The CRM objects function can also be [used with custom and integrator objects](/guides/cms/content/data-driven-content/crm-objects). For security purposes, of the [HubSpot standard object types](https://knowledge.hubspot.com/get-started/manage-your-crm-database#standard-objects) only the `product`, and `marketing_event` objects can be retrieved on a publicly accessible page. Any other standard object type must be hosted on a page which is either [password protected](https://knowledge.hubspot.com/website-pages/password-protect-a-page) or requires a [CMS Membership login](https://knowledge.hubspot.com/website-pages/require-member-registration-to-access-private-content). Custom objects do not have this same restriction. ```hubl {% set objects = crm_objects("contact", "firstname__not_null=&limit=3", "firstname,lastname") %} {{ objects }} ``` ```html {has_more=true, offset=3, total=331, results=[{firstname=Brian, id=44151, lastname=Halligan}, {firstname=Dharmesh, id=44051, lastname=Shah}, {firstname=George, id=88551, lastname=Washington}]} ``` This function can be called a maximum of 10 times per page and per email. Each `crm_objects` call can return at most 100 objects. The default limit is 10 objects. | Parameter | Type | Description | | --- | --- | --- | | `object_type` | String | The type of object by name. Object type names are case sensitive. Singular and plural are accepted for standard object types (e.g., `contact`, `contacts`). Learn more about the [supported object types](/guides/cms/content/data-driven-content/crm-objects#supported-crm-object-types). To find the names of the account specific and integrator object types available in your account, use the [CRM objects schema API](/guides/cms/content/data-driven-content/crm-objects#getting-a-custom-object-type-s-details) to get the type definitions and the name property.For integrator and account specific object types with the same name as the built-in objects, use the objects [fully qualified name](/guides/cms/content/data-driven-content/crm-objects#getting-a-custom-object-type-s-details) (FQN). | | `query` | String | Optional. The ID of the record or a query string, delimited by `&`. All expressions are ANDed together.Supported operators are:Example: `"email=contact@company.com"` | | `properties` | String | Optional. A comma-separated list of properties to return. By default, a small set of common properties are returned. The ID property is always returned. A full list of properties can be found using the [get all contact properties](/reference/api/crm/properties/v1-contacts#get-all-contact-properties) and [get all company properties](/reference/api/crm/properties/v1-companies#get-all-company-properties) endpoints.The record ID is always included in the returned object properties even when not explicitly added in the property list. | | `formatting` | Boolean | Optional. Format values such as dates and currency according to this portal's settings. Pass `false` for raw strings. | When building a query, the values of `range`, `distinct`, `ndistinct`, and `startswith` are reserved keywords. To query a property that uses one of those names, you'll need to use the following format: `range__eq=` (rather than `range=`). ## crm_property_definition Get the property definition for a given object type and property name. Supported object types are HubSpot standard objects (e.g., contacts), portal specific objects, and integrator objects. ```hubl {{ crm_property_definition("house_listing", "agent_name") }} ``` | Parameter | Type | Description | | --- | --- | --- | | `object_type` | String | The object type name. Object type names are case sensitive. [The supported object types](/guides/cms/content/data-driven-content/crm-objects#supported-crm-object-types). To find the names of the account specific and integrator object types available in your account use the [CRM Objects Schema API](/guides/cms/content/data-driven-content/crm-objects#getting-a-custom-object-type-s-details) to get the type definitions and look for the name property. It contains the internal object type name that should be used in the functionFor integrator and account specific object types with the same name as the built-in objects use the objects [fully qualified name](/guides/cms/content/data-driven-content/crm-objects#getting-a-custom-object-type-s-details) (FQN). | | `property_name` | String | The case-insensitive property name to retrieve the definition for. | ## crm_property_definitions Get the property definitions for a given object type and set of property names. Supported object types are HubSpot standard objects (e.g. contacts), portal specific objects, and integrator objects. ```hubl {{ crm_property_definitions("house_listing", "agent_name,address") }} ``` | Parameter | Type | Description | | --- | --- | --- | | `object_type` | String | The object type name. Object type names are case sensitive. [The supported object types](/guides/cms/content/data-driven-content/crm-objects#supported-crm-object-types). To find the names of the account specific and integrator object types available in your account use the [CRM Objects Schema API](/guides/cms/content/data-driven-content/crm-objects#getting-a-custom-object-type-s-details) to get the type definitions and look for the name property. It contains the internal object type name that should be used in the functionFor integrator and account specific object types with the same name as the built-in objects use the objects [fully qualified name](/guides/cms/content/data-driven-content/crm-objects#getting-a-custom-object-type-s-details) (FQN). | | `property_name` | String | Optional. The case-insensitive property names comma separated, to retrieve the definition for. If empty, the definitions for all the properties will be retrieved. | ## cta Because CTA modules have so many parameters that contain variations of their code, you can use the CTA function to easily generate a particular CTA in a template, page, or email. This function is what the rich text editor uses when you add a CTA via the editor. This function has a limit of 10 calls per page and per email. ```hubl {{ cta("ccd39b7c-ae18-4c4e-98ee-547069bfbc5b") }} {{ cta("ccd39b7c-ae18-4c4e-98ee-547069bfbc5b", "justifycenter") }} ``` ```html Start a HubSpot trial Start a HubSpot trial ``` | Parameter | Type | Description | | --- | --- | --- | | `guid` | String | The ID of the CTA to render. Can be found in the URL of the details screen of the CTA. | | `align_opt` | Enumeration | Adjusts alignment of CTA. Values include `justifyleft`, `justifycenter`, `justifyright`, `justifyfull`. | ## extend Extend a list by appending all items from an iterable. In other words, insert all list items from one list into another list. ```hubl {% set vehicles = ["boat","car","bicycle"] %} {% set more_vehicles = ["airplane","motorcycle"] %} {% do vehicles.extend(more_vehicles) %} {{vehicles}} ``` ```html [boat, car, bicycle, airplane, motorcycle] ``` ## file_by_id This function returns the metadata of a file by ID. It accepts a single parameter, the numeric ID of the file to look up. To retrieve multiple files at once, use the [files_by_ids](#files_by_ids) function instead. This function is limited to 10 calls per page and per email. ```hubl {% set file = file_by_id(123) %} {{ file.friendlyUrl }} ``` ```html https://www.hubspot.com/hubfs/HubSpot_Logos/HSLogo_color.svg ``` | Parameter | Type | Description | | --------- | ------ | ------------------------------ | | `file_id` | Number | The ID of the file to look up. | ## files_by_ids Returns the metadata of multiple files by ID. It accepts an array of file IDs. This function limited to 10 calls per page and per email, and can accept up to 100 file IDs. ```hubl {% set files = files_by_ids([123,456])%} {% for file in files %} URL:{{file.friendlyUrl}}
{% endfor %} ``` ```html URL:https://api-na1.hubspot.com/filemanager/api/v2/files/123/example-file-1 URL:https://api-na1.hubspot.com/filemanager/api/v2/files/456/example-file-2 ``` | Parameter | Type | Description | | --- | --- | --- | | `file_id` | Number | The IDs of the files to look up, separated by commas (up to 100). | For example, you can use this function, to loop through a contact's uploaded files. To loop through results, you'll need to use the [split HubL filter](/reference/cms/hubl/filters#split), as the CRM property will be returned as a semicolon-separated string. ```hubl {% set objects = crm_objects("contact", "email=bh@hubspot.com", "file").results %} {% for item in objects %} {% set fileIDs = item.file|split(";") %} {% set files = files_by_ids(fileIDs) %} {% for file in files %} {% endfor %} {% endfor %} ``` ## flag_content_for_access_check When a blog post is [configured for self-registration access](https://knowledge.hubspot.com/blog/use-self-registration-for-private-blog-content), visitors will need to register or log in to view the full post content. HubSpot's default blog listing assets automatically include this functionality, and will also include a lock icon indicator for posts that require self-registration to access. Learn more about [building custom solutions using this function and its corresponding API](/guides/cms/content/memberships/overview#private-blog-posts-with-self-registration). This function checks whether a blog post can be accessed by the current visitor. When called, the function is replaced with the following attribute: `hs-member-content-access=` A value of `true` indicates that the blog post requires the visitor to log in to view the full content. ```hubl {% for content in contents %}
...
{% endfor %} ``` ```html
...
``` | Parameter | Type | Description | | --- | --- | --- | | `ID` | ID | The ID of the content that will be checked against the current logged in viewer. | ## follow_me_links Returns the [social media account links set in account settings](https://knowledge.hubspot.com/design-manager/use-default-modules-in-your-template#add-social-accounts-to-the-follow-me-module). Used in the [default follow_me module](/reference/cms/modules/default-modules#follow-me). ```hubl {% set fm = follow_me_links() %} {{ fm }} ``` ```html [FollowMeLink{type=URL, id=5142734, altName=RSS, followMeUrl=http://website.com/rss.xml, iconName=rss, name=http://website.com/rss.xml}] ``` ## format_address Formats an address based on the context's locale. ```hubl {{ format_address('en-us', { address: "25 First Street", address2: "2nd Floor", city: "Cambridge", state: "MA", country: "United States", zip: "02141"}) }} ``` ```html [25 First Street, 2nd Floor, Cambridge, MA 02141, United States] ``` | Parameter | Type | Description | | --- | --- | --- | | `locale` | String | The locale to format the address in. | | `address` | String | The street address. | | `address2` | String | The second line of the address, such as floor or apartment number. | | `city` | String | The city of the address. | | `state` | String | The state of the address. | | `country` | String | The country of the address. | | `zip` | String | The zip code of the address. | ## format_company_name Formats a company's name by adding Japanese honorifics where appropriate. ```hubl {{ format_company_name("companyName", addJapaneseHonorifics) }} ``` ```html HubSpot ``` | Parameter | Type | Description | | --- | --- | --- | | `companyName` | String | The name of the company. | | `useHonorificIfApplicable` | Boolean | When set to `true` and the context's language is Japanese, a Japanese company honorific will be added where appropriate. | ## format_name Formats a person's name by putting the surname before the first name and adds Japanese honorifics where appropriate ```hubl {{ format_name("firstName", "surname", addJapaneseHonorifics) }} ``` ```html Hobbes Baron ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `firstName` | String | The person's first name. | | | `surname` | String | The person's surname or last name. | `False` | | `useHonorificIfApplicable` | Boolean | When set to `true` and the context's language is Japanese, a Japanese honorific will be added where appropriate. | | ## format_datetime Formats both the date and time components of a date object, similar to the [format_datetime](/reference/cms/hubl/filters#format_datetime) HubL filter. This function replaces the deprecated `datetimeformat` function. ```hubl {{ format_datetime(content.publish_date, "short", "America/New_York", "en") }} ``` ```html 12/31/69 7:00 PM ``` | Parameter | Type | Description | | --- | --- | --- | | `format` | | `timeZone` | String | The time zone of the output date in [IANA TZDB format](https://data.iana.org/time-zones/tzdb/). | | `locale` | String | The locale to use for locale-aware formats. | ## geo_distance This function contains 4 parameters and calculates the ellipsoidal 2D distance between two points on Earth. Use this function as a filter query for getting HubDB data. ```hubl {% for row in hubdb_table_rows(1234567, "geo_distance(loc,1.233,-5.678,mi)__gt=500") %} {{row.name}}
{% endfor %} ``` ```html Cambridge, MA
Salem, MA
San Diego, CA
Chicago, IL
``` | Parameter | Type | Description | | --- | --- | --- | | `point1` | Location | location from a HubDB column. | | `point2_lat` | Latitude | Latitude of point2. | | `point2_long` | Longitude | Longitude of point2. | | `units` | String | Units for the return value. Options are `FT` for feet, `MI` for miles, `M` for meters or `KM` for kilometers. | ## get_asset_url This function returns the public URL of a specified template or code file. The parameter of this function is the asset's path in the design manager. Coded file URLs update each time you publish them; therefore, by using this function your ensure you are always using the latest version of the file. You can automatically generate this function in the app, by either right-clicking a **file** and selecting **Copy public URL**, or by clicking **Actions**, then selecting **Copy public URL**. The example below gets the URL of a Javascript file authored in Design Manager that can be included as the `src` of a ` ``` ## resize_image_url Rewrites the URL of image stored in File Manager to a URL that will resize the image on request. The function accepts one required parameter, and five optional parameters. At least one optional parameter must be passed. Required - **URL:** string, URL of a HubSpot-hosted image. Optional - **width:** number, the new image width in pixels. - **height:** number, the new image height in pixels. - **length:** number, the new length of the largest side, in pixels. - **upscale:** boolean, use the resized image dimensions even if they would scale up the original image (images may appear blurry). - **upsize:** boolean, return the resized image even if it is larger than the original in bytes. Images that are larger than 4096 pixels in height or width will not be automatically resized. Instead, you'll need to [manually resize the image](https://knowledge.hubspot.com/files/automatic-image-resizing-on-hubspot-content). ```hubl {{ resize_image_url("http://your.hubspot.site/hubfs/img.jpg", 0, 0, 300) }} ``` ```html http://your.hubspot.site/hubfs/img.jpg?length=300&name=img.jpg ``` | Parameter | Type | Description | | --- | --- | --- | | `url` | String | URL of a HubSpot-hosted image. | | `width` | Integer (px) | The new image width, in pixels. | | `height` | Integer (px) | The new image height, in pixels. | | `length` | Integer (px) | The new length of the largest side, in pixels. | | `upscale` | Boolean | Use the resized image dimensions even if they would scale up the original image (images may appear blurry).Default value is `false`. | | `upsize` | Boolean | Return the resized image even if it is larger than the original in bytes.Default value is `false`. | ## reverse Reverses the order of items in a list. Doesn't take any parameters. To reverse an object or return an iterator to iterate over the list in reverse, use [`|reverse`](/reference/cms/hubl/filters#reverse) ```hubl {% set numbers = [1,2,3,4] %} {% do numbers.reverse() %} {{numbers}} ``` ```html [4, 3, 2, 1] ``` ## set_response_code Set the response code as the specified code. [404](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404) is the only supported code for now. When using this, your page will return a `404` error. ```hubl {{ set_response_code(404) }} ``` ```html ``` | Parameter | Type | Description | | --- | --- | --- | | `code` | Integer | The HTTP response code. Currently, `404` is the only supported code. | ## super This function prints content from the parent template into a child template using the [extends](/reference/cms/hubl/overview#blocks-and-extends) tag. For example, in the code below, a basic HTML template has been created with a HubL block named `sidebar` and saved as `parent.html`. A second template file is created that will extend that parent file. Normally, the `

` would be printed in the parent HTML's sidebar block. But by using `super`, content from the parent template sidebar block is combined with the content from the child template. ```hubl {% extends "custom/page/web_page_basic/parent.html" %} {% block sidebar %}

Table Of Contents

{{ super() }} {% endblock %} ``` ```html This is the parent template

Table of contents

Sidebar content from parent. ``` ## today Returns the start of today (12:00am). Optionally you can add a parameter to change the timezone from the default UTC. ```hubl {{ today() }} {{ today("America/New_York") }} {{ unixtimestamp(today("America/New_York").plusDays(1)) }} ``` ```html 2018-10-23T00:00Z 2018-10-23T00:00-04:00[America/New_York] 1540353600000 ``` ## to_local_time Converts a UNIX timestamp to the local time, based on your HubSpot Report Settings. You can then apply a datetimeformat filter to format the date. ```hubl {{ to_local_time(eastern_dt) }} ``` ```html 2015-05-13 10:37:31 ``` | Parameter | Type | Description | | --------- | -------- | ---------------------------------------- | | `date` | Datetime | UNIX timestamp to convert to local time. | ## topic_cluster_by_content_id Returns a HubL dict representing the topic cluster associated with a piece of content (determined by the passed content id), including metadata about the associated pillar page, core topic, and subtopics. Can be used to "auto-link" a piece of content with its associated pillar page \[if it exists\]. Available metadata can be found in: attachableContent (the current content's metadata), topic (the current content's associated topic metadata), coreTopic (the associated cluster's core topic metadata), and pillarPage (the associated pillar page's metadata). Use `{{ topicCluster|pprint }}` to see a full a display of available properties/attributes. ```hubl {{ topic_cluster_by_content_id(content.id) }} {%- if content.id -%} {%- set topicCluster = topic_cluster_by_content_id(content.id) -%} {%- if topicCluster.pillarPage.url.value and topicCluster.pillarPage.publishState == "PUBLISHED" -%} {%- endif -%} {%- endif -%} ``` ```html { attachedContent, topic, coreTopic, pillarPage } (AttachedContentWithContext: {attachedContent=AttachedContent{id=1234, topicId=1234, clusterId=1234, portalId=0, attachable=Attachable{contentId=124, url=ValidatedUri{https://www.hubspot.com/}, attachableType=LANDING_PAGE, title=title, publishState=PUBLISHED, deletedAt=null}, isLinkedToPillarPage=null, isPillarPage=null, createdAt=1547238986475, updatedAt=1547238986475, deletedAt=null}, coreTopic=Optional[Topic{id=1234, portalId=0, clusterId=1234, phrase=TOPIC PHRASE, attachedContent=null, isCoreTopic=false, createdAt=1547157062081, updatedAt=1547232313421, deletedAt=null}], pillarPage=Optional[AttachedContent{id=1234, topicId=1234, clusterId=1234, portalId=0, attachable=Attachable{contentId=null, url=ValidatedUri{https://www.hubspot.com.com/}, attachableType=EXTERNAL_URL, title=null, publishState=PUBLISHED, deletedAt=null}, isLinkedToPillarPage=null, isPillarPage=null, createdAt=1547157062086, updatedAt=1547157062086, deletedAt=null}], topic=Optional[Topic{id=1234, portalId=0, clusterId=8345, phrase=topic phrase, attachedContent=null, isCoreTopic=false, createdAt=1547238962703, updatedAt=1547238962703, deletedAt=null}]}) ``` This function has a limit of 10 calls per page and per email. | Parameter | Type | Description | | ------------ | ---- | ------------------------------ | | `content_id` | Id | The id of the page to look up. | ## truncate The truncate function works just like the [truncate filter](/reference/cms/hubl/filters#truncate), but uses function syntax instead of filter syntax. The first parameter specifies the string. The second parameter specifies the length at which to truncate. The final parameter specifies the characters to add when the truncation occurs. Because this function relies on the spaces between words to shorten strings, it may not work as expected for languages without spaces between characters, such as Japanese. ```hubl {{ truncate("string to truncate at a certain length", 19, false, "...") }} {% set longString = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus sodales ultricies velit sit amet ornare." %} {{ truncate(longString, 40, false, "...") }} ``` ```html string to truncate ... Lorem ipsum dolor sit amet, consectetur ... ``` | Parameter | Type | Description | | --- | --- | --- | | `string_to_truncate` | String | String that will be truncated. | | `length` | integer | Specifies the length at which to truncate the text (includes HTML characters). | | `killwords` | boolean | If true, the string will cut text at length, regardless of if it's in the middle of a word. | | `end` | String | The characters that will be added to indicate where the text was truncated. | ## type This function accepts one argument and returns the type of an object. The return value is one of: `"bool"`, `"datetime"`, `"dict"`, `"float"`, `"int"`, `"list"`, `"long"`, `"null"`, `"str"` or `"tuple"`. ```hubl {{ type("Blog") }} {% set my_type = type("Blog") %}

{{my_type}}

``` ```html

str

``` ## unixtimestamp This function returns a unix timestamp when you supply a datetime object. ```hubl {{ unixtimestamp(d) }} ``` ```html 1565983117868 ``` ## update Updates the dict with the elements from another dict object or from an iterable of key-value pairs. Use this function to combine or merge objects. ```hubl {% set dict_var = {"authorName": "Douglas Judy", "authorTitle": "Mastermind" } %} {% do dict_var.update({"authorFriend": "Jake"}) %} {% do dict_var.update({"authorLocation": "unknown"}) %} {{ dict_var }} ``` ```html {authorName=Douglas Judy, authorTitle=Mastermind, authorFriend=Jake, authorLocation=unknown} ``` # If statements @ A practical guide to HubSpot's HubL if statements for CMS developers. $ reference & cms/hubl --- # If Statements You can include conditional logic in your modules and templates by using HubL [if statements](#basic-if-statement-syntax) and [unless statements](#unless-statements). If statements often contain HubL [supported operators](/reference/cms/hubl/operators-and-expression-tests) and can be used to execute [expression tests](/reference/cms/hubl/operators-and-expression-tests#expression-tests). If you're using [personalization tokens](/reference/cms/hubl/functions#personalization-token) within a conditional statement of your email module, you must [enable programmable email for the module](/guides/cms/content/data-driven-content/emails-with-programmable-content). Information passed via the [v3](/guides/api/marketing/emails/transactional-emails#single-send-api) or [v4](/guides/api/marketing/emails/single-send-api) single send APIs will not function within `if` statements, as the templates compile before the information populates. ## Basic if statement syntax HubL uses if statements to help define the logic of a template. The syntax of HubL if statements is very similar to conditional logic in Python. `if` statements are wrapped in [statement delimiters](/reference/cms/hubl/variables-macros-syntax), starting with an opening `if` statement and ending with an `endif`. The example below provides the basic syntax of an if statement, where "condition" would be replaced with the boolean rule that you were going to evaluate as being true of false. ```hubl {% if condition %} If the condition is true print this to template. {% endif %} ``` Now that you have seen the basic syntax, let's look at a few actual examples of basic if statements. The next examples below show if statements that check to see whether or not a HubL module with the name `my_module` and whether a variable named `my_module` are present on a template. Notice that without any operators, the if statement will evaluate whether or not the module is defined in the context of the template. ```hubl {% module "my_module" path="@hubspot/rich_text", label="My rich text module", html="Default module text" export_to_template_context=true %} {% if widget_data.my_module %} A module named "my_module" is defined in this template. {% endif %} {% set my_variable = "A string value for my variable" %} {% if my_variable %} The variable named my_variable is defined in this template. {% endif %} ``` Notice that when evaluating the HubL module, the module name is left in quotes within the `if` statement and while testing the variable no quotes are used around the variable name. In both examples above, the module and the variable exist in the template, so the statements evaluate to print the markup. Please note that these examples are only testing whether the module and variable are defined, not whether or not they have a value. Now let's look at an `if` statement that evaluates whether a module has a value, instead of evaluating whether it exists on the template. To do this, we need to use the [export_to_template_context](/reference/cms/modules/export-to-template-context) parameter. In the example below, if the text module is valued in the content editor, the markup would print. If the module's text field were cleared, no markup would render. If you are working within custom modules, there is a simplified `widget.widget_name` syntax outlined in the [example here](/reference/cms/modules/configuration). ```hubl {% module "product_names" path="@hubspot/text", label="Enter the product names that you would like to render the coupon ad for", value="all of our products", export_to_template_context=True %} {% if widget_data.product_names.value %} {% endif %} ``` ```html ``` ## Using elif and else `if` statements can be made more sophisticated with additional conditional statements or with a rule that executes when the condition or conditions are false. `elif` statements allow you to add additional conditions to your logic that will be evaluated after the previous condition. **`else`** statements define a rule that executes when all other conditions are false. You can have an unlimited number of `elif` statements within a single if statement, but only one `else` statement. Below is the basic syntax example of if statement that uses the [\<= operator](/reference/cms/hubl/operators-and-expression-tests#comparison) to check the value of a variable. In this example, the template would print: "Variable named number is less than or equal to 6." ```hubl {% set number = 5 %} {% if number <= 2 %} Variable named number is less than or equal to 2. {% elif number <= 4 %} Variable named number is less than or equal to 4. {% elif number <= 6 %} Variable named number is less than or equal to 6. {% else %} Variable named number is greater than 6. {% endif %} ``` Below is one more example that uses a choice module to render different headings for a careers page, based on the department chosen by the user. The example uses the [\== operator](/reference/cms/hubl/operators-and-expression-tests#comparison), to check for certain predefined values in the choice module. ```hubl {% choice "department" label="Choose department", value="Marketing", choices="Marketing, Sales, Dev, Services" export_to_template_context=True %} {% if widget_data.department.value == "Marketing" %}

Want to join our amazing Marketing team?!

We have exciting career opportunities on the {{ widget_data.department.value }} team.

{% elif widget_data.department.value == "Sales" %}

Are you a Sales superstar?

We have exciting career opportunities on the {{ widget_data.department.value }} team.

{% elif widget_data.department.value == "Dev" %}

Do you love to ship code?

We have exciting career opportunities on the {{ widget_data.department.value }} team.

{% else %}

Want to work with our awesome customers?

We have exciting career opportunities on the {{ widget_data.department.value }} team.

{% endif %} ``` ## Unless statements `unless` statements are conditionals just like `if` statements, but they work on the inverse logic. They will render and compile the code between the opening and closing tags, unless the single boolean condition evaluates to true. Unless statements begin with an **`unless`** and end with an **`endunless`.** `unless` statements support `else` but not `elif`. Below is an example that prints an "Under construction" header, unless the rich text field is valued. If the rich text field has content, then that content will display. ```hubl {% module "my_page_content" path="@hubspot/rich_text", label="Enter your page content", html="" export_to_template_context=true %} {{ widget_data.my_page_content.html }} {% unless widget_data.my_page_content.html %}

This page is under construction.

Come back soon!

{% endunless %} ``` ## ifchanged In addition to if and unless statements, HubL supports `ifchanged` statements. These statements can be used to only render markup when a variable has changed since a prior invocation of this tag. ## Inline if statements HubL supports inline `if` statements. These can be used to write conditional logic in a concise manner with [operators and expression tests](/reference/cms/hubl/operators-and-expression-tests). ```hubl {% set color = "Blue" if is_blue is truthy else "Red" %} // color == "blue" {{ "Blue" if is_blue is truthy else "Red" }} // "Blue" {% set dl = true %} Download PDF ``` ## Ternary operators It is also possible to use ternary operators to quickly write conditional logic with [operators and expression tests](/reference/cms/hubl/operators-and-expression-tests#logical). ```hubl // If the variable is_blue is true, output "blue", otherwise output"red" {{ is_blue is truthy ? "blue" : "red" }} // Set the variable is_red to false if is_blue is true, otherwise set to true {% set is_red = is_blue is truthy ? false : true %} ``` # For Loops @ For loops can be used in HubL to iterate through sequences of objects. $ reference & cms/hubl --- # For loops For loops can be used in HubL to iterate through sequences of objects. They will most commonly be used with rendering [blog content](/guides/cms/content/templates/types/blog) in a listing format, but they can also be used to sort through other sequence variables. For loops begin with a `{% for %}` statement and end with an `{% endfor %}` statement. Within the `{% for %}` statement a single sequence item is named followed by `in` and then the name of the sequence. The code between the opening and closing `for` statements is printed with each iteration, and generally includes the printed variable of the individual sequence item. Below is the basic syntax of a for loop: ```hubl {% for item in items %} {{ item }} {% endfor %} ``` Below is a basic example that shows how to print a sequence of variable values into a list. ```hubl {% set languages = ["HTML", "CSS", "Javascript", "Python", "Ruby", "PHP", "Java"] %}

Languages

;
    {% for language in languages %}
  • {{ language }}
  • {% endfor %}
``` ```html

Languages

  • HTML
  • CSS
  • Javascript
  • Python
  • Ruby
  • PHP
  • Java
``` ## Loop properties As a loop iterates, you can use [conditional logic](/reference/cms/hubl/if-statements) to define the loop's behavior. The variable property `loop.index` keeps a count of the current number of the iterations of the loop. There are several other loop variable properties that count the iterations in different ways. These properties are described below: | Variable | Description | | --- | --- | | `loop.cycle` | A helper function to cycle between a list of sequences. See the explanation below. | | `loop.depth` | Indicates how deep in deep in a recursive loop the rendering currently is. Starts at level 1 | | `loop.depth0` | Indicates how deep in deep in a recursive loop the rendering currently is. Starts at level 0 | | `loop.first` | This variable evaluates to true, if it is the first iteration of the loop. | | `loop.index` | The current iteration of the loop. This variable starts counting at 1. | | `loop.index0` | The current iteration of the loop. This variable starts counting at 0. | | `loop.last` | This variable evaluates to true, if it is the last iteration of the loop. | | `loop.length` | The number of items in the sequence. | | `loop.revindex` | The number of iterations from the end of the loop. Counting down to 1. | | `loop.revindex0` | The number of iterations from the end of the loop. Counting down to 0. | Below are some examples that use different loop variables. The following basic example uses `loop.index` to keep a count that is printed with each iteration. ```hubl {% set loopy = ["Content", "Social", "Contacts", "Reports"] %} {% for app in loopy %} {{ loop.index }}. {{app}}
{% endfor %} ``` ```html 1. Content
2. Social
3. Contacts
4. Reports
``` The next example uses conditional logic to check whether the length of the loop is `divisibleby` certain numbers. It then renders the width of the post-item div accordingly. The example uses the standard blog post loop and assumes that there are 6 posts in the loop. ```hubl {% for content in contents %} {% if loop.length is divisibleby 4 %} {% elif loop.length is divisibleby 3 %} {% else %} {% endif %} {% endfor %} ``` ```html ``` ## Nested loops Loops can also be nested with loops. The child for loop will run with each iteration of the parent for loop. In the example below, a list of child items is printed in a nested `
    ` within a `
      ` of parent items. ```hubl {% set parents = ["Parent item 1", "Parent item 2", "Parent item 3"] %} {% set children = ["Child item 1", "Child item 2", "Child item 3"] %}
        {% for parent in parents %}
      • {{parent}}
          {% for child in children %}
        • {{child}}
        • {% endfor %}
      • {% endfor %}
      ``` ```html
      • Parent item 1
        • Child item 1
        • Child item 2
        • Child item 3
      • Parent item 2
        • Child item 1
        • Child item 2
        • Child item 3
      • Parent item 3
        • Child item 1
        • Child item 2
        • Child item 3
      ``` ## cycle The cycle tag can be used within a for loop to cycle through a series of string values and print them with each iteration. One of the most practical applications to this technique is applying alternating classes to your blog posts in a listing. This tag can be used on more than two values and will repeat the cycle if there are more loop iterations than cycle values. In the example below, a class of `odd` and `even` are applied to posts in a listing (the example assumes that there are 5 posts in the loop). Please note that there are **no spaces** between the comma-separated cycle string values. ```hubl {% for content in contents %} {% endfor %} ``` ```html ``` ## Variables within loops Any variables defined within loops are limited to the scope of that loop and cannot be called from outside of the loop. You can call variables that are defined outside of a loop, from within a loop, but not the other way around. ## Key, value pairs in loops If the dict of information you are looping through has key and value pairs, a simple for loop would only have access to the values. If you wish to have access to both the keys and values within the for loop, the HubL would be formatted as such: ```hubl {% set dict_var = {"name": "Cool Product", "price": "$20", "size":"XL"} %} {% for key, val in dict_var.items() %} {{ key }}: {{ val }}
      {% endfor %} ``` ```html name: Cool Product
      price: $20
      size: XL
      ``` ## Iterate a set number of times Sometimes you want to iterate a set number of times, this can be useful in generating HTML or CSS. You can do this using the range function. ```hubl {% for x in range(0,5) %} {{loop.index}} {% endfor %} ``` ```html 1 2 3 4 5 ``` ## Using HubL tags in loops When you add a tag to page HubSpot automatically assigns an `id` to the wrapping HTML. That tag is unique per tag "name". In situations where you need to use a tag in a for loop setting unique names is not practical. Add the `unique_in_loop` parameter to your tag to generate unique ids. This parameter appends the module name with the current loop iteration number, ensuring that it is unique. Unique id's are not only needed for valid HTML but are important for [accessibility](/guides/cms/content/accessibility). ```hubl {% for item in module.icon_field %} {% icon name="{{ item.name }}", style="{{ item.type }}", unicode="{{ item.unicode }}", unique_in_loop=True %} {% endfor %} ``` # Operators & Expression Tests @ In order to expand the logic and functionality of your templates, HubL supports several key operators and expression tests. The operators allow you to execute math functions, make comparisons, complicate template logic, and alter what markup renders. In addition, this article contains a comprehensive list of expression tests that can be used in HubL . $ reference & cms/hubl --- # Operators & Expression Tests In order to expand the logic and functionality of your templates, HubL supports several key operators and expression tests. The [operators](/reference/cms/hubl/operators-and-expression-tests#operators) allow you to execute math functions, make comparisons, complicate template logic, and alter what markup renders. In addition, this article contains a comprehensive list of [expression tests](/reference/cms/hubl/operators-and-expression-tests#expression-tests) that can be used in HubL. ## Operators Operators are symbols that tell the HubL compiler to execute various operations that result in the final output. Operators are placed between operands in order to relate the two values, whether for executing math functions, making comparisons, or implementing boolean expressions. Below are the operators you can use in HubL, organized by type. ### Math Standard math operators can be used to calculate values in the context of a template. ```hubl {% set my_num = 11 %} {% set my_number = 2 %} {{ my_num + my_number }} {{ my_num - my_number }} {{ my_num / my_number }} {{ my_num % my_number }} {{ my_num // my_number }} {{ my_num * my_number }} {{ my_num ** my_number }} ``` | Symbol | Description | | --- | --- | | `+` | Adds two objects together, generally number values. To concatenate strings or lists, you should use the [`~` operator](#other-hubl-operators) instead. | | `-` | Subtracts one number from another. | | `/` | Divides numbers. | | `%` | Returns the remainder from dividing numbers. | | `//` | Divide two numbers and return the truncated integer result. For example, `{{ 20 // 7 }}` is `2`. | | `*` | Multiplies numbers. | | `**` | Raise the left operand to the power of the right operand. | ### Comparison Comparison operators can be used to evaluate values for template logic. You can see some examples of comparison operators being used in [if statements](/reference/cms/hubl/if-statements). ```hubl {% set my_num = 11 %} {% set my_number = 2 %} {{ my_num == my_number }} {{ my_num != my_number }} {{ my_num > my_number }} {{ my_num >= my_number }} {{ my_num < my_number }} {{ my_num <= my_number }} ``` | Symbol | shorthand | Description | | --- | --- | --- | | `==` | eq | Equal to. Evaluates to true if the two objects have equal values. | | `!=` | ne | Not equal to. Evaluates to true if the two objects are not equal. | | `>` | gt | Greater than. Evaluates to true if the left operand value is greater than the right operand. | | `>=` | gte | Greater than or equal to. Evaluates to true if the left operand is greater or equal to the right operand. | | `<` | lt | Less than. Evaluates to true if the left operand is lower than the right operand. | | `<=` | lte | Less than or equal to. Evaluates to true if the left operand is lower or equal to the right operand. | The shorthand version of the comparison operators are usable in HubL filters that involve testing an expression such as [`|selectattr()`](/reference/cms/hubl/filters#selectattr). ### Logical Logical operators allow you to implement boolean expressions, as well as combine multiple expressions into single statements. ```hubl Two non-empty strings: {{ "a" and "b" }} Empty string and non-empty string: {{ "" and "b" }} Two non-zero numbers: {{ 1 and 2 }} Zero and non-zero number: {{ 0 and 1 }} Two non-empty lists: {{ [1] and [2] }} Empty list and non-empty list: {{ [] and [2] }} Two non-empty dicts: {{ {a: 1} and {b: 2} }} Empty dict and non-empty dict: {{ {} and {b: 2} }} ``` ```hubl Two non-empty strings: {{ "a" or "b" }} Empty string and non-empty string: {{ "" or "b" }} Two non-zero numbers: {{ 1 or 2 }} Zero and non-zero number: {{ 0 or 1 }} Two non-empty lists: {{ [1] or [2] }} Empty list and non-empty list: {{ [] or [2] }} Two non-empty dicts: {{ {a: 1} or {b: 2} }} Empty dict and non-empty dict: {{ {} or {b: 2} }} ``` ```hubl Conditional using `is` operator: {% set string = "strings are a cat's best friend" %} {% if string is string_containing "cat" %} Meow {% else %} Woof {% endif %} Conditional using `not` operator: {% set string = "strings are a cat's best friend" %} {% if string is not string_containing "cat" %} Meow {% else %} Woof {% endif %} ``` ```hubl {% set my_num = 10 %} {% set my_variable = 5 %} {{ (my_num + 2) * my_variable }} ``` ```hubl {% set date = local_dt %} {{ date ? date : 'No date'}} {{ todays_date ? todays_date : 'No date'}} ``` | Symbol | Description | | --- | --- | | `and` | Returns `true` if both the left and right operand are truthy. Otherwise, returns `false`.

      This operator does not behave like the `and` operator in Python or the `&&` operator in JavaScript. Learn more about using `and` operators below. | | `or` | Returns the first operand if it is truthy. Otherwise, returns the second operand.

      This operator is equivalent to `or` in Python and || in JavaScript. Learn more about using `or` operators below. | | `is` | Joins two operands for an affirmative statement. | | `not` | Negates a statement, in conjunction with `is`. | | `(expr)` | Group an expression for the order of operations. For example, `(10 - 2) * variable`. | | `?` | The [ternary operator](/reference/cms/hubl/if-statements) can be used to quickly write conditional logic. Accepts 3 arguments: expression, true condition, false condition. Evaluates an expression and returns the corresponding condition. | **Using and/or operators** In HubL, the `or` operator behaves like the `or` operator in Python and the `||` operator in JavaScript. It will return the first operand if the expression evaluates as true, otherwise it will return the second operand. A common use case for the `or` operator is setting a fallback value when a variable value isn't defined. ```hubl Two non-empty strings: {{ "a" or "b" }} Empty string and non-empty string: {{ "" or "b" }} Defining a fallback value: {{ some_variable or "default value" }} ``` However, the `and` operator behaves differently than the `and` operator in Python and the `&&` operator in JavaScript. In HubL, `and` will always return a boolean value: when the expression evaluates as true, `true` is returned, otherwise it will return `false`. The Python and JavaScript operators, on the other hand, will return an operand value based on whether the statement evaluates as true or false. ```hubl Two non-empty strings: {{ "a" and "b" }} Empty string and non-empty string: {{ "" and "b" }} ``` In HubL, empty lists (`[]`) and empty dicts (`{}`) are considered falsy. This is equivalent to the behavior in Python, but different from JavaScript, where `[]` and `{}` are truthy. ```hubl Empty list and non-empty list: {{ [] or [2] }} Empty dict and non-empty dict: {{ {} and {b: 2} }} ``` ### Other HubL operators Below are other important HubL operators that can be used to perform various tasks. | Symbol | Description | | --- | --- | | `in` | Checks to see if a value is in a sequence. | | `is` | Performs an [expression test](/reference/cms/hubl/operators-and-expression-tests#expression-tests). | | | | Applies a filter. | | `~` | Concatenates values. | ## Expression tests Expression tests are various boolean conditions that can be evaluated by using logical operators. ### boolean Tests whether the object is boolean (in a strict sense, not in its ability to evaluate to a truthy expression). ```hubl {% set isActive = false %} {% if isActive is boolean %} isActive is a boolean {% endif %} ``` ```html isActive is a boolean ``` ### containing Tests whether a list variable has a value in it. ```hubl {% set numbers = [1, 2, 3] %} {% if numbers is containing 2 %} Set contains 2! {% endif %} ``` ```html Set contains 2! ``` ### containingall Tests if a list variable contains all of the values of another list. ```hubl {% set numbers = [1, 2, 3] %} {% if numbers is containingall [2, 3] %} Set contains 2 and 3! {% endif %} {% if numbers is containingall [2, 4] %} Set contains 2 and 4! {% endif %} ``` ```html Set contains 2 and 3! ``` ### defined Tests whether a variable is defined within the context of the template. While you can use this expression test, writing an if statement without any operators will default to checking whether or not the variable is defined. In the example below, a color module's color parameter is tested. If the color parameter had no value, the template would render a default black background color. If it is defined, it renders the background color set by the user. ```hubl {% color "my_color" color="#930101", export_to_template_context=True %} ``` ```html ``` ### divisibleby Tests whether an object is divisible by another number. For example, below a for loop is created that iterates through a list of types of animals. Each type of animal gets printed in a div, and every 5th div has different inline styling applied (width:100%). This concept could be applied to a blog where different markup is rendered for a certain pattern of posts. To learn more about for loops and loop.index, [check out this article](/reference/cms/hubl/loops). ```hubl {% set animals = ["lions", "tigers", "bears", "dogs", "sharks"] %} {% for animal in animals %} {% if loop.index is divisibleby 5 %} {% else %} {% endif %} {% endfor %} ``` ```html ``` ### equalto Tests whether a variable's value is equal to a constant or another variable. You can also use the operator `==` to do the same test. In the example below, the width of the blog posts is adjusted based on the total number of posts in the loop. The example output assumes there were 4 posts in the blog. ```hubl {% for content in contents %} {% if loop.length is equalto 2 %} {% elif loop.length is equalto 3 %} {% elif loop.length is equalto 4 %} {% else %} {% endif %} {% endfor %} ``` ```html ``` ### even Tests whether a numeric variable is an even number. The example below shows a simplified blog listing loop, where if the current iteration of the loop is even, a class of `even-post` is assigned to the post item div. Otherwise, a class of `odd-post` is assigned. ```hubl {% for content in contents %} {% if loop.index is even %} {% else %} {% endif %} {% endfor %} ``` ```html ``` ### float Tests whether a numeric variable is a floating-point number. ```hubl {% set quantity = 1.20 %} {% if quantity is float %} quantity is a floating point number {% endif %} ``` ```html quantity is a floating-point number ``` ### integer Tests whether a variable is an integer. ```hubl {% set quantity = 120 %} {% if quantity is integer %} Quantity is an integer {% endif %} ``` ```html Quantity is an integer ``` ### iterable Tests whether a variable can be looped through. This example checks a variable called `jobs` to see if it can be iterated through. Since the variable contains a list of jobs, the [if statement](/reference/cms/hubl/if-statements) would evaluate to `true`, and the loop would run. If the variable had contained a single value, the if statement would print that value with different markup instead. [Learn more about for loops](/reference/cms/hubl/loops). ```hubl {% set jobs = ["Accountant", "Developer", "Manager", "Marketing", "Support"] %} {% if jobs is iterable %}

      Available positions

        {% for job in jobs %}
      • {{ job }}
      • {% endfor %}
      {% else %}

      Available position

      {% endif %} ``` ```html

      Available positions

      • Accountant
      • Developer
      • Manager
      • Marketing
      • Support
      ``` ### lower Tests whether a string is lowercase. The example below uses an [unless statement](/reference/cms/hubl/if-statements#unless-statements) and a lower filter to ensure that a string of text entered into a text module is always lowercase. ```hubl {% module "my_text" path="@hubspot/text" label="Enter text", value="Some TEXT that should be Lowercase", export_to_template_context=True %} {% unless widget_data.my_text.value is lower %} {{ widget_data.my_text.value|lower }} {% endunless %} ``` ```html some text that should be lowercase ``` ### mapping Tests whether an object is a dict (dictionary). The example below is checking to see if the contact object is a dictionary. ```hubl {% if contact is mapping %} This object is a dictionary. {% else %} This object is not a dictionary. {% endif %} ``` ```html This object is a dictionary. ``` ### none Tests whether a variable has a `null` value. ```hubl {% module "user_email" path="@hubspot/text" label="Enter user email", value="example@hubspot.com", export_to_template_context=True %} {% unless widget_data.user_email.value is none %} {{ widget_data.user_email.value }} {% endunless %} ``` ```html example@hubspot.com ``` ### number Tests whether the value of a variable is a number. The example below checks a variable to see whether or not it is a variable, and if so it converts it into millions. ```hubl {% set my_var = 40 %} {% if my_var is number %} {{ my_var * 1000000 }} {% else %} my_var is not a number. {% endif %} ``` ```html 40000000 ``` ### odd Tests whether a numeric variable is an odd number. Below is the same example as the inverse even expression test previously described. ```hubl {% for content in contents %} {% if loop.index is odd %} {% else %} {% endif %} {% endfor %} ``` ```html ``` ### sameas Tests whether or not two variables have the same value. The example below sets two variables and then checks to see whether or not they are the same. ```hubl {% set var_one = True %} {% set var_two = True %} {% if var_one is sameas var_two %} The variables values are the same. {% else %} The variables values are different. {% endif %} ``` ```html The variables values are the same. ``` ### sequence Similar to the **iterable** test, this expression test checks whether a variable is a sequence. The example below tests whether a variable is a sequence, then iterates through that sequence of musical genres. ```hubl {% set genres = ["Pop", "Rock", "Disco", "Funk", "Folk", "Metal", "Jazz", "Country", "Hip-Hop", "Classical", "Soul", "Electronica" ] %} {% if genres is sequence %}

      Favorite genres

        {% for genre in genres %}
      • {{ genre }}
      • {% endfor %}
      {% else %}

      Favorite genre:

      {% endif %} ``` ```html
      • Pop
      • Rock
      • Disco
      • Funk
      • Folk
      • Metal
      • Jazz
      • Country
      • Hip-Hop
      • Classical
      • Soul
      • Electronica
      ``` ### string Tests whether the value stored in a variable is text. The example below checks whether a variable is a string, and if so it applies a title filter to change the capitalization. ```hubl {% set my_var = "title of section" %} {% if my_var is string %} {{ my_var|title }} {% else %} my_var is not a string {% endif %} ``` ```html Title Of Section ``` ### string_containing Tests whether a provided substring is contained within another string. This expression test is used in conjunction with the `is` operator. ```hubl {% if content.domain is string_containing ".es" %} Markup that will only render on content hosted on .es domains {% elif content.domain is string_containing ".jp" %} Markup that will only render on content hosted on .jp domains {% else %} Markup that will render on all other domains {% endif %} ``` ```html Markup that will render on all other domains ``` ### string_startingwith Tests whether a string starts with a particular string. It is used in conjunction with the `is` operator. ```hubl {% if content.slug is string_startingwith "es/" %} Markup that will only render on content hosted in a /es/ subdirectory {% elif content.slug is string_startingwith "jp/" %} Markup that will only render on content hosted in a /jp/ subdirectory {% else %} Markup that will render on all subdirectories {% endif %} ``` ```html Markup that will render on all subdirectories ``` ### truthy Tests whether an expression evaluates to `True`. The example below uses a boolean checkbox module to display an alert message. ```hubl {% boolean "check_box" label="Show alert", value=True, export_to_template_context=True %} {% if widget_data.check_box.value is truthy %} {% endif %} ``` ```html ``` ### undefined Tests whether a variable is undefined in the context of the template. This test is different from the `none` expression test in that undefined will be `true` when the variable is present but has no value; whereas, none will be `true` when the variable has a null value. The example below checks a template for the existence of the variable "my_var". ```hubl {% if my_var is undefined %} A variable named "my_var" does not exist on this template. {% else %} {{ my_var }} {% endif %} ``` ```html A variable named "my_var" does not exist on this template. ``` ### upper Tests whether a string is all uppercase. Below is an inverse example of the `lower` expression test [above](#lower). ```hubl {% module "my_text" path="@hubspot/text" label="Enter text", value="Some TEXT that should be Uppercase", export_to_template_context=True %} {% unless widget_data.my_text.value is upper %} {{ widget_data.my_text.value|upper }} {% endunless %} ``` ```html SOME TEXT THAT SHOULD BE UPPERCASE ``` ### within Tests whether a variable is present within a list. ```hubl {% set numbers = [1, 2, 3] %} {% if 2 is within numbers %} 2 is in the list! {% endif %} {% if 4 is within numbers %} 4 is in the list! {% endif %} ``` ```html 2 is in the list! ``` # HubL Syntax @ HubL is the jinja inspired templating language used for building templates and modules on the HubSpot CMS. If statements, for loops, includes, etc. $ reference & cms/hubl --- # HubL syntax overview HubSpot’s CMS uses the HubSpot Markup Language, referred to as HubL (pronounced “Hubble”). HubL is HubSpot’s extension of [Jinjava](https://github.com/HubSpot/jinjava), a templating engine based on [Jinja](https://jinja.palletsprojects.com/en/latest/). HubL uses a fair amount of markup that is unique to HubSpot and does not support all features of Jinja. This article will take you through the basics of HubL's features and syntax. ## Types of delimiters Similar to other commonly used templating languages such as PHP, HubL can be mixed into your HTML in coded templates files or HubL template modules. In order to differentiate, where your HubL starts and ends, you will need to learn a few key symbols that act as delimiters. ```hubl {% %} - statement delimiters {{ }} - expression delimiters {# #} - comment delimiters ``` Be mindful of nesting comments in your code, as it can result in the trailing comment tag to render as text. ### Statements HubL statements are used to create editable modules, define conditional template logic, set up for loops, define variables, and more. Statements are delimited by `{%`. They do not print anything to the page. ### Expressions Expressions print values stored in the context of the template. Expressions are delimited by `{{`. For example, a variable must be defined as a statement, but then a HubL expression would be used to print the variable. #### Do tag The 'do' tag works exactly like the regular statements `{% ... %}`; This can be used to modify lists and dictionaries. Note: When adding to arrays, you want to use [`.append()`](/reference/cms/hubl/functions#append) and when adding to objects, you want to use [`.update()`](/reference/cms/hubl/functions#update) ```hubl # Arrays {% set navigation = ["Home", "About"] %} {% do navigation.append("Contact Us") %} {{navigation}} # Objects {% set book = {"name" : "Rocking HubL", "author" : "John Smith" }%} {% do book.update({"ebook" : "yes"}) %} {{book}} ``` ```html # Arrays [Home, About, Contact Us] # Objects {name=Rocking HubL, author=John Smith, ebook=yes} ``` ### Comments The final type of delimiter that you may encounter or decide to employ while developing with HubL, is a HubL comment. Comments are defined by a `{#`. ## Modules [Modules](/guides/cms/content/modules/overview) are dynamic areas of a template that can be customized by the end user in the content editor. For example, if you were coding a template file from scratch, you would add modules to templates with HubL tags, to give your content creators the ability to edit areas of the page. Module tags are made up of the following components: - **Type of module:** specifies which module to render. Please refer to the [HubL Supported Tags](/reference/cms/hubl/tags/standard-tags) page for a listing of the different types of modules available. - **A unique name for that module:** gives the module a unique identity in the context of the template. - **Path:** depending on the tag, defines the location of where the module is in the design manager. The path for HubSpot default modules should always start with @hubspot/ followed by the type of module. See the example below and the [using modules in templates page](/reference/cms/modules/using-modules-in-templates) for more. - **Parameters:** optionally specify additional module information. ```hubl {% module "unique_module_name", path="@hubspot/text", label="Single Text Line", value="This is a single text line" %} ``` ```html
      This is a single text line
      ``` The example above is a text module with the label and value parameters defined. The label defines the help text in the editor and value sets the default text for that module. See the sample gif below for how this looks inside of the editor. You can [learn more about using modules in templates, here](/reference/cms/modules/using-modules-in-templates). ## Variables and macros HubL includes many [predefined HubSpot variables](/reference/cms/hubl/variables) that print their helpful values from the app. In addition, you can [define your own variables in a template](/reference/cms/hubl/variables-macros-syntax). In the following example, a variable named `primaryColor` is defined in a statement and then printed with a HubL expression. This example mixes the HubL variable with CSS. ```hubl {% set primaryColor = '#F7761F' %} {# defines variable and assigns HEX color #} a { color: {{ primaryColor }}; {# prints variable HEX value #} } ``` ```css a { color: #f7761f; } ``` HubL macros allow you to print multiple statements with a dynamic value. This technique proves useful when you find yourself writing the same basic code blocks over and over, but only need to change certain values. In the following example, a macro is used to print a CSS3 transition property that includes all the vendor prefixes. You can [learn more about macros, here](/reference/cms/hubl/variables-macros-syntax#macros). ```hubl {% macro trans(value) %} -webkit-transition: {{value}}; -moz-transition: {{value}}; -o-transition: {{value}}; -ms-transition: {{value}}; transition: {{value}}; {% endmacro %} a { {{ trans("all .2s ease-in-out") }} } ``` ```css a { -webkit-transition: all 0.2s ease-in-out; -moz-transition: all 0.2s ease-in-out; -o-transition: all 0.2s ease-in-out; -ms-transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out; } ``` ## Filters and functions Filters can be added to your HubL to transform or alter the value of a template variable. A simple example displayed below is formatting a date variable. Filters use a | (pipeline symbol) and are applied without spaces to a variable. In the example below, assume a blog post was published on the 29th of April. The publish date of the post is formatted with a `datetimeformat` filter. [You can view a full list of filters here.](/reference/cms/hubl/filters) ```hubl {{ content.publish_date_local_time|datetimeformat("%B %e, %Y") }} ``` ```html April 29, 2020 ``` While filters affect how variables render, functions process value and account information and render that info. For example, a function can be used to get the total number of posts for a particular blog or to lighten/darken a color variable by a particular amount. The example would print the total number of blog posts from the designers.hubspot.com/blog. It uses a Blog ID (available in the URL of the blog dashboard) parameter to specify which blog to target. [You can view a full list of functions here](/reference/cms/hubl/functions). ```hubl // blog_total_post_count {{ blog_total_post_count(359485112) }} ``` ```html 2 ``` ## If statements [If statements](/reference/cms/hubl/if-statements) allow you to use conditional logic to dictate how your template will render conditional logic in HubL statements for `if`, `elif`, `else`, and `endif`. An if statement must begin with an `if` and end with an `endif`. The example below defines a choice module that lets the end user select a department from a dropdown. Depending on what the user selects in the editor, the template will generate a dynamic heading for a careers page. This example requires the use of the [`export_to_template_context`](/reference/cms/modules/export-to-template-context) parameter. ```hubl {% choice "department" label="Choose department", value="Marketing", choices="Marketing, Sales, Dev, Services" export_to_template_context=True %} {% if widget_data.department.value == "Marketing" %}

      Want to join our amazing Marketing team?

      We have exciting career opportunities on the {{ widget_data.department.value }} team.

      {% elif widget_data.department.value == "Sales" %}

      Are you a Sales superstar?

      We have exciting career opportunities on the {{ widget_data.department.value }} team.

      {% elif widget_data.department.value == "Dev" %}

      Do you love to ship code?

      We have exciting career opportunities on the {{ widget_data.department.value }} team.

      {% else %}

      Want to work with our awesome customers?

      We have exciting career opportunities on the {{ widget_data.department.value }} team.

      {% endif %} ``` ## For loops [For loops](/reference/cms/hubl/loops) allow you to iterate over items stored in a sequence. They will most commonly be used with rendering blog content in a listing format. For loops begin with a `for` statement and end with an `endfor` statement. In the example below, an array of types of bears is stored as a variable called `bears`. A for loop is then used to iterate through the different types of bears printing a list item for each type. ```hubl {% set bears = ["panda bear", "polar bear", "black bear", "grizzly bear", "koala bear"] %}

      Types of bears

        {% for bear in bears %}
      • {{ bear }}
      • {% endfor %}
      ``` ```html

      Types of bears

      • panda bear
      • polar bear
      • black bear
      • grizzly bear
      • koala bear
      ``` ## Other HubL features Below are a few other miscellaneous templating features that may be useful, as you develop with HubL. ### Escaping HubL delimiters Many other languages share the same delimiters as HubL, which can create issues when working in coded files on the CMS. If you want to use a HubL delimiter for a different language, you need to wrap that code in: ```hubl {{"Code you want to escape"}} ``` ```html {{"Code you want to escape"}} ``` ### Including files in files You can include multiple `.html` files in one [HubL template](/guides/cms/content/templates/types/html-hubl-templates) using the [include tag](/guides/cms/content/templates/types/html-hubl-templates#standard-partials). In order to create an HTML file that does not require the standard template variables, you must uncheck the option "Make template available for new content." The syntax is displayed below: ```hubl {% include "custom/page/web_page_basic/my_footer.html" %} {% include "hubspot/styles/patches/recommended.css" %} ``` You can also compile multiple CSS files into a single CSS file with the same include tag. When you publish the parent file, the child file will be compiled into a single minified CSS file with the parent's code. ### Blocks and extends When creating complex templates, you can use create compartmentalized blocks that extend a parent template. First, you'll create a main template that includes the required [`standard_header_includes`](/reference/cms/hubl/variables#required-page-template-variables) and [`standard_footer_includes`](/reference/cms/hubl/variables#required-page-template-variables) variables. Within that template, you need to define a unique block using the following syntax where `my_sidebar` is a unique name: ```hubl {% extends "custom/page/web_page_basic/my_template.html" %} {% block my_sidebar %}

      Sidebar title

      • Bullet 1
      • Bullet 2
      • Bullet 3
      {% endblock %} ``` Next, you can create a child HTML file that will populate that block. First, you must declare an [extends statement](/guides/cms/content/templates/types/html-hubl-templates#blocks-and-extends) that references the path to the parent. This block of code would be rendered in the parent template but maintained in another smaller and more manageable file. This technique is not for everyone but can be useful to stay organized when coding complex email or page templates. When using this technique, you should choose the child template, when creating content. ### Copy section HubL In the page editor, you can copy the HubL markup for a [drag and drop section](/guides/cms/content/templates/drag-and-drop/sections) to reuse the code as needed. This can be helpful when wanting to recreate a drag and drop section in a coded file. Learn more about [copying section HubL.](/guides/cms/content/templates/drag-and-drop/sections#copy-section-hubl) # Quote template variables @ Use HubL to access and display CRM data within your Custom Quote Template. The template_data object holds most of the data associated to the quote. For anything not found there you can either associated the object record to the quote, or use the HubL CRM_object functions. $ reference & cms/hubl --- # Quote template variables Custom quote templates can access quote data and some associated objects directly from the templates. The data available depends on data you have in your CRM, as well as data added to the quote itself. While developing a quote template, you can use HubSpot-provided mock data to populate the template, which may help for previewing the template. In the `@hubspot` folder, navigate to the `cms-quotes-theme` folder. Within the `templates` folder, view the `basic.html`, `modern.html`, or `original.html` templates. These templates contain the following code block at the top: ```hubl {% from "../imports/mock_data.html" import SAMPLE_TEMPLATE_DATA as mock_data %} {% from "../imports/module_defaults.html" import MODULE_DEFAULTS as module_defaults %} {% set QUOTE = template_data.quote || mock_data.quote %} {% set CURRENCY = QUOTE.hs_currency || "USD" %} {% set LOCALE = QUOTE.hs_locale || "en-US" %} {% set ASSOCIATED_OBJECTS = QUOTE.associated_objects %} {% set LINE_ITEMS = ASSOCIATED_OBJECTS.line_items %} {% set ADDITIONAL_FEES = ASSOCIATED_OBJECTS.additional_fees %} {% set TOTALS = ASSOCIATED_OBJECTS.totals || ASSOCIATED_OBJECTS.totals %} {% set QUOTE_TOTAL = TOTALS.total %} {% set SUB_TOTALS = TOTALS.subtotals %} {% set DEAL = ASSOCIATED_OBJECTS.deal %} ``` The mock data is first imported from the `mock_data.html` file, then is set to the `QUOTE` variable to use the data found in `template_data` if available. The `QUOTE` variable is also used to populate the other variables in this list, such as `ASSOCIATED_OBJECTS`, to make accessing that data less verbose. However, you can structure your data differently, depending on your preferences. In the above code, you'll notice that `template_data` is also used to set the main `QUOTE` variable. `template_data` is an object containing all of the actual data for the quote and deal in the page. If that object is not found in the template, HubSpot loads the data from `mock_data.html` instead. ## Template_data object The vast majority of the data can be directly accessed through the `template_data` object. You can use `{{ template_data|pprint }}` in your template to see the full object provided. | Variable | Type | Description | | --- | --- | --- | | `template_data` | dict | A dict containing the quote, quote.associated_objects, and totals dicts. | ## Quote variables The information specific to this individual quote. | Variable | Type | Description | | --- | --- | --- | | `template_data.quote` | dict | Dict containing all of the data for the quote itself. | | `template_data.quote.associated_objects.deal.hs_object_id` | Integer | Deal Id | | `template_data.quote.hubspot_owner_id` | Integer | Deal owner id | | `template_data.quote.hs_all_owner_ids` | integer or array of integers | Deal owner ids | | `template_data.quote.hs_created_by_user_id` | Integer | User that created the quote. | | `template_data.quote.hs_lastmodifieddate` | datetime | Date the quote was last modified. In epoch format. | | `template_data.quote.hubspot_owner_assigneddate` | datetime | Date the quote was assigned an owner. In epoch format. | | `template_data.quote.hs_createdate` | datetime | Date and time the quote was created. In epoch format. | | `template_data.quote.hs_expiration_date` | datetime | Date quote expires. In epoch format. | | `template_data.quote.hs_title` | String | Quote Title | | `template_data.quote.hs_template_type` | String | "CUSTOMIZABLE_QUOTE_TEMPLATE" | | `template_data.quote.hs_slug` | String | URL slug for quote web page. | | `template_data.quote.hs_proposal_template_path` | String | Developer file system path to template. (includes file extension) | | `template_data.quote.hs_quote_amount` | String | Amount of money | | `template_data.quote.hs_currency` | String | Currency the quote amount is in in 3 character ISO 4217 currency code."USD" | | `template_data.quote.hs_language` | String | Language code"en" | | `template_data.quote.hs_locale` | String | Locale code"en-us" | | `template_data.quote.hs_terms` | String | Terms text provided by quote creator | | `template_data.quote.hs_sender_firstname` | String | First name of the person sending the quote. | | `template_data.quote.hs_sender_company_name` | String | Company name of the person sending the quote | | `template_data.quote.hs_sender_company_image_url` | String | Company logo for the person sending the quote. | | `template_data.quote.hs_status` | String | Status of the quote."APPROVAL_NOT_NEEDED" | | `template_data.quote.hs_primary_color` | string/hex color code | "#425b76" | | `template_data.quote.hs_quote_number` | String | Unique quote id number. | | `template_data.quote.hs_payment_enabled` | boolean | Use to test if payment fields need to be shown. | | `template_data.quote.hs_esign_enabled` | boolean | Use to test if esignature fields need to be shown. | **Can't find a variable you're looking for?** There are more variables you can access within `template_data`. Use `|pprint` to view them. Additionally some variables in quote associations may only be available based on the quote/deal. We will be iterating on this documentation to showcase and explain more of the data you have access to. Aside from pretty printing, you can view the mock data file within the cms-quote-theme, to see what is available and the structure it comes in. ## Associated objects In a quote template, you can access data from a quote's associated records, such as deals or companies, by using `associated_objects`. For example, you can add the logo from the quote recipient's associated company record to a quote by using the following code: ```hubl {% set company_avatar_url = template_data.quote.associated_objects.company.hs_avatar_filemanager_key %} {% if company_avatar_url %} {{ template_data.quote.associated_objects.company.name }} {% else %} {% endif %} ``` Only manually set logos will appear. Automatically detected logos will not appear to prevent unintentional logos from appearing on the quote template. The above code first sets a variable that searches for the quote's associated company's logo. Then, using an `if` statement, the template displays that logo, if available. If no logo has been manually set for the company, no logo is displayed. ## Custom Objects Custom object data can be displayed or used within a quote in a couple different ways. Because each custom object's structure may vary, you'll need to get specific properties based on how you've structured your custom object. The quote `template_data` by default has custom associated objects in it. For example, custom objects associated with deals are included. To access them, you can use the following code: ```hubl {% set quote_associated_custom_objects = template_data.quote.associated_objects.deal.associated_objects.custom_objects %} {{ quote_associated_custom_objects|pprint }} {# |pprint is useful for understanding the structure of the data, you can leave it off when outputting values for display. #} ``` Because custom objects are unique to each account, the mock data doesn't include an example custom object. This means that in the template preview in the design manager you may see an error or the custom object data simply won't display. You'll instead need to preview the template with your real CRM data, which you can do by creating a quote from the template. You can then access each custom object type by appending its custom object type ID formatted with underscores. For example: `template_data.quote.associated_objects.deal.associated_objects.custom_objects._2_2193031` You can also look up a custom object by using the [`crm_associations()`](/reference/cms/hubl/functions#crm-associations) function and [`crm_objects()`](/reference/cms/hubl/functions#crm-objects) functions. For example, if you wanted to look up a custom object associated with a deal, you could pass in data from `template_data`: ```hubl {% set quote_associated_object = crm_associations(template_data.quote.associated_objects.deal.hs_object_id, "USER_DEFINED", 152) %} {# 152 is an example of an association type id, you would need to use the appropriate id for your use-case. #} {{ quote_associated_object }} ``` ## Related Resources - [Custom quote templates](/guides/cms/content/templates/types/quotes) - [Getting started with the CMS quotes theme](https://developers.hubspot.com/docs/cms/guides/getting-started-from-the-cms-quotes-theme-beta) - [Create and use custom quote templates (from the sales, sales ops/manager perspective)](https://knowledge.hubspot.com/quotes/create-custom-quote-templates) # Deprecated HubL Supported Tags @ The following is a list of HubL supported tags that are deprecated. While these tags still operate as intended, newer tags have been created to replace them that are more streamlined and optimized. These new tags are indicated below. This page is for historical reference. $ reference & cms/hubl/tags --- # Deprecated HubL tags The following is a list of HubL supported tags that are deprecated. While these tags still operate as intended, newer tags have been created to replace them that are more streamlined and optimized. These new tags are indicated below. This page is for historical reference. ## Custom Widgets This tag has been replaced by [**custom module tag.**](/reference/cms/hubl/tags/standard-tags#custom-modules) ## Follow Me Follow me modules render icons that link to your various social media profiles. The icons that display are based upon your Social Settings. This tag is replaced by the newer Follow Me default module. ```hubl {% follow_me "follow_me" %} {% follow_me "follow_us" title='Follow Us', module_title_tag="h2" %} ``` ```html

      Follow Us

      ``` ## Global Widget A global widget is one which can be shared across template The tag has been replaced with the [custom module tag](/reference/cms/hubl/tags/standard-tags#custom-modules). ```hubl {% global_widget "facebook_fan_box" overrideable=False, label='Facebook Fan Box' %}" ``` ## Google search The Google Search Tag and modules are no longer available. This has been replaced by HubSpot's own native search. ## Image slider Generates a HubSpot image slider module. This slider module is based on [FlexSlider](http://flexslider.woothemes.com/). While you can create a slider module with standard module HubL syntax, If you want to predefine default slides using HubL, you must use block syntax. Both methods are shown below. This tag has been deprecated in favor of the [Gallery tag](/reference/cms/hubl/tags/standard-tags#gallery). ```hubl {% image_slider "image_slider" %} <-- Block syntax --> {% widget_block image_slider "crm_slider" sizing='static', only_thumbnails=False, transition='slide', caption_position='below', with_thumbnail_nav=False, lightbox=False, auto_advance=True, overrideable=True, description_text='', show_pagination=True, label='Image Slider', loop_slides=True, num_seconds=5 %} {% widget_attribute "slides" is_json=True %}[{"caption": "CRM Contacts App", "show_caption": true, "link_url": "http://www.hubspot.com/crm", "alt_text": "Screenshot of CRM Contacts", "img_src": "http://go.hubspot.com/hubfs/Contacts-View-1.png?t=1430860504240", "open_in_new_tab": true}, {"caption": "HubSpot CRM Contact Profile", "show_caption": true, "link_url": "http://www.hubspot.com/", "alt_text": "HubSpot CRM Contact Profile", "img_src": "http://cdn2.hubspot.net/hubfs/53/Contact-Profile.png?t=1430860504240", "open_in_new_tab": true}]{% end_widget_attribute %} {% end_widget_block %} ``` ```html ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `sizing` | Enumeration | Determines whether the slider changes sizes, based on the height of the slides. Possible values include: "static" or "resize" | `"static"` | | `only_thumbnails` | Boolean | Display images as thumbnails instead of a slider. | `False` | | `transition` | Enumeration | Sets the type of slide transition. Possible values include: "fade" or "slide" | `"slide"` | | `caption_position` | Enumeration | Affects positioning of caption on or below the slide. Possible values include "below" or "superimpose" | `"below"` | | `with_thumbnail_nav` | Boolean | Include thumbnails below slider for navigation (only_thumbnails must be False for this to be True) | `False` | | `lightbox` | Boolean | Displays thumbnail image in lightbox, when clicked (with_thumbnail_nav must be True for this to be True) | `False` | | `auto_advance` | Boolean | Automatically advance slides after the time set in num_seconds | `False` | | `show_pagination` | Boolean | Provide buttons below slider to randomly navigate among slides | `True` | | `label` | String | A label for this module, visible in the editor only | `"Image Slider"` | | `loop_slides` | Boolean | When True, continuously loop through slides | `True` | | `num_seconds` | Number | Time in seconds to pause between slides | `5` | | `slides` | JSON | A JSON list of the default caption, the link url, the alt text, the image src, and whether to open in a new tab. See block syntax above. | | # Drag and Drop Areas HubL Tags @ The HubL tags used to build drag and drop areas that allow developers to create sections of pages that support layout, stylistic and content changes directly within the content editors. $ reference & cms/hubl/tags --- # Drag and Drop Area HubL tags [Drag and drop areas](/guides/cms/content/templates/drag-and-drop/overview) allow developers to create sections of pages and global partials that support layout, stylistic and content changes directly within the content editors. See the [creating a drag and drop area tutorial](/guides/cms/content/templates/drag-and-drop/tutorial) for an introduction to setting up drag and drop areas. Drag and drop areas are based on a 12 column responsive grid. Drag and drop tags render markup with class names designating columns and rows. You'll need to add a stylesheet to target those class names. An example of layout styles you could implement can be found in the [HubSpot CMS Boilerplate](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/css/objects/_layout.css). Your stylesheet can be added to the template using [`{{ require_css() }}`](/reference/cms/hubl/tags/standard-tags#require-css). Drag and drop areas can't be used in [blog post](/guides/cms/content/templates/types/blog) and [email templates](/guides/cms/content/templates/overview#email) at this time. ## dnd_area A [drag and drop area](/guides/cms/content/templates/drag-and-drop/overview) is a container that makes a portion of the web page editable in terms of its structure, design, and content. The body of a `{% dnd_area %}` tag supplies the default content for the drag and drop area. Modules themselves cannot contain drag and drop areas. To provide content creators an interface for adding uniform content within a module, use [repeatable fields and groups](/reference/cms/fields/module-theme-fields#repeaters) instead. A `dnd_area` tag can contain the following parameters: | Parameter | Type | Description | | --------- | ------ | ---------------------------------------------------- | | `class` | String | A class added to the wrapping div of a dnd_area | | `label` | String | Used in the editor to label the area in the sidebar. | ```hubl {% dnd_area "unique_name", class="main" %} {% end_dnd_area %} ``` ```html ``` `dnd_area` tags can also contain `dnd_section` tags. A content creator can swap a page's template for another template of the same type, depending on whether it has [dnd_area](/reference/cms/hubl/tags/dnd-areas) tags. - Templates built with the visual drag and drop layout editor can be swapped for other drag and drop templates or coded templates with or without `dnd_area` tags. - Coded templates with _dnd_area_ tags can only be swapped for other coded templates with `dnd_area` tags. - Coded templates without `dnd_area` tags can only be swapped for other coded templates without `dnd_area` tags. ## dnd_section A [`{% dnd_section %}`](/guides/cms/content/templates/drag-and-drop/sections) is a top-level row, and must be nested within a `{% dnd_area %}` tag. [Sections can also be defined as a template](/guides/cms/content/templates/drag-and-drop/sections#section-templates), and then [included](/guides/cms/content/templates/drag-and-drop/sections#section-context) into a `dnd_area`, making them ideal for quickly scaffolding out a template. A `dnd_section` tag can contain the following parameters: | Parameter | Type | Description | | --- | --- | --- | | `background_color` | Dict | A dict which supports specifying a [background color](#background-color). Can also be provided as a string. | | `background_image` | Dict | A dict which supports specifying a [background image](#background-image). | | `background_linear_gradient` | Dict | A dict which supports specifying a [linear gradient background](#background-linear-gradient). | | `full_width` | Boolean | A boolean which determines if the section is intended to be full width or constrained by an inner container. | | `margin` | Dict | A dict which supports specifying margin values in `cm`, `mm`, `Q`, `in`, `pc`, `pt`, `px`, `em`, `ex`, `ch`, `rem`, `lh`, `vw`, `vh`, `vmin`, `vmax`, and `%`. When no unit of measure is provided, the default of `px` is applied. | | `max_width` | Integer | A pixel value which sets a content max-width on a wrapping dict. | | `padding` | Dict | A dict which supports specifying padding values in `cm`, `mm`, `Q`, `in`, `pc`, `pt`, `px`, `em`, `ex`, `ch`, `rem`, `lh`, `vw`, `vh`, `vmin`, `vmax`, and `%`. When no unit of measure is provided, the default of `px` is applied. | | `vertical_alignment` | String | Vertical alignment of child content. Available options include:
      • `TOP`
      • `MIDDLE`
      • `BOTTOM`
      | You can only use one background parameter per `dnd_section` tag. ```hubl {% dnd_section background_image={ "backgroundPosition": "MIDDLE_CENTER", "backgroundSize": "cover", "imageUrl": "https://example.com/path/to/image.jpg" }, margin={ "top": 32, "bottom": 32 }, padding={ "top": "1em", "bottom": "1em", "left": "1em", "right": "1em" }, max_width=1200, vertical_alignment="MIDDLE" %} {% end_dnd_section %} ``` ```html
      ``` `dnd_section` tags can also contain the following tags: - `dnd_column` - `dnd_module` ## dnd_column A [`{% dnd_column %}`](/guides/cms/content/templates/drag-and-drop/overview#column) is a vertical structural building block that occupies one or more layout columns defined by its parent row. This HubL tag must be nested within a `{% dnd_area %}` tag. A `dnd_column` tag can contain the following parameters: | Parameter | Type | Description | | --- | --- | --- | | `background_color` | Dict | A dict which supports specifying a [background color](#background-color). | | `background_image` | Dict | A dict which supports specifying a [background image](#background-image). | | `background_linear_gradient` | Dict | A dict which supports specifying a [linear gradient background](#background-linear-gradient). | | `margin` | Dict | A dict which supports specifying margin values in `cm`, `mm`, `Q`, `in`, `pc`, `pt`, `px`, `em`, `ex`, `ch`, `rem`, `lh`, `vw`, `vh`, `vmin`, `vmax`, and `%`. When no unit of measure is provided, the default of `px` is applied. | | `max_width` | Integer | A pixel value which sets a content max-width on a wrapping dict. | | `padding` | Dict | A dict which supports specifying padding values in `cm`, `mm`, `Q`, `in`, `pc`, `pt`, `px`, `em`, `ex`, `ch`, `rem`, `lh`, `vw`, `vh`, `vmin`, `vmax`, and `%`. When no unit of measure is provided, the default of `px` is applied. | | `vertical_alignment` | String | Vertical alignment of child content. Available options include:
      • `TOP`
      • `MIDDLE`
      • `BOTTOM`
      | You can only use one background parameter per `dnd_column` tag. ```hubl {% dnd_column offset=0, width=12, background_color={ r: 255, g: 0, b: 0, a: 1 }, margin={ "top": "1em", "bottom": "1em" }, %} {% end_dnd_column %} ``` ```html
      ``` A `dnd_column` tag can also contain `dnd_row`. ## dnd_row A [`{% dnd_row %}`](/guides/cms/content/templates/drag-and-drop/overview#row) is a horizontal structural building block that creates a nested 12-column layout grid in which columns and modules can be placed. This HubL tag must be nested within a `{% dnd_area %}` tag. A `dnd_row` tag can include the following parameters: | Parameter | Type | Description | | --- | --- | --- | | `background_color` | Dict | A dict which supports specifying a [background color](#background-color). | | `background_image` | Dict | A dict which supports specifying a [background image](#background-image). | | `background_linear_gradient` | Dict | A dict which supports specifying a [linear gradient background](#background-linear-gradient). | | `margin` | Dict | A dict which supports specifying margin values in `cm`, `mm`, `Q`, `in`, `pc`, `pt`, `px`, `em`, `ex`, `ch`, `rem`, `lh`, `vw`, `vh`, `vmin`, `vmax`, and `%`. When no unit of measure is provided, the default of `px` is applied. | | `max_width` | Integer | A pixel value which sets a content max-width on a wrapping dict. | | `padding` | Dict | A dict which supports specifying padding values in `cm`, `mm`, `Q`, `in`, `pc`, `pt`, `px`, `em`, `ex`, `ch`, `rem`, `lh`, `vw`, `vh`, `vmin`, `vmax`, and `%`. When no unit of measure is provided, the default of `px` is applied. | | `vertical_alignment` | String | Vertical alignment of child content. Available options include:
      • `TOP`
      • `MIDDLE`
      • `BOTTOM`
      | You can only use one background parameter per `dnd_row` tag. ```hubl {% dnd_row background_color={ r: 123, g: 123, b: 123, a: 1.0 }, margin={ "top": 20, "bottom": 200 }, padding={ "top": 20, "bottom": 200, "left": 20, "right": 20 } %} {% end_dnd_row %} ``` ```html
      ``` A dnd_row can also contain the following tags: - `dnd_column` - `dnd_module` ## dnd_module A [`{% dnd_module %}`](/guides/cms/content/templates/drag-and-drop/overview#module) is a [module](/guides/cms/content/modules/overview) wrapped within a div where layout, styles and content can be added. The module is specified by referencing its path, which can either be a HubSpot default module (using the `@hubspot/` namespace), or modules you have built, specifying their path within the design manager file tree. This HubL tag must be nested within a `{% dnd_area %}` tag. A `dnd_module` tag can contain the following parameters: | Parameter | Type | Description | | --- | --- | --- | | `path` | String | The path to a module. | | `horizontal_alignment` | String | Horizontal positioning, supports:`LEFT`, `CENTER`, `RIGHT` | | `offset` | Integer | The offset from 0 in the 12 column grid. | | `width` | Integer | The number of columns occupying the 12 column grid. | | `flexbox_positioning` | Have an old module which has a field name that matches one of the `dnd_module` parameters above? You can [pass default values through a fields parameter](/reference/cms/modules/using-modules-in-templates#passing-fields-that-have-dnd-associated-parameters), much like you would a field group. ```hubl {% dnd_module path="@hubspot/rich_text", offset=0, width=8, %} {% module_attribute "html" %}

      Hello, world!

      {% end_module_attribute %} {% end_dnd_module %} ``` ```html

      Hello, world!

      ``` ## Background There are a few ways to set backgrounds on column, section and row dnd elements, [`background_image`](#background-image), [`background_linear_gradient`](#background-linear-gradient), and [`background_color`](#background-color). ### background_color The column, section, and row dnd tags support background colors. You can set the default background color for a drag and drop element using `background_color`. This parameter is a string based parameter and can include the following formats outlined in the example below. ```hubl {% dnd_section %} // Hex Value (both 3 and 6 char length) {% dnd_column background_color="#F7F7F7" %} {% end_dnd_column %} {% dnd_column background_color="#FFF" %} {% end_dnd_column %} // Both RGB and RGBA {% dnd_column background_color="rgb(255,255,255)" %} {% end_dnd_column %} {% dnd_column background_color="rgba(0,0,0,.25)" %} {% end_dnd_column %} {% end_dnd_section %} ``` ### background_linear_gradient The column, section and row dnd elements support background linear gradients. You can set a default gradient using the `background_linear_gradient` parameter. The parameter expects a dict. Currently only supports two color stops. | Parameter | Type | Description | | --- | --- | --- | | `direction` | String | The direction of the gradient.
      • `to bottom`
      • `to top`
      • `to left`
      • `to right`
      | | `colors` | array | Array of color strings. Currently supports 2 values, the start and end. Values are provided as strings, and the following formats are supported:
      • `RGB`
      • `RGBA`
      • `3 char hex`
      • `6 char hex`
      • `8 char hex`
      | ```hubl {% dnd_section background_linear_gradient={ "direction": "to bottom", "colors": [ "#1EB6C3", "#2A2859" ] } %} {% dnd_module path="@hubspot/rich_text" width="6" %} {% end_dnd_module %} {% end_dnd_section %} ``` ### background_image The column, section and row dnd elements support background images. You can provide a default background image by using the `background_image` parameter which expects a dict. | Key | Type | Description | | --- | --- | --- | | `backgroundPosition` | String | The background position of the image. Supports a string indicating vertical position followed by horizontal.
      • `TOP_LEFT`
      • `TOP_CENTER`
      • `TOP_RIGHT`
      • `MIDDLE_LEFT`
      • `MIDDLE_CENTER`
      • `MIDDLE_RIGHT`
      • `BOTTOM_LEFT`
      • `BOTTOM_CENTER`
      • `BOTTOM_RIGHT`
      | | `backgroundSize` | String | The CSS background size property used for the image.
      Supported values are:
      • `cover`
      • `contain`
      • `auto`
      | | `imageUrl` | String | Absolute URL to the image. | ```hubl {% dnd_section background_image = { "backgroundPosition": "MIDDLE_CENTER", "backgroundSize": "cover", "imageUrl": "https://www.example.com/bg-image.jpg" }, %} {% dnd_module path="@hubspot/rich_text" width="6" %} {% end_dnd_module %} {% end_dnd_section %} ``` ## How dnd style parameters translate to the page When you are using style based parameters such as [backgrounds](#background), margins, or padding, the class names are automatically computed for your sections, columns, rows, and modules. The property values you have assigned are then added to those automatically created class names and the resulting CSS code is then placed before the closing `` tag on the page in a ` ``` | Parameter | Type | Description | | --- | --- | --- | | `module_id` | String | The id of the module to render. | | `path` | String | The path of the module to render. Include leading slash for absolute path, otherwise path is relative to template. Reference HubSpot default modules with paths corresponding to their HubL tags such as @hubspot/rich_text, @hubspot/linked_image, etc. | ## Editor placeholders To render placeholder content for a module in the editor, you can either add default content to module fields or use the `editor_placeholder` HubL tag. This can be useful when the module doesn't have or need default content, or to streamline module building. To add an editor placeholder to a custom module, first add an [if statement](/reference/cms/hubl/if-statements) to the module's HTML to render the placeholder when there's no content selected in the editor. For example, the following code could be used to add an editor placeholder to a CTA module: ```hubl {% if module.label %} {% cta guid="{{ module.guid }}" label="my_cta" %} {% elif is_in_editor %} {% editor_placeholder %} {% endif %} ``` The first `if` statement identifies whether the module is present. Then, the `elif` statement identifies if the module is being rendered in the context of the editor using the `is_in_editor` variable. This variable returns `true` if the content is being rendered in any content editor, but you can be more specific with other [in-app editor and preview variables](/reference/cms/hubl/variables#in-app-editor-and-preview-variables). Then, define the placeholder content in the module's `meta.json` file. ```json { "global": false, "host_template_types": ["PAGE"], "module_id": 62170380654, "is_available_for_new_content": true, "placeholder": { "show_module_icon": true, "title": "Call to action", "description": "Select a CTA" } } ``` | Parameter | Type | Description | | --- | --- | --- | | `show_module_icon` | Boolean | Whether to display the module's icon. | | `title` | String | The title that appears in the placeholder. | | `description` | String | The description that appears in the placeholder. | ## Flexible column Flexible columns are vertical columns in a template that enable content creators to insert and remove modules to the page using the [content editor](https://knowledge.hubspot.com/website-pages/edit-page-content-in-a-drag-and-drop-area). When coding a flexible column with HubL, you can choose to wrap other HubL modules to make them appear in the flexible column by default. The sample code below shows the basic syntax and a sample flexible column with a rich text and form module contained as default content. Please note that flexible columns can only be added to page templates, not blog or email templates. Modules cannot contain flexible columns, but they can instead contain [repeatable fields and groups](/reference/cms/fields/module-theme-fields#repeaters), which provide a similar functionality. ```hubl {% widget_container "my_flexible_column" label="My flex column"%} {% module "rich_text" path="@hubspot/rich_text" %} {% module "linked_image" path="@hubspot/linked_image" %} {% end_widget_container %} ``` ```html

      Something Powerful

      Tell The Reader More

      The headline and subheader tells us what you're offering, and the form header closes the deal. Over here you can explain why your offer is so great it's worth filling out a form for.

      Remember:

      • Bullets are great
      • For spelling out benefits and
      • Turning visitors into leads.
      placeholder_200x200
      ``` When using this tag, the `label` must follow the `name` value for the flexible column to function in the content editor. For example, the following syntax is invalid: `[% widget_container label="My label" "my_flexible_column" %}` ## Form Allows users to select a HubSpot form to add to their page. The code below includes an example of using the standard HubL tag syntax and an example of using [block syntax](/reference/cms/modules/using-modules-in-templates#block-syntax). ```hubl {% form "my_form" form_to_use="{formID}", title="Free Trial" %} {% module_block form "my_form" form_follow_ups_follow_up_type="", form_to_use="9e633e9f-0634-498e-917c-f01e355e83c6", title="Free Trial", notifications_are_overridden=True, response_message="Thanks for submitting the form.", response_type="inline", overrideable=True, gotowebinar_webinar_key="", webinar_id="", webinar_source"", label="Form" %} {% module_attribute "notifications_override_email_addresses" is_json=True %} ["email_address@website.com"] {% end_module_attribute %} {% end_module_block %} ``` ```html

      Free Trial

      ``` | Parameter | Type | Description | | --- | --- | --- | | `form_key` | String | Specifies a unique ID for the form at the page level. | | `form_to_use` | String | Specifies which form to load by default, based on the Form ID. This ID is available in the form editor URL of a each form. | | `title` | String | Populates an `h3` header tag above the form. | | `no_title` | Boolean | If `True`, the h3 tag above the title is removed. | | `form_follow_ups_follow_up_type` | Enumeration | Specifies follow up actions such as enrolling a contact into a workflow or sending a simple follow up email. Possible values include: `no_action`, `simple`, and `automation`. | | `simple_email_for_live_id` | Number | Specifies the ID of the simple follow-up email for the live page. | | `simple_email_for_buffer_id` | Number | Specifies the ID of the simple follow-up email for the auto-save version of a page. | | `follow_up_type_simple` | Boolean | If true, enables a simple follow-up email. Alternative to `form_follow_ups_follow_up_type`. | | `follow_up_type_automation` | Boolean | If true, enrolls submissions in a workflow. Alternative to `form_follow_ups_follow_up_type`. | | `simple_email_campaign_id` | Number | Specifies the ID of the simple follow-up email. Alternative to `simple_email_for_live_id`. | | `form_follow_ups_workflow_id` | Number | Specifies the ID of the workflow in which to enroll submissions. | | `response_redirect_url` | String | If redirecting to an external page, this parameter specifies the URL to redirect to. | | `response_redirect_id` | Number | If redirecting to HubSpot hosted page, this parameter specifies the page ID of that page. The page ID is available in the page editor URL of each page. | | `response_response_type` | Enumeration | Determines whether to redirect to another page or to display an inline thank you message on submission. The value of this parameter should either be `"redirect"` or `"inline"` (default). | | `response_message` | String | Sets an inline thank you message. This parameter supports HTML. | | `notifications_are_overridden` | Boolean | If set to `True`, the form will send form notifications to specified email addresses selected in the `notifications_override_email_addresses` parameter, instead of the form defaults. Default is `False`. | | `notifications_override_guid_buffer` | String | ID of override settings in auto-save version of page. | | `notifications_override_guid` | String | ID of override settings in live version of page. | | `notifications_override_email_addresses` | JSON | Block syntax supports a JSON list of email recipients that will be notified upon form submission. These email addresses will override the email notification settings set in the form. | | `sfdc_campaign` | String | Specifies the Salesforce campaign to enroll contacts who submit the form into. This parameter's value should be the SFDC campaign ID and is only available for accounts that are [integrated with Salesforce](https://knowledge.hubspot.com/salesforce/install-the-hubspot-salesforce-integration). | | `gotowebinar_webinar_key` | String | Specifies the GoToWebinar webinar to enroll contacts into after submitting the form. Only available for accounts using the [GoToWebinar integration](https://knowledge.hubspot.com/integrations/use-the-gotowebinar-integration-with-hubspot). | | `webinar_id` | String | A more generic field that specifies the ID of the Microsoft Teams or GoToWebinar webinar to enroll contacts into after submiting the form. Only available in accounts using the [GoToWebinar](https://knowledge.hubspot.com/integrations/use-the-gotowebinar-integration-with-hubspot) or [Microsoft Teams](https://knowledge.hubspot.com/integrations/connect-hubspot-and-microsoft-teams) integrations, and when `support_all_webinar_types` is set to `true` in the [form fields definition](/reference/cms/fields/module-theme-fields#form). | | `webinar_source` | String | The ID of the integration app, either GoToWebinar (`35161`) or Microsoft Teams (`221635`). | ## Footer Renders copyright information with the year and company name specified in the account's [marketing email settings](https://knowledge.hubspot.com/marketing-email/can-i-customize-my-can-spam-email-footer#edit-your-default-email-footer-information-in-your-settings). ```hubl {% page_footer "page_footer" %} ``` ```html
      © 2020 HubSpot
      ``` ## Gallery Generates a HubSpot gallery tag. This gallery tag is based on [Slick](http://kenwheeler.github.io/slick/). While you can create a gallery module with standard module HubL syntax, If you want to predefine default slides using HubL, you must use block syntax. Both methods are shown below. Gallery images are lazy loaded using JavaScript. ```hubl {% gallery "crm_gallery" %} <-- Block syntax --> {% widget_block gallery "Gallery" display_mode="standard" sizing="static", transition="slide", caption_position="below", auto_advance=True, overrideable=True, description_text="", show_pagination=True, label="Gallery", loop_slides=True, num_seconds=5 %} {% widget_attribute "slides" is_json=True %} [{ "caption": "CRM Contacts App", "show_caption": true, "link_url": "http://www.hubspot.com/crm", "alt_text": "Screenshot of CRM Contacts", "img_src": "http://go.hubspot.com/hubfs/Contacts-View-1.png?t=1430860504240", "open_in_new_tab": true }, { "caption": "HubSpot CRM Contact Profile", "show_caption": true, "link_url": "http://www.hubspot.com/", "alt_text": "HubSpot CRM Contact Profile", "img_src": "http://cdn2.hubspot.net/hubfs/53/Contact-Profile.png?t=1430860504240", "open_in_new_tab": true }] {% end_widget_attribute %} {% end_widget_block %} ``` ```html ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `slides` | JSON | A JSON list of the default caption, the link url, the alt text, the image src, and whether to open in a new tab. See block syntax above. | | | `loop_slides` | Boolean | When True, continuously loop through slides. | `True` | | `num_seconds` | Number | Time in seconds to pause between slides. | `5` | | `show_pagination` | Boolean | Provide buttons below slider to navigate among slides. | `True` | | `sizing` | Enumeration | Determines whether the slider changes sizes, based on the height of the slides. Possible values include: "static" or "resize". | `"static"` | | `auto_advance` | Boolean | Automatically advance slides after the time set in num_seconds. | `False` | | `transition` | Enumeration | Sets the type of slide transition. Possible values include: "fade" or "slide". | `"slide"` | | `caption_position` | Enumeration | Affects positioning of caption on or below the slide. Possible values include "below" or "superimpose". | `"below"` | | `display_mode` | Enumeration | Determines how the image gallery will be displayed. Possible values include: "standard", "lightbox", "thumbnail". | `"standard"` | | `lightboxRows` | Number | If "display_mode" is set to "lightbox", this parameter will control the number of rows displayed within the lightbox. | `3` | ## Header Generates a header module that will render text as an h1-h6 tag. ```hubl {% header "header" %} {% header "my_header" header_tag="h1", overrideable=True, value="A clear and bold header", label="Header" %} ``` ```html

      A clear and bold header

      ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `header_tag` | String | Select which heading tag to render. Possible values include: h1, h2, h3, h4, h5, h6. | `h1` | | `value` | String | Renders default text within the heading module. | `"A clear bold header"` | ## Icon Adds an icon tag that allows users to select and icon for use. Supported icons sets are FontAwesome [5.0.10](https://github.com/FortAwesome/Font-Awesome/releases/tag/5.0.10), [5.14.0](https://github.com/FortAwesome/Font-Awesome/releases/tag/5.14.0), and [6.4.2](https://github.com/FortAwesome/Font-Awesome/releases/tag/6.4.2). This tag cannot be used in modules enabled for email. ```hubl {% icon name="Accessible Icon" style="REGULAR" unicode="f368" icon_set="fontawesome-5.14.0" purpose="decorative" title="Accessible Icon" %} ``` ```html ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `name` | String | Name of the icon. | | | `style` | String | Style of the icon. Possible values: `REGULAR` or `SOLID` | `REGULAR` | | `unicode` | String | The Unicode character representation of the icon. | | | `icon_set` | String | The FontAwesome icon set to use. Possible values are:
      • `fontawesome-5.14.0`
      • `fontawesome-5.0.10`
      • `fontawesome-6.4.2`
      | | | `purpose` | String | The purpose of the icon, used for [accessibility](https://docs.fontawesome.com/v5/web/other-topics/accessibility). Possible values are `decorative` or `semantic`. If set to `decorative`, an additional attribute of `aria-hidden="true"` will be added to the icon. | `decorative` | | `title` | String | The title element of the icon's svg, along with a `labelledby` attribute that points to the title. | | ## Image Creates a image tag that allows users to select an image from the content editor. If you want the image to be linked to a destination URL, you should use linked_image below. ```hubl {% image "image" %} {% image "executive_image" label="Executive photo", alt="Photo of Brian Halligan", src="//cdn2.hubspot.net/hub/53/file-733888619-jpg/assets/hubspot.com/about/management/brian-home.jpg", width="300" %} ``` ```html Photo of Brian Halligan ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `alt` | String | Sets the default alt text for the image. | | | `src` | String | Populates the src attribute of the img tag. | | | `width` | Number | Sets the width attribute of the img tag. | `The width of the image` | | `height` | Number | Sets a min-height in a style attribute of the img tag **for email templates only**. | `The height of the image` | | `hspace` | Number | Sets the hspace attribute of the img tag. | | | `align` | String | Sets the align attribute of the img tag. Possible values include: left, right, & center. | | | `style` | String | Adds inline CSS declarations to the img tag. For example style="border:1px solid blue; margin:10px" | | | `loading` | String | Controls img element loading attribute. [Used for browser based lazy loading.](/guides/cms/content/performance/lazy-loading) | | ## Image src An image src module creates a image selector in the content editor, but rather than printing a img tag, it renders the URL of the image. This tag is generally used with `no_wrapper=True` parameter, so that the image src can be added to inline CSS or other markup. An alternative to using this tag is to use the [`export_to_template_context`](/reference/cms/modules/export-to-template-context) parameter. ```hubl {% image_src "image_src" %} {% image_src "executve_image_src" src="//cdn2.hubspot.net/hub/53/file-733888614-jpg/assets/hubspot.com/about/management/dharmesh-home.jpg", no_wrapper=True %} ``` ```html //cdn2.hubspot.net/hub/53/file-733888614-jpg/assets/hubspot.com/about/management/dharmesh-home.jpg ``` | Parameter | Type | Description | | --------- | ------ | ------------------------------------ | | `src` | String | Specifies the default URL image src. | ## Language switcher Adds a Globe Icon with links to the translated versions of a given CMS page. Learn more about [multi-language content here](https://knowledge.hubspot.com/website-and-landing-pages/create-pages-in-multiple-languages). ```hubl {% language_switcher "language_switcher" overrideable=false, display_mode="localized", label="Language Switcher" %} ``` ```html ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `display_mode` | Enumeration | The language of the text in the language switcher. Values are:
      • `Pagelang` means the names of languages will display in the language of the page the switcher is on.
      • `Localized` means the name of each language will display in that language.
      • `Hybrid` is a combination of the two.
      | `Localized` | ## Linked image Creates a user-selectable image that is wrapped in a link. This tag has all of the parameters of an image module with two additional parameters that specify the link destination URL and whether the link opens in a new window. ```hubl {% linked_image "linked_image" %} {% linked_image "executive_image" label="Executive photo", link="https://twitter.com/bhalligan", \ open_in_new_tab=True, alt="Photo of Brian Halligan", src="//cdn2.hubspot.net/hub/53/file-733888619-jpg/assets/hubspot.com/about/management/brian-home.jpg", width="300" %} ``` ```html Photo of Brian Halligan ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `alt` | String | Sets the default alt text for the image. | | | `src` | String | Populates the src attribute of the img tag. | | | `width` | Number | Sets the width attribute of the img tag. | `The width of the image` | | `height` | Number | Sets a min-height in a style attribute of the img tag for **email templates only.** | `The height of the image` | | `hspace` | Number | Sets the hspace attribute of the img tag. | | | `align` | String | Sets the align attribute of the img tag. Possible values include: left, right, & center. | | | `style` | String | Adds inline CSS declarations to the img tag. For example style="border:1px solid blue; margin:10px" | | | `open_in_new_tab` | Boolean | Selects whether or not to open the destination URL in another tab. | `False` | | `link` | String | Sets the destination URL of the link that wraps the img tag. | | | `target` | String | Sets the target attribute of the link tag. | | | `loading` | String | Controls img element loading attribute. [Used for browser based lazy loading.](/guides/cms/content/performance/lazy-loading) | | ## Logo A logo tag renders your company's logo from the account's [brand kit settings](https://knowledge.hubspot.com/branding/edit-your-logo-favicon-and-brand-colors#edit-your-logo). ```hubl {% logo "logo" %} {% logo "my_logo" width="200" %} ``` ```html ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `alt` | String | Sets the default alt text for the image. | `Value in brand kit settings` | | `src` | String | Populates the src attribute of the img tag. | `Value in brand kit settings` | | `link` | String | Sets the destination URL of the link that wraps the img tag. | | | `width` | Number | Sets the width attribute of the img tag. | `The width of the image` | | `height` | Number | Sets a min-height in a style attribute of the img tag for **email templates only.** | `The height of the image` | | `hspace` | Number | Sets the hspace attribute of the `img` tag. | | | `align` | String | Sets the align attribute of the `img` tag. Possible values include: `left`, `right`, and `center`. | | | `style` | String | Adds inline CSS declarations to the img tag. For example:`style="border:1px solid blue; margin:10px"` | | | `suppress_company_name` | Boolean | Hides company name if an image logo isn't set. | `False` | | `use_account_default` | Boolean | Set to `true` to use the company name from the account's default settings. | `False` | | `open_in_new_tab` | Boolean | Selects whether or not to open the destination URL in another tab. | `False` | | `override_inherited_src` | Boolean | When set to `true`, use src from logo widget rather than src inherited from settings or template. | `True` | | `heading_level` | String | When using non-linked text-based logos, this wraps the text-based logo in one of the following available options as an HTML tag: `h1`, `h2`, `h3`, `h4`. | `h1` | | `loading` | String | Controls img element loading attribute for [browser based lazy loading.](/guides/cms/content/performance/lazy-loading) | | ## Menu Generates an advanced menu based on a menu tree in **Content Settings > Advanced Menus.** See [menus and navigation](/guides/cms/content/menus-and-navigation) for more information on using menus in [templates](/guides/cms/content/templates/overview) and [modules](/guides/cms/content/modules/overview). If `id` is set to `null` the menu tag will render the default menu for the HubSpot account. ```hubl {% menu "menu" %} {% menu "my_menu" id=456, site_map_name="Default", overrideable=False, root_type="site_root", flyouts="true", max_levels="2", flow="horizontal", label="Advanced Menu" %} ``` ```html ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `id` | Number | ID of Menu Tree from Advanced Menus in Content Settings. | | | `site_map_name` | String | Name of Menu Tree from Advanced Menus in Content Settings. | `"default"` | | `root_type` | Enumeration | Specifies the type of advanced menus. Options include: "site_root", "top_parent", "parent", "page_name", "page_id", and "breadcrumb". These values correspond to static, dynamic by section, dynamic by page, and breadcrumb. | `"site_root"` | | `flyouts` | String | When true, a class is added to the menu tree that can be styled to allow child menu items will appear when you hover over the parent. When false, child menu items will always appear. | `"true"` | | `max_levels` | Number | Determines how many levels of nested menus render in the markup. This parameter dictates number of menu tree children that can be expanded in the menu. | `2` | | `flow` | Enumeration | Sets orientation of menu items. This adds classes to menu tree, so that they can be styled accordingly. Possible values include "horizontal", "vertical" or "vertical_flyouts". Horizontal menus display items side-by-side, and vertical menus are top-to-bottom. | `"horizontal"` | | `root_key` | String | Used to find the menu root. When root_type is set to page_id or page_name, this param should be the page ID or the label of the page, respectively. | `"horizontal"` | | `label` | String | Some rich content to describe this entity | `False` | | `label` | String | Some rich content to describe this entity | `False` | ## Require_css A HubL tag that enqueues a style element to be rendered in the ``. This tag is similar to the [require_css function](/reference/cms/hubl/functions#require-css), except that this tag inserts styling inline rather than from a stylesheet. This tag also does not deduplicate against other instances of the CSS on the same page. If you're building a module and want to insert a stylesheet, but you might use that module multiple times on a single page, you may want to use the `require_css` function instead. ```hubl {{ standard_header_includes }} {% require_css %} {% end_require_css %} {{ standard_footer_includes }} ``` ```html ``` ## Require_head A HubL tag that enqueues anything placed inside of it into the `standard_header_includes` which is in the template's ``. For most Javascript and CSS see `require_js` and `require_css`. Some use-cases for `require_head` include supplying meta tags, and special link tags (like prefetch and preconnect) from modules. ```hubl {% require_head %} {% end_require_head %} ``` ## Require_js A HubL tag that enqueues a script element to be rendered. To enqueue a script to render in the ``from a different file via a ` {% end_require_js %} {{ standard_footer_includes }} ``` ```html ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `position` | String | Set the position where the inline script will be rendered. Options include: `"head"` and `"footer"`. | `"footer"` | ## Rich text Creates a WYSIWYG content editor. ```hubl {% rich_text "rich_text" %} " %} Block Syntax Example: {% widget_block rich_text "right_column" overrideable=True, label="Right Column" %} {% widget_attribute "html" %}

      Something Powerful

      Tell The Reader More

      The headline and subheader tells us what you're offering, and the form header closes the deal. Over here you can explain why your offer is so great it's worth filling out a form for.

      Remember:

      • Bullets are great
      • For spelling out benefits and
      • Turning visitors into leads.
      {% end_widget_attribute %} {% end_widget_block %} ``` ```html

      Something Powerful

      Tell The Reader More

      The headline and subheader tells us what you're offering, and the form header closes the deal. Over here you can explain why your offer is so great it's worth filling out a form for.

      Remember:

      • Bullets are great
      • For spelling out benefits and
      • Turning visitors into leads.
      ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `html` | String | Default rich text content for module. | `

      Something Powerful

      Tell The Reader More

      The headline and subheader tells us what you're offering, and the form header closes the deal. Over here you can explain why your offer is so great it's worth filling out a form for.

      Remember:

      • Bullets are great
      • For spelling out [benefits](#) and
      • Turning visitors into leads.
      ` | ## RSS listing Loads a list of content from an internal or external RSS feed. This module loads asynchronously on the client-side. As a result, if you want to manipulate the feed after it's loaded, you'll need to define a global JS function to handle that manipulation. Use the function `hsRssFeedComplete(feeds)`, where `feeds` is the jQuery selector on all feeds that have been completed. You can directly manipulate the DOM object in that function. ```hubl {% rss_listing "rss_listing" %} {% rss_listing "my_rss_listing" rss_url="", publish_date_text="posted at", feed_source={rss_url="", is_external=False, content_group_id="30808594297"}, click_through_text="Read more", show_date=True, include_featured_image=True, overrideable=False, publish_date_format="short", show_detail=True, show_author=True, number_of_items="3", is_external=False, title="", content_group_id="24732847", label="RSS Listing", limit_to_chars="200", attribution_text="by" %} ``` ```html

      ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `show_title` | Boolean | Shows or hides RSS feed title. | `True` | | `show_date` | Boolean | Displays post date. | `True` | | `show_author` | Boolean | Displays author name. | `True` | | `show_detail` | Boolean | Display post summary up to number of characters set by `limit_to_chars` parameter. | `True` | | `title` | String | Populates a heading above the RSS feed listing. | | | `limit_to_chars` | Number | Maximum number of characters to display in summary. | `200` | | `publish_date_format` | String | Format for the publish date. Possible values include `"short"`, `"medium"` and `"long"`. Also accepts custom formats including `"MMMM d, yyyy 'at' h:mm a"`. | `"short"` | | `attribution_text` | String | The text which attributes an author to a post. | `"by"` | | `click_through_text` | String | The text which will be displayed for the click through link at the end of a post summary. | `"Read more"` | | `publish_date_text` | String | The text which indicates when a post was published. | `"posted at"` | | `include_featured_image` | Boolean | Displays featured image with post link for HubSpot generated RSS feeds. | `False` | | `item_title_tag` | String | Specifies HTML tag of each post title. | `span` | | `is_external` | Boolean | RSS feed is from an external blog. | `False` | | `number_of_items` | Number | Maximum number of posts to display. | `5` | | `publish_date_language` | String | Specifies the language of the publish date. | `en_US` | | `rss_url` | String | The URL where the RSS feed is located. | | | `content_group_id` | String | ID for blog when feed source is internal blog. | | | `select_blog` | String | Can be used to select an internal HubSpot blog feed. | `default` | | `feed_source` | String | Set source for RSS feed. When internal, general format is `{rss_url="", is_external=False, content_group_id="2502431580"}`. When external, general format is `{rss_url="http://blog.hubspot.com/marketing/rss.xml", is_external=True}`. | | | `tag_id` | Number | ID for tag when feed source is internal blog. | | ## Section header Generates an html heading and `

      ` subheader. ```hubl {% section_header "section_header" %} {% section_header "my_section_header" subheader="A more subdued subheader", header="A clear and bold header", label="Section Header" %} ``` ```html ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `header` | String | Text to display in header. | `"A clear and bold header"` | | `subheader` | String | Text to display in subheader. | `"A more subdued subheader"` | | `heading_level` | String | The semantic HTML heading level. h1 to h6 are supported. | `"h1"` | ## Simple menu Simple menus allow you to create basic [navigation menus](/guides/cms/content/menus-and-navigation) that can be modified at the page level. Unlike regular menu modules, simple menus are not managed from the Navigation screen in Website Settings, but rather from the template and page editors. You can use [block syntax](/reference/cms/modules/using-modules-in-templates#block-syntax) to set up a default menu tree. ```hubl {% simple_menu "simple_menu" %} {% simple_menu "my_simple_menu" orientation="horizontal", label="Simple Menu" %} Block Syntax Example: {% widget_block simple_menu "block_simple_menu" overrideable=True, orientation="horizontal", label="Simple Menu" %} {% widget_attribute "menu_tree" is_json=True %}[{"contentType": null, "subCategory": null, "pageLinkName": null, "pageLinkId": null, "isPublished": false, "categoryId": null, "linkParams": null, "linkLabel": "Home", "linkTarget": null, "linkUrl": "http://www.hubspot.com", "children": [], "isDeleted": false}, {"contentType": null, "subCategory": null, "pageLinkName": null, "pageLinkId": null, "isPublished": false, "categoryId": null, "linkParams": null, "linkLabel": "About", "linkTarget": null, "linkUrl": "http://www.hubspot.com/internet-marketing-company", "children": [{"contentType": null, "subCategory": null, "pageLinkName": null, "linkUrl": "http://www.hubspot.com/company/management", "isPublished": false, "children": [], "linkParams": null, "linkLabel": "Our Team", "linkTarget": null, "pageLinkId": null, "categoryId": null, "isDeleted": false}], "isDeleted": false}, {"contentType": null, "subCategory": null, "pageLinkName": null, "pageLinkId": null, "isPublished": false, "categoryId": null, "linkParams": null, "linkLabel": "Pricing", "linkTarget": null, "linkUrl": "http://www.hubspot.com/pricing", "children": [], "isDeleted": false}]{% end_widget_attribute %} {% end_widget_block %} ``` ```html

        ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `orientation` | Enumeration | Defines classes of menu markup to allow to style the orientation of menu items on the page. Possible values include `"horizontal"` and `"vertical"`. | `"horizontal"` | | `menu_tree` | JSON | Menu structure including page link names and target URLs. | `[]` | ## Social sharing Social sharing tags generate social media icons that can be used to share a particular page. This module can be used with [block syntax](/reference/cms/modules/using-modules-in-templates#block-syntax) to customize the icon images and more. ```hubl {% social_sharing "social_sharing" %} {% social_sharing "my_social_sharing" use_page_url=True %} Block Syntax Example: {% widget_block social_sharing "my_social_sharing" label="Social Sharing", use_page_url=True, overrideable=True %} {% widget_attribute "pinterest" is_json=True %}{"custom_link_format": "", "pinterest_media": "http://cdn1.hubspot.com/hub/158015/305390_10100548508246879_837195_59275782_6882128_n.jpg", "enabled": true, "network": "pinterest", "img_src": "https://static.hubspot.com/final/img/common/icons/social/pinterest-24x24.png"}{% end_widget_attribute %} {% widget_attribute "twitter" is_json=True %}{"custom_link_format": "", "enabled": true, "network": "twitter", "img_src": "https://static.hubspot.com/final/img/common/icons/social/twitter-24x24.png"}{% end_widget_attribute %} {% widget_attribute "linkedin" is_json=True %}{"custom_link_format": "", "enabled": true, "network": "linkedin", "img_src": "https://static.hubspot.com/final/img/common/icons/social/linkedin-24x24.png"}{% end_widget_attribute %} {% widget_attribute "facebook" is_json=True %}{"custom_link_format": "", "enabled": true, "network": "facebook", "img_src": "https://static.hubspot.com/final/img/common/icons/social/facebook-24x24.png"}{% end_widget_attribute %} {% widget_attribute "email" is_json=True %}{"custom_link_format": "", "enabled": true, "network": "email", "img_src": "https://static.hubspot.com/final/img/common/icons/social/email-24x24.png"}{% end_widget_attribute %} {% end_widget_block %} ``` ```html ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `use_page_url` | Boolean | If true, the module shares the URL of the page by default. | `True` | | `link` | String | Specifies a different URL to share, if `use_page_url` is false. | | | `pinterest` | JSON | Parameters for Pinterest link format and icon image source. | `See block syntax example, above` | | `twitter` | JSON | Parameters for Twitter link format and icon image source. | `See block syntax example, above` | | `linked_in` | JSON | Parameters for LinkedIn link format and icon image source. | `See block syntax example, above` | | `facebook` | JSON | Parameters for Facebook link format and icon image source. | `See block syntax example, above` | | `email` | JSON | Parameters for email sharing link format and icon image source. | `See block syntax example, above` | ## Spacer A spacer tag generates an empty span tag. This tag can be styled to act as a spacer. In drag and drop layouts, the spacer module is wrapped in a container with a class of span1-span12 to determine how much space the module should take up in the twelve column responsive grid. ```hubl {% space "space" %} {% space "spacer" label="Horizontal Spacer" %} ``` ```html ``` ## System page tags The following tags can be used on [system pages](/guides/cms/content/templates/overview#system-pages), such as the password reset or email subscription pages. ### Email backup unsubscribe The backup unsubscribe tag renders for email recipients, if HubSpot is unable to determine their email address, when that recipient tries to unsubscribe. This tag renders a form for the contact to enter his or her email address to unsubscribe from email communications. It should be used on an [Unsubscribe Backup system template.](https://knowledge.hubspot.com/design-manager/use-system-templates-to-customize-error-subscription-and-password-prompt-pages) ```hubl {% email_simple_subscription "email_simple_subscription" %} {% email_simple_subscription "email_simple_subscription" header="Email Unsubscribe", input_help_text="Your email address:", input_placeholder="email@example.com", button_text="Unsubscribe", label="Backup Unsubscribe" %} ``` ```html
        ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `header` | String | Renders text in an h1 tag above the unsubscribe form. | `"Email Unsubscribe"` | | `input_help_text` | String | Renders help text in an h3 tag above your email unsubscribe form field. | `"Your email address:"` | | `input_placeholder` | String | Adds placeholder text within the email address form field. | `"email@example.com"` | | `button_text` | String | Changes the text of the unsubscribe form submit button. | `"Unsubscribe"` | ### Email subscriptions This module renders when an email recipient goes to edit his or her subscription preferences. It should be used on a [Subscription Preference system template.](https://knowledge.hubspot.com/design-manager/use-system-templates-to-customize-error-subscription-and-password-prompt-pages) ```hubl {% email_subscriptions "email_subscriptions" %} {% email_subscriptions "email_subscriptions" resubscribe_button_text="Yes, resubscribe me!", unsubscribe_single_text="Uncheck the types of emails you do not want to receive:", subheader_text="\n If this is not your email address, please ignore this page since the email associated with this page was most likely forwarded to you.\n", unsubscribe_all_unsubbed_text="You are presently unsubscribed from all of our emails. Would you like to receive our emails again?", button_text="Update email preferences", label="Subscription Preferences", header="Communication Preferences", unsubscribe_all_option="Unsubscribe me from all mailing lists.", unsubscribe_all_text="Or check here to never receive any emails:" %} ``` ```html

        Your email type description

        Your email type description

        ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `header` | String | Renders text in an h1 tag above the subscription preferences form. | `"Communication Preferences"` | | `subheader_text` | String | Populates text below the heading above the unsubscribe preferences. | `"

        If this is not your email address, please ignore this page since the email associated with this page was most likely forwarded to you.

        "` | | `unsubscribe_single_text` | String | Renders text in a `

        ` above the subscription options. | `"Uncheck the types of emails you do not want to receive:"` | | `unsubscribe_all_text` | String | Renders text in a `

        ` above the unsubscribe from all emails checkbox input. | `"Or check here to never receive any emails:"` | | `unsubscribe_all_unsubbed_text` | String | Populates text within a `

        ` that renders, if a contact is currently unsubscribed from all emails. | `"You are presently unsubscribed from all of our emails. Would you like to receive our emails again?"` | | `unsubscribe_all_option` | String | Sets the text next to the unsubscribe from all emails checkbox input. | `"Unsubscribe me from all mailing lists."` | | `button_text` | String | Sets the text of the submit button that updates subscription preferences. | `"Update email preferences"` | | `resubscribe_button_text` | String | Sets the text of the submit button for when contacts are resubscribing. | `"Yes, resubscribe me!"` | ### Email subscriptions confirmation The email subscriptions update confirmation is a module that can be added to the thank you template for when a recipient updates his or her subscription preferences or unsubscribes. It should be used on a [Subscription Preference system template.](https://knowledge.hubspot.com/design-manager/use-system-templates-to-customize-error-subscription-and-password-prompt-pages) ```hubl {% email_subscriptions_confirmation "email_subscriptions_confirmation" %} {% email_subscriptions_confirmation "email_subscriptions_confirmation" label="Subscriptions Update Confirmation", unsubscribe_all_success="You have successfully unsubscribed from all email communications.", subscription_update_success="You have successfully updated your email preferences.", subheader_text="\n If this is not your email address, please ignore this page since the email associated with this page was most likely forwarded to you.\n" %} ``` ```html ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `header` | String | Renders text in an h1 tag above the unsubscribe form. | `"Communication Preferences"` | | `subheader_text` | String | Populates text above the confirmation message. | `"

        If this is not your email address, please ignore this page since the email associated with this page was most likely forwarded to you.

        "` | | `unsubscribe_all_success` | String | Sets the text that will display when someone unsubscribes from all email communications. | `"You have successfully unsubscribed from all email communications."` | | `subscription_update_success` | String | Sets the text when a recipient updates his or her subscription preferences. | `"You have successfully updated your email preferences."` | ### Membership login Creates a login form to provide access to [private content](https://knowledge.hubspot.com/website-pages/require-member-registration-to-access-private-content). ```hubl {% member_login "my_login" %} {% member_login "my_login" email_label="Email", password_label="Password", remember_me_label="Remember Me", reset_password_text="Forgot your password?", submit_button_text="Login", show_password="Show password" %} ``` ```html ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `email_label` | String | The label for the email entry field. | `"Email"` | | `password_label` | String | The label for the password entry field. | `"Password"` | | `remember_me_label` | String | The label for the "Remember Me" checkbox. | `"Remember Me"` | | `reset_password_text` | String | The text for the password reset hyperlink. | `"Forgot your password?"` | | `submit_button_text` | String | The text for the submit button. | `"Login"` | | `show_password` | String | The text for the password reveal link. | `"Show password"` | ### Membership registration Creates a form to register for access to [private content](https://knowledge.hubspot.com/website-pages/require-member-registration-to-access-private-content). ```hubl {% member_register "my_register" %} {% member_register "my_register" email_label="Email", password_label="Password", password_confirm_label="Confirm Password", submit_button_text="Save Password", show_password="Show password" %} ``` ```html
        ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `email_label` | String | The label for the email entry field. | `"Email"` | | `password_label` | String | The label for the password entry field. | `"Password"` | | `password_confirm_label` | String | The label for the password confirmation field. | `"Confirm Password"` | | `submit_button_text` | String | The text for the submit button. | `"Save Password"` | | `show_password` | String | The text for the password reveal link. | `"Show password"` | ### Password reset request Creates a form to send a password reset email for accessing password-protected pages. ```hubl {% password_reset_request "my_password_reset_request" %} {% password_reset_request "my_password_reset_request" email_label="Email", submit_button_text="Send Reset Email" %} ``` ```html ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `email_label` | String | The label for the email entry field. | `"Email"` | | `submit_button_text` | String | The text for the submit button. | `"Send Reset Email"` | | `password_reset_message` | String | The message that displays after requesting the password reset email. | `False` | ### Password reset Renders a password reset form for accessing password-protected pages. ```hubl {% password_reset "my_password_reset" password_label="Password", password_confirm_label="Confirm Password", submit_button_text="Save password", show_password="Show password" %} ``` ```html ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `password_label` | String | The text label for the password input field. | `"Email"` | | `password_confirm_label` | String | The text label for the password confirmation input field. | `"Send Reset Email"` | | `submit_button_text` | String | The text label for the form submit button. | `False` | | `show_password` | String | The text label for the button to unhide the entered password value. | `False` | | `password_requirements` | String | The text label that describes password requirements. | `False` | ### Password prompt Adds a password prompt to password-protected pages. ```hubl {% password_prompt "password_prompt" %} {% password_prompt "my_password_prompt" submit_button_text="Submit", bad_password_message="Sorry, please try again.\n", label="Password Prompt" %} ``` ```html
        ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `submit_button_text` | String | Label for button below password entry field. | `"Submit"` | | `bad_password_message` | String | Message displayed if incorrect password entered. | `"

        Sorry, please try again.

        "` | | `password_placeholder` | String | Defines placeholder text within the password field. | `"Password"` | ## Text Creates a single line of text. This tag can be useful to be mixed into your markup, when used in conjunction with the `no_wrapper=True` parameter. For example, if you wanted your end users to be able to define a destination of a predefined anchor, you could populate the `href` with a text module with `no_wrapper=True`. ```hubl {% text "text" %} {% text "simple_text_field" label="Enter text here", value="This is the default value for this text field" %} ``` ```html This is the default value for this text field ``` | Parameter | Type | Description | | --------- | ------ | ---------------------------------------------------- | | `value` | String | Sets the default text of the single line text field. | ## Textarea A textarea is similar to a text module in that it allows users to enter plain text, but it gives them a larger area to work in the content editor. This module does not support HTML. If you want to use directly within a predefined wrapping tag, add the `no_wrapper=true` parameter. ```hubl {% textarea "my_textarea" %} {% textarea "my_textarea" label="Enter plain text block", value="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean a urna quis lacus vehicula rutrum.", no_wrapper=True %} ``` ```html Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean a urna quis lacus vehicula rutrum. ``` | Parameter | Type | Description | | --------- | ------ | -------------------------------------- | | `value` | String | Sets the default text of the textarea. | ## Video Player Render a video player for a video file from the file manager that has the _Allow embedding, sharing, and tracking_ setting turned on. ```hubl {% video_player "embed_player" %} {% video_player "embed_player" overrideable=False, type="scriptV4", hide_playlist=True, viral_sharing=False, embed_button=False, width="600", height="375", player_id="6178121750", style="", conversion_asset="{"type":"FORM","id":"9a77c63f-bee6-4ff8-9202-b0af020ea4b2","position":"POST"}" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `hide_playlist` | Boolean | Hide the video playlist. | `True` | | `playlist_color` | String | A HEX color value for the playlist. | | | `play_button_color` | String | A HEX color value for the play and pause buttons. | `#2A2A2A` | | `embed_button` | Boolean | Display embed button on the player | `False` | | `viral_sharing` | Boolean | Display the social networks sharing button on the player. | `False` | | `width` | Number | Width of the embedded video player. | `Auto` | | `height` | Number | Height of the embedded video player. | `Auto` | | `player_id` | Number | The `player_id` of the video to embed. | | | `style` | String | Additional style for player. | | | `conversion_asset` | JSON | Event setting for player. Can render CTA or Form before or after video plays. This parameter takes a JSON object with three parameters: type (FORM or CTA), id (The guid of the type object), position (POST or PRE). | `See above example` | | `placeholder_alt_text` | String | The video's alt text. | | # HubL variables & macros syntax @ Learn how to use HubL variables & macros to build dynamic pages on HubSpots CMS and emails. $ reference & cms/hubl --- # HubL variables & macros syntax HubL uses variables to store and output values to the template. Variables can be used in template logic or iterated through with for loops. In addition to variables, macros are another useful tool for printing repetitive yet dynamic sections of code throughout your templates. Variables are expressions delimited by `}}`. The basic syntax of variables is as follows: ```hubl // variables {{ variable }} {{ dict.attribute }} ``` ## Variables Variables are either a single word in an expression or an attribute of a dictionary. HubL uses Python-based data structures called dictionaries or **dicts** to store various sets of variables. For example, HubSpot uses a dictionary "content" to house many attributes that pertain to the content created with that template. For example the `content.absolute_url` prints the URL of the specific piece of content. HubSpot has many predefined variables that can be used throughout your page, blog, and email templates. We have a [reference list of variables](/reference/cms/hubl/variables), you can also view the [developer info](/guides/cms/debugging/troubleshooting#developer-info) when browsing any page from your account to see the variables available within that page.. In addition to printing the values of variables and dictionary attributes in a template, you can also define your own variables. You can store strings, booleans, integers, sequences, or create dictionaries within a single variable. Variables are defined within statement delimiters using the word "set". Once stored, variables can then be printed by stating the variable name as an expression. Below you can see various types of information stored in variables and then printed. Variables should either be single words or use underscores for spaces (ie my_variable). **HubL does not support hyphenated variable names.** ```hubl {% set string_var = "This is a string value stored in a variable" %} {{ string_var}} {% set bool_var = True %} {{ bool_var}} {% set int_var = 53 %} {{ int_var}} {% set seq_var = ["Item 1", "Item 2", "Item 3"] %} {{ seq_var}} {% set var_one = "String 1" %} {% set var_two = "String 2" %} {% set sequence = [var_one, var_two] %} {{ sequence }} {% set dict_var = {"name": "Item Name", "price": "$20", "size":"XL"} %} {{ dict_var.price }} ``` ```html This is a string value stored in a variable True 53 [Item1, Item2, Item3] [String 1, String 2] $20 ``` Each example above stores a different type of variable, with the final example storing two different variables in a sequence. In addition to printing values, variables can be used in [if statements](/reference/cms/hubl/if-statements), as [filter](/reference/cms/hubl/filters) parameters, as [function](/reference/cms/hubl/functions) parameters, as well as iterated through with [for loops](/reference/cms/hubl/loops) (sequence variables only). One common usage is to use variables to define common CSS values in your stylesheet. For example, if you have a color that you use over and over again throughout your CSS file. That way, if you need to change that color, you can change the variable value, and all references to that variable will be updated, the next time that you publish the file. ```hubl {% set primary_color = "#F7761F" %} a { color: {{ primary_color }}; {# prints variable HEX value #} } ``` ```css a { color: #f7761f; } ``` ## Macros HubL macros allow you to print multiple statements with a dynamic value. For example, if there is a block of code that you find yourself writing over and over again, a macro may be a good solution, because it will print the code block while swapping out certain arguments that you pass it. The macro is defined, named, and given arguments within a HubL statement. The macro is then called in a statement that passes its dynamic values, which prints the final code block with the dynamic arguments. The basic syntax of a macro is as follows: ```hubl {% macro name_of_macro(argument_name, argument_name2) %} {{ argument_name }} {{ argument_name2 }} {% endmacro %} {{ name_of_macro("value to pass to argument 1", "value to pass to argument 2") }} ``` If your macro is returning whitespace in the form of new lines, you can strip whitespace in templates by hand. If you add a minus sign (`-`) to the start or end of a block, a comment, or a variable expression, the whitespaces before or after that block will be removed. ```hubl {% macro name_of_macro(argument_name, argument_name2) -%} {{ argument_name }} {{ argument_name2 }} {%- endmacro %} ``` Below shows a practical application of a macro to print a CSS3 properties with the various vendor prefixes, with a dynamic value. This allows you to print 5 lines of code with a single macro tag. ```hubl {% macro trans(value) %} -webkit-transition: {{value}}; -moz-transition: {{value}}; -o-transition: {{value}}; -ms-transition: {{value}}; transition: {{value}}; {% endmacro %} a { {{ trans("all .2s ease-in-out") }} } ``` ```css a { -webkit-transition: all 0.2s ease-in-out; -moz-transition: all 0.2s ease-in-out; -o-transition: all 0.2s ease-in-out; -ms-transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out; } ``` Macros introduce the ability to have recursive code. To prevent reliability and performance issues you can only nest macros 20 levels deep. If you go over this limit you will get the error: `max recursion limit of 20 reached for macro ` ## Call In some instances, you may want to pass additional dynamic information back into the macro block. For example, you may have a large piece of code that you want to feed back into the macro, in addition to the arguments. You can do this using call block and caller(). A call block works essentially like a macro but does not get its own name. The expression caller() specifies where the contents of the call block will render. In the example below, a `

        ` is added into a macro in addition to the two arguments. ```hubl {% macro render_dialog(title, class) %} {% endmacro %} {% call render_dialog("Hello World", "greeting") %}

        This is a paragraph tag that I want to render in my.

        {% endcall %} ``` ```html ``` ## Import Another useful feature of macros is that they can be used across templates by importing one template file into another. To do this you will need to use the **import** tag. The import tag will let you specify a Design Manager file path to the template that contains your macros and give the macros a name in the template that you are including them in. You can then pass values into these macros without needing to redefine them. For example, let's say that you have a .html template file that contains the following 2 macros. One macro is defined to set up a header tag and one is defined to generate a footer tag. This file is saved in Design Manager with the name `my_macros.html`. ```hubl {% macro header(tag, title_text) %}
        <{{ tag }}>{{ title_text }}
        {% endmacro %} {% macro footer(tag, footer_text) %}
        <{{ tag }}>{{ footer_text }}
        {% endmacro %} ``` In the template that will use these macros, an import tag is used that specifies the file path to the `my_macros.html` file. It also names the group of macros (in this example `header_footer`). Macros can then be executed by appending the macro name to the name given to the imported template. See the example below. ```hubl {% import "custom/page/web_page_basic/my_macros.html" as header_footer %} {{ header_footer.header("h1", "My page title") }}

        Some content

        {{ header_footer.footer("h3:", "Company footer info") }} ``` ```html

        My page title

        Some content

        Company footer info

        ``` ## From If you want to only import specific macros, instead of all macros contained in a separate .html file, you can use the **from** tag. With the from tag, specify only the macros that you want to import. Generally, using **import** will provide more flexibility, but this alternative is also supported. The example below accesses the same `my_macros.html` file from the previous section of this article. But this time instead of importing all macros, it accesses only the footer macro. ```hubl {% from "custom/page/web_page_basic/my_macros.html" import footer %} {{ footer("h2", "My footer info") }} ``` ```html

        My footer info

        ``` ## Variables within loops Any variables defined within loops are limited to the scope of that loop and cannot be called from outside of the loop. You can call variables that are defined outside of a loop, from within a loop, but not the other way around. You can also use functions in order to mutate objects for settings values on dict's or performing list operations. The following example is using the [`.update` list operation](/reference/cms/hubl/functions#update): ```hubl {% set obj = {val : 0} %} {% for i in range(0, 10) %} {% do obj.update({val: obj.val + i }) %} {% endfor %} {{ obj.val }} ``` ```html 45 ``` # HubL variables @ HubSpot templates can use a host of predefined variables that can be used to render useful website and email elements. This page is a reference listing of those variables. $ reference & cms/hubl --- # HubL variables HubSpot templates can use a host of predefined variables that can be used to render useful website and email elements. This page is a reference listing of those variables. [Learn more about creating your own variables](/reference/cms/hubl/variables-macros-syntax) in a [HubL template](/guides/cms/content/templates/types/html-hubl-templates) or [module](/guides/cms/content/modules/overview). While most of the variables listed on this page are optional, there are a few variables that are required for creating emails and pages from your templates. The variables listed below can be used individually by wrapping them in the `}}` delimiter as noted on our [Variables and Macros page](/reference/cms/hubl/variables-macros-syntax). You can optionally use these variables with other parts of the HubL Templating Language such as [loops](/reference/cms/hubl/loops), [filters](/reference/cms/hubl/filters), [functions](/reference/cms/hubl/functions), [tags](/reference/cms/hubl/tags/standard-tags), and more. ## Variables available in all templates The HubL variables below can be used in email, page, or blog templates. To see additional information about what these variables output, you can use the [pprint filter](/reference/cms/hubl/filters#pprint), as shown below. ```hubl {% set variable = content %} {{variable|pprint}} ``` ```html (ContentMeta: { "canonicalUrl" : "https://www.website.com/url-slug", "deferDynamicValues" : false, "featuredImageAltText" : "", "featuredImageHeight" : 0, "featuredImageUrl" : "", "featuredImageWidth" : 0, "htmlTitle" : "", "linkRelCanonicalUrl" : "https://www.website.com/url-slug", "metaDescription" : "", "metaKeywords" : "", "name" : "", "unescapedHtmlTitle" : "", "unescapedMetaDescription" : null }) ``` | Variable | Type | Description | | --- | --- | --- | | `account` | Dict | This variable is a dictionary that stores company personalization properties for a known contact. Properties can be accessed from this dict, by adding a period and the property name. For example, `account.name` would print the company name of a contact._Use of this variable will disable page caching._ | | `company_domain` | String | Prints the company domain from **Website** **> Pages > Branding > Logo Link**. | | `contact` | Dict | This variable is a dictionary that stores contact personalization properties for a known contact. Properties can be accessed from this dict, by adding a period and the property name. For example, `contact.firstname` would print the first name of a contact._Use of this variable will disable page caching._ | | `content` | Dict | This variable is a dictionary that stores various properties pertaining to a specific piece of content such as an email, a page, or a post. | | `content.absolute_url` | String | Prints the full URL of a page, post, or web page version of an email. | | `content.archived` | Boolean | This variable evaluates to True, if the page or email was marked as archived by the user. | | `content.author_email` | String | The email address of the content creator. | | `content.author_name` | String | The first and last name of the content creator. | | `content.author_username` | String | The HubSpot username of the content creator. | | `content.campaign` | String | The GUID for the marketing campaign that this page or email is associated with. This unique ID can be found in the URL of a particular campaign in the Campaign's tool. | | `content.campaign_name` | String | The name of the marketing campaign that this page, this post, or this email is associated with. | | `content.created` | Datetime | A datetime object for when the content was originally created, in UTC time. This variable can be formatted with the [datetime filter](/reference/cms/hubl/filters#datetimeformat). | | `content.meta_description` | String | When pulling the meta description of a page, it is better to use the variable `page_meta.meta_description`. | | `content.name` | String | The name of a post, email, or page. For pages and emails this will print the internal content name, while for posts this will print the post title. For blog posts, this is the post title that displays. For other types of content, this is generally an internal name. This variable includes a wrapper so that it is editable via the UI, when included in blog posts. If you want to print the content name without a wrapper, use page_meta.name. | | `content.publish_date` | Datetime | A datetime object representing when the content was published, in UTC time. This variable can be formatted with the [format_datetime filter](/reference/cms/hubl/filters#format-datetime). | | `content.publish_date_localized` | String | A string representing the datetime when the content was published using the time zone defined in the account's [default settings](https://knowledge.hubspot.com/account/change-your-language-and-region-settings). This variable is also subject to the language and date format settings in **Settings > Website > Blog > Date Formats**. | | `content.template_path` | String | The Design Manager file path to your template (ie `custom/page/web_page_basic/my_template.html`). | | `content.slug` | String | The URL slug of a page, post, or web page version of an email. This is the value that follows the domain. For example, in `https://example.com/about-us`, the slug is `about-us`.For the full URL, use `content.absolute_url` instead. | | `content.updated` | Datetime | A datetime object for when the user last updated the content, in UTC time. This variable can be formatted with the datetime filter. _Does not equal_ `content.publish_date` _on initial publish. Use_ [\|between_times](/reference/cms/hubl/filters#between-times) _filter to test if a post has been updated after publishing._ | | `content_id` | String | Prints the unique ID for a page, post, or email. This ID can be found in the URL of the editor. You can use this variable as an alias for content.id. | | `favicon_link` | String | Prints the source URL of the favicon. This image is set in **Settings > Website > Pages > Branding**. | | `hub_id` | String | The portal ID of your HubSpot account. | | `hubspot_analytics_tracking_code` | String | Includes the analytics tracking code. This tag is not necessary, because `standard_footer_includes`, already renders the tracking code. | | `local_dt` | Datetime | A datetime object of the current time in the time zone defined in your Report Settings. _Usage of this variable will disable page caching in order to return the current time. May hurt page performance. Use JavaScript instead to get current date and time in a cacheable way._ | | `local_time_zone` | String | The time zone, as configured in your HubSpot Report Settings. | | `page_meta.canonical_url` | String | The official URL that this page should be accessed at. Usually does not include any query string parameters. Use this for the `rel="canonical"` tag. HubSpot automatically canonicalizes URLs. | | `page_meta.html_title` | String | The title of the page. This variable should be used in the `` tag of HTML templates. | | `page_meta.meta_description` | String | The meta description of a page. This variable should be used in the "description" `<meta>` tag of HTML templates. | | `page_meta.name` | String | An alias for `content.name`. | | `portal_id` | String | An alias for hub_id | | `request_contact` | Dict | A dictionary containing data about the requested contact._Use of this variable will disable page caching._ _Not available in email templates._ | | `site_settings` | Dict | The site_settings dict contains various settings from such as colors and fonts (see below). | | `year` | String | Prints the current year. | ## Email variables Below are the HubL variables that can be used in email templates. Some variables, such as the [color and font settings variables](#color-and-font-settings), can be used in other content types if you want to reference your email color and font settings elsewhere. ### Required email template variables To be [CAN-SPAM](https://knowledge.hubspot.com/email/can-i-customize-my-can-spam-email-footer) compliant, all emails sent through HubSpot require certain company and opt-out information. This information is set by the following variables, which must be included. There are additional email variables that are optional which are listed [further down this page](#email-variables). | Variable | Type | Description | | --- | --- | --- | | `site_settings.company_city` | String | Prints the company city (set in **Settings > Marketing > Email > Configuration > Footer**). | | `site_settings.company_name` | String | Prints the company name (set in **Settings > Marketing > Email > Configuration > Footer**). | | `site_settings.company_state` | String | Prints the company state (set in **Settings > Marketing > Email > Configuration > Footer**). | | `site_settings.company_street_address_1` | String | Prints the company address (set in **Settings > Marketing > Email > Configuration > Footer**). | | `unsubscribe_link` | String | Prints the URL of the page that allows recipients to manage subscription preferences or unsubscribe from email communications. This variable should be used in the href attribute of an <a>. | ### Color and font settings There are several basic color and font controls in your [marketing email settings](https://knowledge.hubspot.com/marketing-email/manage-your-marketing-email-account-settings) that can be printed to emails. Where noted, you can use some of these variables on page and blog templates by adding `site_settings.` before the variable. | Variable | Type | Description | | --- | --- | --- | | `background_color` | String | Background color setting as a hex value.To print this value in page or blog templates, use:`site_settings.background_color` | | `body_border_color` | String | Body border color setting as a hex value. This option becomes available when you select **Manually set email border color** under the _Border color options_ dropdown menu in settings.To print this value in page or blog templates, use:`site_settings.body_border_color` | | `body_border_color_choice` | String | The value for this variable is controlled by the _Border color options_ dropdown menu in settings. Values are: `BORDER_AUTOMATIC`, `BORDER_MANUAL`, `BORDER_NONE`.To print this value in page or blog templates, use:`site_settings.body_border_color_choice` | | `body_color` | String | Body color setting as a hex value.To print this value in page or blog templates, use:`site_settings.body_color` | | `primary_accent_color` | String | Primary accent color setting as a hex value.To print this value in page or blog templates, use:`site_settings.primary_accent_color` | | `primary_font` | Enumeration | Primary font setting as a string.To print this value in page or blog templates, use:`site_settings.primary_font` | | `primary_font_color` | String | Primary font color setting as a hex value.To print this value in page or blog templates, use:`site_settings.primary_font_color` | | `primary_font_size` | String | Primary font size setting as a `px` value.To print this value in page or blog templates, use:`site_settings.primary_font_size` | | `primary_font_size_num` | Number | Primary font size number, excluding `px`. | | `secondary_accent_color` | String | Secondary accent color setting as a hex value.To print this value in page or blog templates, use:`site_settings.secondary_accent_color` | | `secondary_font` | Enumeration | Secondary font setting as a string.To print this value in page or blog templates, use:`site_settings.secondary_font` | | `secondary_font_color` | String | Secondary font color setting as a hex value.To print this value in page or blog templates, use:`site_settings.secondary_font_color` | | `secondary_font_size_num` | String | Secondary font size number, excluding `px`. | ### Email content | Variable | Type | Description | | --- | --- | --- | | `content.create_page` | Boolean | Will be `True` if there is a web page version of the email. | | `content.email_body` | Richtext | The main body of the email. This variable renders a rich text module. | | `content.emailbody_plaintext` | String | The optional override of the plain text email body | | `content.from_name` | String | The from name of the email sender | | `content.reply_to` | String | The reply to address for the email | | `content.subject` | String | The subject of the email | | `email_body_padding` | string | The [email body padding setting](https://knowledge.hubspot.com/marketing-email/manage-your-marketing-email-account-settings#size). | | `email_body_width` | String | The [email body width setting](https://knowledge.hubspot.com/marketing-email/manage-your-marketing-email-account-settings#size). | | `site_settings.company_street_address_2` | String | Prints the address line 2 from the account's [CAN-SPAM footer settings](https://knowledge.hubspot.com/marketing-email/can-i-customize-my-can-spam-email-footer). | | `site_settings.office_location_name` | String | Prints the office location name from the account's [CAN-SPAM footer settings](https://knowledge.hubspot.com/marketing-email/can-i-customize-my-can-spam-email-footer). | | `subscription_confirmation_url` | String | Prints the URL of the subscription preferences confirmation page. This URL is dynamically generated on send. | | `subscription_name` | String | Prints the name of the email type specified for that email. | | `unsubscribe_anchor` | String | Generates an anchor tag with the word "unsubscribe" linked to your unsubscribe page. | | `unsubscribe_link_all` | String | Renders a link to unsubscribe from all email communications, as opposed to a link to manage subscription preferences. | | `unsubscribe_section` | String | Renders an unsubscribe section that includes an unsubscribe link, as well as help text. | | `view_as_page_section` | String | Generates a link with help text that leads to a webpage version of an email. | | `view_as_page_url` | String | Generates a link that leads to a webpage version of an email. | ### Private content email variables The following list of variables are only available inside of email templates that are used for [private content email templates](https://knowledge.hubspot.com/cms-pages-editor/control-audience-access-to-pages#customize-your-membership-registration-page-and-email-templates). | Variable | Type | Description | | --- | --- | --- | | `membership_company_name` | String | The company name, as set in the account's [private content settings](https://knowledge.hubspot.com/website-pages/manage-private-content-settings#edit-general-settings-for-private-content). | | `membership_domain` | String | The domain of the private content website. | | `membership_logo_src` | String | The URL of the logo to display, as set in the account's [brand kit settings](https://knowledge.hubspot.com/branding/edit-your-logo-favicon-and-brand-colors). | | `membership_logo_alt` | String | The logo's `alt` attribute, as set in the account's [brand kit settings](https://knowledge.hubspot.com/branding/edit-your-logo-favicon-and-brand-colors). | | `membership_registration_link` | URL | Link to the registration page for the private content website. | | `membership_password_saved_link` | URL | Link to the password saved page. The link will redirect the visitor to a random restricted page that they have access to. | | `membership_password_reset_link` | URL | Link to the reset password page for the private content website. | | `membership_passwordless_auth_link` | URL | Link to the password-less member login page. | | `membership_verify_registration_link` | URL | Link for the registration verification page. | | `membership_website_admin` | String | The name of the website admin, as set in the account's [private content settings](https://knowledge.hubspot.com/website-pages/manage-private-content-settings#edit-general-settings-for-private-content). | ## Website page variables The following variables are available for site pages, landing pages, system pages, and blogs. ### Required page template variables To publish a coded file as an editable page or blog template, the following variables must be included. To publish an HTML file without these variables, to use within another template, include `isAvailableForNewContent: false` in the [template annotations](/guides/cms/content/templates/types/html-hubl-templates#template-annotations). | Variable | Type | Description | | --- | --- | --- | | `standard_footer_includes` | String | Renders the [HubSpot tracking code](/guides/cms/content/templates/overview#hubspot-tracking-code) and any other code included in your Footer HTML in [**Content Settings**](https://app.hubspot.com/l/settings/website/pages/all-domains/page-templates) or the options of a particular page. This tag should be inserted directly before the closing body tag. | | `standard_header_includes` | String | Adds [jQuery, layout.css](/guides/cms/content/templates/overview#included-cms-files), any attached stylesheets, a meta viewport tag, Google Analytics tracking code, other page meta information, and code added to the head tag at the domain/template/page level. This variable should be added to the <head> of HTML templates. | ### Content and contact variables | Variable | Type | Description | | --- | --- | --- | | `builtin_body_classes` | String | This variable dynamicaly prints helpful classes that help differentiate the markup of content created with that template (ie type of content, content name, etc). This makes styling different types of content or particular pages easier. This variable should be used in the class attribute of the body tag on coded templates. | | `request_contact.is_logged_in` | String | This variable defines whether or not the requesting contact is logged in to a website's gated content (see [control audience access documentation](https://knowledge.hubspot.com/cms-pages-editor/control-audience-access-to-pages) for further information). The value of this variable will return true if the requesting contact is logged in, and false if the requesting contact has logged out. A contact can be logged out by directing them to the URL `https://www.yourdomain.com/_hcms/mem/logout`._Use of this variable will disable page caching._ | | `request_contact.list_memberships` | String | This variable returns a dict of ids that represents the lists of which the contact is a member._Use of this variable will disable page caching._ | | `content.language` | Dict | This variable returns a dict of information about the language settings of a page. `{{ content.language.languageTag }}` returns the language identifier of a page (i.e. "en" or "es"). `{{ content.language.textDirection.value }}` returns the text direction of the language of the page (i.e. "rtl" or "ltr"). | ## HTTP request variables The following variables print information about the HTTP page request. | Variable | Type | Description | | --- | --- | --- | | `request.cookies` | Dict | A dictionary of cookie names mapped to cookie values._Use of this variable will disable page caching._ | | `request.domain` | String | The domain used to access this page | | `request.full_url` | String | The URL used to access this page. | | `request.path` | String | The path component of the URL | | `request.path_and_query` | String | The path and query component of the URL | | `request.query` | String | The query string component of the URL. request.query_dict automatically splits the query strings into key value pairs, and is recommended over the raw query for most use-cases. | | `request.query_dict` | Dict | The query string converted into a name->value dictionary. | | `request.referrer` | String | The HTTP referrer, the url of the page that linked to the current page._Use of this variable will disable page caching._ | | `request.remote_ip` | String | The IP address of the visitor._Use of this variable will disable page caching._ | | `request.scheme` | String | The protocol of the request (either http or https) | | `request.search_engine` | String | The search engine used to find this page, if applicable. Ex: google, aol, live, yahoo, images.google, etc | | `request.search_keyword` | String | The keyword phrase used to find this page, if applicable | | `request.headers` | String | A dictionary of available request headers._Usage of this variable will disable page caching in order to interpret individualized headers for each request. May hurt page performance._ | ## Blog variables The following variables are available for blog templates. Some variables are only available for post listings, while others may only be available for blog posts. | Variable | Type | Description | | --- | --- | --- | | `blog_author` | String | This variable contains blog author information for blog author listing pages. It can be used to create conditional logic to >[render markup for blog author listings](/guides/cms/content/templates/types/blog#if-blog-author-statement). It also contains the following properties:<ul><li>`blog_author.avatar`</li><li>`blog_author.bio`</li><li>`blog_author.display_name`</li><li>`blog_author.email`</li><li>`blog_author.facebook`</li><li>`blog_author.google_plus`</li><li>`blog_author.has_social_profiles`</li><li>`blog_author.linkedin`</li><li>`blog_author.twitter`</li><li>`blog_author.website`</li></ul> | | `content.blog_post_author` | String | This variable contains individual blog post author information for a given post. It contains the following properties:<ul><li>`content.blog_post_author.avatar`</li><li>`content.blog_post_author.bio`</li><li>`content.blog_post_author.display_name`</li><li>`content.blog_post_author.email`</li><li>`content.blog_post_author.facebook`</li><li>`content.blog_post_author.google_plus`</li><li>`content.blog_post_author.has_social_profiles`</li><li>`content.blog_post_author.linkedin`</li><li>`content.blog_post_author.slug`</li><li>`content.blog_post_author.twitter`</li><li>`content.blog_post_author.website`</li></ul> | | `blog` | String | An alias for group. | | `content.comment_count` | Integer | The number of comments for the current blog post. | | `content.comment_list` | String | A list of the comments for the current blog post. | | `current_page_num` | Integer | The integer index of the current page of blog posts in the view. | | `content.featured_image` | String | The source URL of the featured image, selected when the blog was published. | | `content.featured_image_alt_text` | String | The alt text of the featured image. | | `last_page_num` | Integer | The integer index of the last page of blog posts in the view. | | `next_page_num` | Integer | The integer index of the next page of blog posts in the view. | | `content.next_post_featured_image` | String | The URL of the featured image of the next blog post, if one exists. | | `content.next_post_featured_image_alt_text` | String | Alt text for the next post's featured image if alt text exists. | | `content.next_post_name` | String | The name of the next blog post, if one exists. | | `content.next_post_slug` | String | The URL slug of the next blog post, if one exists. | | `content.post_body` | String | The body of the blog post. | | `content.post_list_content` | String | The body blog post content, modified for the listing page. The final output is affected by summary settings in **Settings > Website > Blog**. If featured images are enabled in settings, this variable will remove any images above the read more separator automatically. | | `content.post_list_summary_featured_image` | String | The featured image of post summaries to be used in listing templates. This variable is affected by the settings in **Settings > Website > Blog**. | | `content.post_summary` | String | The blog post summary. This content is determined by the read more separator in the blog editor. | | `content.previous_post_featured_image` | String | The URL of the featured image of the previous blog post, if one exists. | | `content.previous_post_featured_image_alt_text` | String | Alt text for the previous post's featured image if alt text exists. | | `content.previous_post_name` | String | The name of the previous blog post, if one exists. | | `content.previous_post_slug` | String | The URL slug of the previous blog post, if one exists. | | `content.publish_date_localized` | String | A string representing the date/time when the blog post was published, formatted according to the blog's language and date formatting settings. | | `simple_list_page` | Boolean | A boolean to indicate whether the requested page is the 'all posts' page containing links to all blog posts. | | `content.topic_list` | Dict | Can be used to render markup for a topic listing by looping through it. `{% for topic in content.topic_list %}` The items within contain the properties: `name` and `slug`. | | `contents` | String | Contents is a sequence of your blog posts that are iterated through using a for loop, available on [blog listing pages (is_listing_view)](/guides/cms/content/templates/types/blog#if-is-listing-view-statement). | | `contents.total_count` | Integer | Total number of posts in a listing (regular, topics, authors, etc.). | | `contents.total_page_count` | Integer | Total number of pages of posts based on your number of posts per page. | | `contents_topics` | String | Get a list of all blog topics in the contents sequence of posts. | | `group` | Dict | The dictionary containing variables that pertain to an entire blog. | | `group.absolute_url` | String | The base URL of a blog. | | `group.allow_comments` | Boolean | Evaluates to True, if comments are allowed. | | `group.description` | String | The meta description of the blog from **Settings > Website > Blog**. Used for the meta description on certain listing pages. | | `group.header` | String | The header of the blog. | | `group.html_title` | String | The title of this blog as it should appear in the `<title>` tag. | | `group.id` | String | The unique ID of a blog. This ID can be found in the URL of the Blog Dashboard for a particular blog. | | `group.language` | Dict | A dictionary containing information about a blog's language. `{{ group.language.languageTag }}`can be used in conditionals to render different content on the different language variations of a multi-language blog. | | `group.public_title` | String | The title of this blog as it should appear at the top of rendered pages. | | `group.show_summary_in_listing` | Boolean | A boolean from **Settings > Website > Blog** to indicate whether to show summaries in post listings. | | `group.slug` | String | The path to this blog. | | `group.use_featured_image_in_summary` | Boolean | A boolean from **Settings > Website > Blog** to indicate whether featured images are shown in post summaries. | | `archive_list_page` | Boolean | Returns true if page is a blog archive page. Ex: `https://www.example.com/blog/archive/2020/02` would return `true`. | ## CRM object dynamic pages The following variables are used to [build dynamic pages with CRM objects](/guides/cms/content/data-driven-content/dynamic-pages/overview#crm-object-dynamic-pages). These variables are only available for CRM object dynamic pages. | Variable | Type | Description | | --- | --- | --- | | `dynamic_page_crm_object` | Dict | The CRM object of the dynamic page that matches with the page request path. If the request is to the listing page, this value will be `null`. | | `dynamic_page_crm_object_type_fqn` | String | The fully qualified name (FQN) of the crm object. The FQN is an assigned unique ID for the object, including portal ID and object name.The fqn can be used in the [`crm_objects` function.](/reference/cms/hubl/functions#crm-objects) | ## HubDB variables The following variables are used to build dynamic pages with [HubDB](/guides/cms/storage/hubdb/overview). These variables are only available for [HubDB dynamic pages](/guides/cms/content/data-driven-content/dynamic-pages/overview#hubdb-dynamic-pages). | Variable | Type | Description | | --- | --- | --- | | `dynamic_page_hubdb_table_id` | Long | The ID of the table selected in the 'Advanced Settings\` tab of the page editor. | | `dynamic_page_hubdb_row` | Dict | The HubDB row of the dynamic page that matches with the page request path. If the request is to the listing page, this value will be `null`. | | `row.hs_id` | Long | The internal ID of a HubDB row. | | `row.hs_name` | String | The name of the HubDB row. | | `row.hs_path` | String | The path of the HubDB row. Used to resolve a request to one row in the table specified by `dynamic_page_hubdb_table_id`. | | `row.hs_child_table_id` | Long | The child table ID of the HubDB row. Can be used to build nested templates. | | `row.hs_parent_row` | Dict | The parent row of the HubDB row. Can only be used when using child tables for nested templates. | | `dynamic_page_route_level` | Integer | Current depth of a page in a multi-level dynamic template. The value starts at `0` and increments with each additional table layer. | ## Menu node variables The following variables are available to use on the object returned by the [HubL menu function](/reference/cms/hubl/functions#menu). | Variable | Type | Description | | --- | --- | --- | | `node.label` | String | The menu label of the page. | | `node.url` | String | URL of the page. | | `node.pageId` | Number | ID of the page if within HubSpot. | | `node.contentGroupId` | Number | Blog ID of the page if it is a HubSpot blog post. | | `node.parentNode` | Object | The parent node of the current node. The parent node will have the current node in its `children` property. | | `node.children` | List | The list of child nodes for the current node. | | `node.activeBranch` | Boolean | True if the node is in the top-level branch that the current page is in. | | `node.activeNode` | Boolean | True if the node is the current page. | | `node.level` | Number | The number of levels deep the current node is from the top-level nodes. | | `node.pageTitle` | String | Name of the content page if within HubSpot. | | `node.slug` | String | Path slug of the page. | | `node.linkTarget` | String | Link target of the page. | ## In-app editor and preview variables You can use the following variables to check if the content is being rendered in the content editor or previewer. For example, you may want to use these to prevent running code in the editor while still running the code on live pages. ```hubl {% if is_in_page_editor %} Display something different within the page editor. {% endif %} ``` | Variable | Type | Description | | --- | --- | --- | | `is_in_hs_app` | String | Returns `true` if content is being rendered within the HubSpot app. | | `is_in_editor` | String | Returns `true` if content is being rendered within any content editor. | | `is_in_global_content_editor` | String | Returns `true` if content is being rendered within the global content editor. | | `is_in_theme_editor` | Number | Returns `true` if content is being rendered within the theme editor. | | `is_in_page_editor` | String | Returns `true` if content is being rendered within the page editor. | | `is_in_blog_post_editor` | String | Returns `true` if content is being rendered within the blog post editor. | | `is_in_email_editor` | String | Returns `true` if content is being rendered within the email editor. | | `is_in_previewer` | Number | Returns `true` if content is being rendered within any preview context. | | `is_in_theme_previewer` | Object | Returns `true` if content is being rendered within the theme previewer. | | `is_in_template_previewer` | String | Returns `true` if content is being rendered within the template previewer. | | `is_in_page_previewer` | String | Returns `true` if content is being rendered within the page previewer. | | `is_in_blog_post_previewer` | String | Returns `true` if content is being rendered within the blog post previewer. | | `is_in_email_previewer` | String | Returns `true` if content is being rendered within the email previewer. | | `is_in_module_previewer` | String | Returns `true` if content is being rendered within the module previewer. | # Configuring a module @ Learn how to a configure a custom module. $ reference & cms/modules --- # Configuring a module At a high level, you configure module options locally within the `meta.json` file, which can include the following properties: | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `icon` | String | URL to an image to use as the icon for a module. | | | `label` | String | Label used when modules are shown in the content editors | | | `module_id` | Number | Unique id for the module that is independent from the path. | | | `is_available_for_new_content` | Boolean | The value for the toggle in the top right corner of the module editor in HubSpot. Determines if the module can be used in content. | `true` | | `global` | Boolean | Indicates whether the module is global or not | `false` | | `content_types` | Array | An `array` of [content types](/guides/cms/content/templates/overview) that the module can be used within. One or more of:<ul><li>`ANY`: any of the types listed below.</li><li>`LANDING_PAGE`: landing pages.</li><li>`SITE_PAGE`: website pages and templates.</li><li>`BLOG_POST`: blog posts and templates.</li><li>`BLOG_LISTING`: blog listing templates.</li><li>`EMAIL`: emails and email templates.</li><li>`KNOWLEDGE_BASE`: knowledge base pages and templates.</li><li>`QUOTE_TEMPLATE`: quote templates.</li><li>`CUSTOMER_PORTAL`: customer portal templates.</li><li>`WEB_INTERACTIVE`: web interactives.</li><li>`SUBSCRIPTION`: subscription templates.</li><li>`MEMBERSHIP`: membership templates.</li></ul>If a module is not to be used in any area within HubSpot, you would set the value to an empty array `[]` instead of `["NONE"]`.Note that this field was previously named `host_template_types`. Modules using the previous field name will continue to function, but it's recommended to use `content_types` moving forward. | | | `css_assets` | Array | An `array` of CSS files that the module depends on. Supports relative paths.e.g. `"css_assets": [{ "path": "../path/to/file.css" }]` | `[]` | | `css_render_options` | Object | Set whether the module CSS renders asynchronously with `async`: `true`, `false` | `{"async": false}` | | `js_assets` | Array | An `array` of JavaScript files that the module depends on. Supports relative paths.e.g. `"js_assets": [{ "path": "../path/to/file.js" }]` | `[]` | | `js_render_options` | Object | Modifies the module JavaScript tag added to the rendered page. Options include:<ul><li>`position`: `head` , `footer`</li><li>`async`: `true`, `false`</li><li>`defer`: `true`, `false`</li><li>`type`: `string`</li></ul> | `{"position":"footer"}` | | `inline_help_text` | String | Help text that will be shown at the top of the module in a blue info box (limit 400 characters).Provides information necessary to use the module. If you have field-specific help text information to convey, refer to the [help text field documentation](/reference/cms/fields/module-theme-fields#properties-used-by-all-fields). | `null` | | `master_language` | String | With [translations enabled](https://knowledge.hubspot.com/design-manager/create-translations-of-your-modules#:~:text=To%20create%20translations%20of%20a,the%20right%2C%20click%20Add%20translations.), the language code of the language the module's fields were originally written in.e.g. `en` | | | `placeholder` | Object | Sets [placeholder content](/reference/cms/hubl/tags/standard-tags#editor-placeholders) for the module. Includes the following properties:<ul><li>`show_module_icon`: whether the module icon displays. `true`, `false`.</li><li>`title`: the title that appears on the module in the editor. String.</li><li>`description`: the description that appears on the module in the editor. String.</li></ul> | | | `categories` | Array | An array containing up to three [module categories](#add-categories).For example: `"categories":["FORMS_AND_BUTTONS"]` | | | `content_tags` | Array | An array of [module tag objects](#adding-categories-and-tags) containing the tag name and `source` of `"USER"`.For example: `"content_tags": [{` `"name" : "BUTTONS",` `"source" : "USER"``}]` | | Below, learn more about individual module configuration options. ## Adding an Icon Modules can include an icon that appears in the [Design Manager](/guides/cms/tools/design-manager) and the page and email editors to provide visual context for content creators. It's recommended to have different icons for the different types of modules in your theme. Icons are [required for marketplace providers](/guides/cms/marketplace/template-guidelines). There are two ways to add an icon, through the [design manager](#add-an-icon-using-the-design-manager) or the [CMS CLI](#add-an-icon-using-the-cli). Module icons must be an `.svg` file and no larger in size than 10kb. For best results your icon should be simple and use only one color. Icons that use more than one color will be automatically converted for you. The default module icon that displays is a wrench and paint brush icon. To add an icon using the design manager: To add an icon when developing locally, open the module's `meta.json` file and add or edit the `icon` parameter's value to be an SVG from the file manager. ```json // meta.json { "global": false, "content_types": ["PAGE"], "icon": "http://example.com/hubfs/code.svg", "label": "Code block", "smart_type": "NOT_SMART", "is_available_for_new_content": true, "inline_help_text": "This module is for code snippets." } ``` ## Changing the label ## Making a module global For normal modules, the content of each instance of a module in a page, email or template is independent. For some use cases, it is useful to be able to have all instances of a module share the same content. When developing locally, you can make a module global by setting `global` to `true`. You can also [convert modules in a drag-and-drop template to global](https://knowledge.hubspot.com/design-manager/use-global-content-across-multiple-templates#create-new-global-content-in-the-design-manager:~:text=To%20convert%20an%20existing%20local%20module%20or%20group) using the design manager. ## Controlling where a module is available for use When developing locally, you can control which types of content a module can be used in through the `hostTemplateTypes` property. Learn more about the [available template types](/guides/cms/content/templates/overview#template-types). Modules also can be hidden so that they can't be added directly to pages through setting `is_available_for_new_content` to `false`. For example, this can be helpful for modules built for navigation menus and search. You can update this in the design manager by clicking the **Template type** option in the right sidebar. ## Adding CSS and JavaScript dependencies In addition to using `module.css` and `module.js` to add CSS and JavaScript that will be added to all pages that include a module instance, dependencies that are shared between modules can be attached using `css_assets` and `js_assets`. Paths can be absolute or relative to the `meta.json` file. ```json // meta.json { "css_assets": [{ "path": "../path/to/file.css" }], "js_assets": [{ "path": "../path/to/file.js" }] } ``` **Warning:** When using relative paths to reference dependencies, running `hs fetch --overwrite` to update the module for local development will overwrite the relative paths with absolute paths. Using the design manager, you can link CSS and JavaScript files to a module using the _Linked files_ section in the right sidebar of the module editor. ## Adding categories and tags You can assign categories and tags to modules to organize them within HubSpot tools: - [Categories](#categories): assign categories to a modules to organize them into groups within the content editor UI. This enables content creators to find modules more easily while building content in HubSpot. Note the following about categories: - A module can have up to three categories, which are pre-defined and cannot be customized. - Currently, categories are not surfaced in the content editor UI. However, you can assign categories for when categorization is available in editors. - Uncategorized modules will be available under an _All_ category. - [Tags](#tags): assign tags to organize modules within the design manager. This enables you to find modules more easily while building templates. In the design manager, you can assign categories and tags using the _Filter tags_ section of the right sidebar in the module editor. Learn more about the available categories and tags below. Locally, you can add categories and tags to a module's `meta.json` file as follows: ```json // meta.json { "label": "Custom module", "categories": ["image_and_video", "commerce"], "content_tags": [ { "name": "BUTTONS", "source": "USER" }, { "name": "CTA", "source": "USER" } ], "is_available_for_new_content": true } ``` ### Categories A module's `categories` array can contain up to three of the following categories (case-insensitive): | Category | Description | | --- | --- | | `blog` | Blog-specific modules, such as a recent post listing. | | `body_content` | Modules that are formatted to graphically showcase content, such as an image gallery. | | `commerce` | Commerce-specific modules, such as pricing cards. | | `design` | Modules that affect content structure and layout, such as accordions. | | `functionality` | Modules that include dynamic responses or behavior on the page, such as menus. | | `forms_and_buttons` | Modules that allow site visitors to input and submit data. | | `media` | Modules that contain elements such as images, icons, video, and banners. | | `social` | Social media-specific modules, such as social sharing. | | `text` | Modules that contain only text. | ### Tags A module's `content_tags` array can contain any of the following module tag objects (case sensitive): **Content types:** - `ACCORDION` - `ANIMATION` - `BLOG_POST` - `BUTTONS` - `CODE` - `CTA` - `FEED` - `FORM` - `ICON` - `IMAGE` - `LISTS` - `LOGO` - `MENU` - `RICH_TEXT` - `SLIDER` - `TEXT` - `VIDEO` **Functions:** - `BANNER` - `BLOG` - `BRANDING` - `CALCULATOR` - `CONVERSION` - `EMAIL` - `GALLERY` - `HEADERS` - `INTERACTION` - `LAYOUT` - `MAP` - `MULTIMEDIA` - `NAVIGATION` - `PROGRESS_BAR` - `SEARCH` - `SETTINGS` - `SOCIAL` - `TRANSLATION` # Default email modules @ Learn about the default modules that HubSpot provides for building email templates. These modules are separate from the default web modules, which are used to build website pages, blog posts, and blog listing pages. $ reference & cms/modules --- # Default email modules Below, learn about the modules that you can use when building email templates. These modules are separate from [default web modules](/reference/cms/modules/default-modules), which can be used to build website pages, blog posts, and blog listing pages. Many of the modules below were released to replace usage of default web modules in emails, such as the `email_logo` module replacing the `logo` module. If your email templates are still using the web versions of these modules, learn how to [update your email templates to use email-specific modules instead](/guides/cms/content/templates/default-email-modules). To view a default module's code, you can view and clone the module within the `@hubspot` folder of the design manager, or [fetch it](/guides/cms/tools/local-development-cli#fetch-files) by its path locally using the HubSpot CLI. ## Email blog post filter A version of the [blog post filter](#blog-post-filter) module for emails. ```hubl {% module "post_filter" path="@hubspot/email_post_filter" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `select_blog` | Blog | The blog to display posts from. | | | `filter_type` | Choice | Type of filtering links to show. Choices include:<ul><li>`tag`</li><li>`month`</li><li>`author`</li></ul> | `tag` | | `order_by` | Choice | Ordering for the values of filter links. Choices include:<ul><li>`post_count`</li><li>`name`</li></ul> | `post_count` | | `list_title` | Text | An H3 heading. | `"Posts by Tag"` | | `max_links` | Number | Number of filter links to show. Leave blank to show all. | `5` | | `expand_link_text` | Text | Text to display if more than the `max_links` value to display are available. | `"See all"` | ## Email blog post listing A version of the [blog post listing](#blog-post-listing) module for emails. ```hubl {% module "post_listing" path="@hubspot/email_post_listing" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `select_blog` | Blog | The blog to display posts from. | | | `listing_type` | Choice | The type of listing for your posts. Choices include:<ul><li>`recent`: most recent.</li><li>`popular_all_time`: most popular of all time.</li><li>`popular_past_year`: most popular the past year.</li><li>`popular_past_six_months`: most popular the past six months.</li><li>`popular_past_month`: most popular the past month.</li></ul> | `recent` | | `list_title` | Text | An H3 heading. | `"Recent Posts"` | | `max_links` | Number | Maximum number of posts to display. | `5` | ## Email Call-to-Action A version of the [Call-to-Action](#call-to-action) module for emails. ```hubl {% module "cta" path="@hubspot/email_cta" %} ``` | Parameter | Type | Description | | --------- | ------ | -------------------------------------- | | `guid` | String | Globally Unique Identifier of the CTA. | ## Email header A version of the [header](#header) module for emails. ```hubl {% module "email_header" path="@hubspot/email_header" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `value` | Text | Text for the heading. | `"A clear and bold header"` | | `header_tag` | Choice | Choose a heading level. Choice include `h1` through `h6`. | `h1` | ## Email HTML Raw HTML module for emails. ```hubl {% module "raw_html_email" path="@hubspot/raw_html_email" %} ``` | Parameter | Type | Description | Default | | --------- | ---- | ----------- | ------------------------------------------- | | `html` | HTML | HTML block. | ` \n Add custom HTML to your email.\n ` | ## Email image Image module for emails. ```hubl {% module "image_email" path="@hubspot/image_email" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `img` | Image | Image to be used for the email. | | | `link` | Text | Optional link for the image. | | | `alignment` | Choice | Alignment of the image. Choice include:<ul><li>`left`</li><li>`center`</li><li>`right`</li></ul> | `center` | ## Email linked image A version of the [image](#image) module for emails. ```hubl {% module "email_linked_image" path="@hubspot/email_linked_image" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `img` | Image | Image object containing:<ul><li>`src`: image url</li><li>`alt`: alt text for image</li><li>`loading`: lazy loading options include:<ul><li>`disabled`</li><li>`lazy`</li></ul></li><li>`width`: px value</li><li>`height`: px value</li></ul> | `{ "src": "https://static.hubspot.com/final/img/content/email-template-images/placeholder_200x200.png", "alt": "placeholder_200x200", "loading": "disabled", "width": 200, "height": 200 }` | | `link` | Text | Optional link for the image. | | | `target` | Boolean | Opens link in a new tab. | `false` | ## Email logo A version of the [logo](#logo) module for emails. ```hubl {% module "logo" path="@hubspot/email_logo" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `img` | Image | Image object containing:<ul><li>`override_inherited_src`: override the default logo from settings</li><li>`src`: image url</li><li>`alt`: alt-text for logo</li></ul> | `{ "override_inherited_src": false, "src": null, "alt": null }` | | `link` | Text | Optional link for the logo. If no url is specified, your logo will link to your primary domain. | | | `open_in_new_tab` | Boolean | Opens link in a new tab. | `false` | | `suppress_company_name` | Boolean | Hide the company name when an image is not selected. | `true` | | `heading_level` | Choice | Choose a heading level when no image is selected and `suppress_company_name` equals `false`. Choices include `h1` through `h6`. | `h1` | ## Main email body The main body module for emails. ```hubl {% module "email_body" path="@hubspot/email_body" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `html` | Rich Text | Default content for the email body. Supports HTML. See below for this field's default value. | | ```hubl <!-- Default html field value --> <p>Hi {{contact.firstname}},</p>\n<p>Describe what you have to offer the customer. Why should they read? What did you promise them in the subject line? Tell them something cool. Make them laugh. Make them cry. Well, maybe don't do that...</p>\n<p>Use a list to:</p>\n<ul>\n<li>Explain the value of your offer</li>\n<li>Remind the reader what they’ll get out of taking action</li>\n<li>Show off your skill with bullet points</li>\n<li>Make your content easy to scan</li>\n</ul>\n<p><a href=\"http://hubspot.com\">LINK TO A LANDING PAGE ON YOUR SITE</a> (This is the really important part.)</p>\n<p>Now wrap it all up with a pithy little reminder of how much you love them.</p>\n<p>Aw. You silver-tongued devil, you.</p>\n<p>Sincerely,</p>\n<p>Your name</p> ``` ## Office location information Office location information footer for emails (CAN-SPAM compliant). ```hubl {% module "email_can_spam" path="@hubspot/email_can_spam" %} ``` | Parameter | Type | Description | | --- | --- | --- | | `html` | Rich Text | Populates required CAN-SPAM information for emails including business address and unsubscribe/preferences links.See below for this field's default value. | ```hubl <!-- Default html field value --> <p id=\"footer\" style=\"font-family: Geneva, Verdana, Arial, Helvetica, sans-serif; text-align: center; font-size: 12px; line-height: 1.34em; color: {{ secondary_font_color }}; display: block;\">{{ site_settings.company_name }} {{ site_settings.company_street_address_1 }} {{ site_settings.company_street_address_2 }} {{ site_settings.company_city }} {{ site_settings.company_state }} {{ site_settings.company_zip }} {{ site_settings.company_country }} <br><br> You received this email because you are subscribed to {{ subscription_name }} from {{ site_settings.company_name }} . <br><br> Update your <a target=\"_blank\" href=\"{{ unsubscribe_link }}\" style=\"text-decoration: underline; whitespace: nowrap; color: {{ secondary_font_color }};\" data-unsubscribe=\"true\">email preferences</a> to choose the types of emails you receive. <br><br> <a target=\"_blank\" href=\"{{ unsubscribe_link_all }}\" style=\"text-decoration: underline; whitespace: nowrap; color: {{ secondary_font_color }};\" data-unsubscribe=\"true\">Unsubscribe from all future emails</a> </p> ``` ## Email one line of text A version of the [text](#one-line-of-text) module for emails. ```hubl {% module "text" path="@hubspot/email_text" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `value` | Text | Add your text to this parameter. | `"Some additional information in one line"` | ## Email section header A version of the [section header](#section-header) module for emails. ```hubl {% module "section_header" path="@hubspot/email_section_header" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `header` | Text | Section header content. | `"A clear and bold header"` | | `heading_level` | Choice | Heading level for the `header`. Choices include `h1` through `h6`. | `h1` | | `subheader` | Text | Subheading paragraph text for the section. | `"A more subdued subheader"` | ## Email social sharing A version of the [social sharing](#social-sharing) module for emails. ```hubl {% module "social_sharing" path="@hubspot/email_social_sharing" %} ``` Note: The variable `social_link_url` in the default column below is the same value as the `link` parameter. | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `link` | Text | This is the destination link that will be shortened for easier sharing on social networks. | | | `facebook` | Object | Object containing:<ul><li>`enabled`: boolean to enable social item</li><li>`custom_link_format`: custom URL for socials sharer URL</li></ul> | `{ "enabled": false, "custom_link_format": "http://www.facebook.com/share.php?u={{ social_link_url }}" }` | | `twitter` | Object | Object containing:<ul><li>`enabled`: boolean to enable social item</li><li>`custom_link_format`: custom URL for socials sharer URL</li></ul> | <code>\{ "enabled": false, "custom_link_format": "https://twitter.com/intent/tweet?original_referer=\{\{ social_link_url }}&url=\{\{ social_link_url }}&source=tweetbutton&text=\{\{ social_page_title\|urlencode }}" }</code> | | `linkedin` | Object | Object containing:<ul><li>`enabled`: boolean to enable social item</li><li>`custom_link_format`: custom URL for socials sharer URL</li></ul> | `{ "enabled": false, "custom_link_format": "http://www.linkedin.com/shareArticle?mini=true&url={{ social_link_url }}" }` | | `pinterest` | Object | Object containing:<ul><li>`enabled`: boolean to enable social item.</li><li>`custom_link_format`: custom URL for socials sharer URL.</li><li>`pinterest_media`: image object including:<ul><li>`src`: image URL.</li><li>`alt`: alt-text for the image.</li></ul></li></ul> | `{ "enabled": false, "custom_link_format": "http://pinterest.com/pin/create/button/?url={{ social_link_url }}&media={{ pinterest_media }}", "pinterest_media": { "src": "", "alt": null } }` | | `email` | Object | Object containing:<ul><li>`enabled`: boolean to enable social item</li><li>`custom_link_format`: custom URL for socials sharer URL</li></ul> | `{ "enabled": false, "custom_link_format": "mailto:?subject=Check out {{ social_link_url }} &body=Check out {{ social_link_url }}" }` | ## Email subscription preferences Module for displaying email subscription preferences. ```hubl {% module "email_subscriptions" path="@hubspot/email_subscriptions" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `header` | Text | H1 heading. | `header` | | `subheader_text` | Rich Text | Supplemental text for your H1 heading. | `"If this is not your email address, please ignore this page since the email associated with this page was most likely forwarded to you."` | | `unsubscribe_single_text` | Text | Preference selection help text. | `"Uncheck the types of emails you do not want to receive:"` | | `unsubscribe_all_text` | Text | Unsubscribe all help text. | `"Or check here to never receive any emails:"` | | `unsubscribe_all_unsubbed_text` | Text | Unsubscribe all help text for a currently unsubbed user. | `"You are presently unsubscribed from all of our emails. Would you like to receive our emails again?"` | | `unsubscribe_all_option` | Text | Label for unsubscribe all option. | `"Unsubscribe me from all mailing lists."` | | `button_text` | Text | Update preferences button text. | `"Update email preferences"` | | `resubscribe_button_text` | Text | Resubscribe button text. | `"Yes, resubscribe me!"` | ## Email subscriptions confirmation message Confirmation of email subscription changes. ```hubl {% module "email_subscriptions_confirmation" path="@hubspot/email_subscriptions_confirmation" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `header` | Text | H1 heading. | | | `subheader_text` | Rich Text | Supplemental text for your H1 heading. | `"If this is not your email address, please ignore this page since the email associated with this page was most likely forwarded to you."` | | `unsubscribe_all_success` | Text | Message on unsubscribe. | `"You have successfully unsubscribed from all email communications."` | | `subscription_update_success` | Text | Message on subscription update. | `"You have successfully updated your email preferences."` | ## Email unsubscribe (backup) Supported in pages, blog posts, and blog listings. ```hubl {% module "email_simple_subscription" path="@hubspot/email_simple_subscription" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `header` | Text | H1 heading. | `"Email Unsubscribe"` | | `input_help_text` | Text | H3 heading for help text. | `"Your email address:"` | | `input_placeholder` | Text | Placeholder content for the input field. | `"email@example.com"` | | `button_text` | Text | Text to display on the unsubscribe button. | `"Unsubscribe"` | ## Email video A video module for emails. ```hubl {% module "video_email" path="@hubspot/video_email" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `video_type` | Choice | Type of video. Choices include:<ul><li>`embed`: embed code from an external source.</li><li>`hubspot_video`: HubSpot hosted video.</li></ul> | `embed` | | `hubspot_video` | Video Player | HubSpot hosted video. Used when `video_type` equals `hubspot_video`. | | | `embed` | Object | Object containing `source_type`. Only value of `oembed` is available. | `{ "source_type": "oembed" }` | | `oembed_thumbnail` | Image | Override oembed thumbnail image when `video_type` equals `embed` and `embed_field` equals `oembed`. | `{"size_type": "exact"}` | | `style_options` | Object | Object containing:<ul><li>`play_button_color`: color hex code.</li><li>`play_button_scale`: number 0-100</li></ul> | `{ {"play_button_color":{ "color":"#2f4254", "opacity":100},"play_button_scale" : 30} }` | | `alignment` | Choice | Alignment of video. Choices include:<ul><li>`left`</li><li>`center`</li><li>`right`</li></ul> | `center` | # Default module versioning @ Learn about versioning, and how to develop a theme to account for multiple versions of assets. $ reference & cms/modules --- # Default module versioning Releasing a new version of a module, rather than update the existing version, enables HubSpot to make significant updates without impacting existing content. For example, HubSpot may release a new version of a particular module to better organize fields and provide more styling options, or to improve functionality by adding JavaScript. New versions of modules are first released to new HubSpot accounts created after a certain date. This means that different accounts may be using different versions of the same default module. If multiple accounts are using a theme that you built, you'll need to take module versioning into consideration so that the theme works as expected across all accounts. Below, learn more about how to develop your theme with versioning in mind, and which modules have multiple versions. ## How versioning works HubSpot's [existing default modules](/reference/cms/modules/default-modules) are treated as `v0` modules, while new versions are released started at `v1`. For compatibility, HubSpot automatically maps `v0` fields to `v1` fields, which means that references to module fields are forward compatible. However, field references are <u>not</u> backwards compatible, meaning that `v1` field references won't map to `v0` fields. To account for changes in an module's HTML, CSS, and JavaScript, you can use conditional statements to configure it depending on the account's available versions. To add add a conditional statement, use `get_asset_version("@hubspot/module-name")` within an HTML, CSS, or JavaScript file. The module's version will be returned as a string, enabling you to configure module options based on the returned version. ```hubl {% if get_asset_version("@hubspot/module-name") == "1" %} hr { border-bottom-width: 5px; } {% endif %} ``` ## Specifying versions in themes When developing a theme, you can specify which module versions that you want to support within the `theme.json` file. When a version is specified, content editors will only allow access to that version in the editor, and the template will render that version accordingly. On upload via the CLI, or when making changes in the design manager, specified module versions will be validated to ensure validity of the modules and their version definitions. This feature is not available for child themes. Instead, child themes will inherit module versions based on the parent theme when provided. You can specify supported module versions in `theme.json` by including a `pinned_module_versions` object. For each module, you can specify a version or a range of versions. For example, the code below would result in the theme allowing access to either `v0` or `v1` of the divider module, but only `v1` of the search input module. ```json "pinned_module_versions": { "@hubspot/divider": "0-1", "@hubspot/search_input": "0" } ``` Any default modules not included in the `pinned_module_versions` object will not be restricted to a particular version. In addition, you shouldn't specify versions of default modules that don't currently support versioning, and you can only specify versions that are available (e.g. you can't specify `2` for a module that doesn't have a `v2`). Learn more about submitting to [HubSpot's Template Marketplace](/guides/cms/marketplace/template-guidelines). ## Manage an account's module versions In an account's _Themes & Modules_ settings (**Settings** >** Content** > **Themes & Modules**) users can opt into new module versions, revert  modules updated within the past 30 days, and view the version information for all default modules. ### Update a module's version To opt into a new version of a module: - In your HubSpot account, click the **settings icon** in the top navigation bar. - In the left sidebar menu, navigate to **Content** > **Themes & Modules**. - On the _Alerts_ tab, the latest updates for default modules will appear with an *Update Available* tag. Click **View update** next to a module to view more details about the update. - In the panel, review the changes. The panel will include a high-level summary of the update, along with a detailed breakdown of the changes and the assets that will be affected. - To update the module, click **Install update**. Then, in the dialog box, click **Confirm update**. Note that it can take up to 10 minutes for pages and templates to reflect the updated module. After updating the module, you'll be taken to the _Modules_ tab of the _Themes & Modules_ settings page. On this page, you can see recently updated modules, modules with available updates, and version information for all your account's default modules. ### Revert an updated module Within 30 days of updating a module, you can revert it to a previous version if needed: - On the *Modules* tab, next to the module that you want to revert, click **Roll back recent update**. - In the panel, review the module's version history, then click **Roll back this update**. - Review the information in the right sidebar to confirm that you'd like to roll back the update, then click **Roll back**. - In the dialog box, click **Confirm**. After reverting the update, it may take some time for your templates and pages to reflect the reverted module. At any point, you can update the module again by following the [update steps above](#update-a-module-s-version). ## Modules with version support Below, learn more about the modules that currently support versioning. - [Divider](#divider) - [Language switcher](#language-switcher) - [Post listing](#post-listing) - [Search input](#search-input) - [Search results](#search-results) ## Divider A new version of the [default divider module](/reference/cms/modules/default-modules#divider) has been released to accounts created after August 25th, 2022 ([changelog announcement](https://developers.hubspot.com/changelog/divider-module-v1)). This version update impacts the following module files: - `fields.json` - `module.html` Below, learn more about the new `v1` version updates in each file, along with the original `v0` version for comparison. ### v1 notes - All fields that were previously in the _Content_ tab have been moved to the _Styles_ tab. - The following fields have moved, but HubSpot automatically links the previous references to the new field names, so you don't need to make any manual updates: - The `height` field has been moved to `styles.line.thickness`. - The `width` field has been moved to `styles.size.width`. - The `color` field has been moved to `styles.line.color`. - The `line` type field has been moved to `styles.line.style`. - The `alignment` field, which was previously a choice field, has been replaced by a new field type of _Alignment_, and can be accessed under `styles.alignment.alignment`. - The `padding` field, which was previously a number field, has been replaced by a new field type of _Spacing_, and can be accessed under `styles.spacing.spacing`. - The `show_padding` field has been removed. ### v1 ```json // v1 [ { "label": "Styles", "name": "styles", "type": "group", "tab": "STYLE", "children": [ { "label": "Line", "name": "line", "type": "group", "children": [ { "label": "Color", "name": "color", "type": "color", "required": true, "aliases": ["color"], "default": { "color": "#000000", "opacity": 100 } }, { "label": "Style", "name": "style", "type": "choice", "choices": [ ["solid", "Solid"], ["dotted", "Dotted"], ["dashed", "Dashed"] ], "display": "select", "required": true, "aliases": ["line_type"], "default": "solid" }, { "label": "Thickness", "name": "thickness", "type": "number", "display": "text", "max": 10, "min": 1, "required": true, "step": 1, "default": 1, "aliases": ["height"], "suffix": "px" } ] }, { "label": "Size", "name": "size", "type": "group", "children": [ { "label": "Width", "name": "width", "id": "styles.size.width", "type": "number", "help_text": "Percentage of the area the divider is placed into.", "max": 100, "min": 1, "required": true, "step": 1, "suffix": "%", "aliases": ["width"], "default": 50 } ] }, { "label": "Alignment", "name": "alignment", "type": "group", "visibility": { "controlling_field": "styles.size.width", "controlling_value_regex": "100", "operator": "NOT_EQUAL" }, "children": [ { "label": "Alignment", "name": "alignment", "type": "alignment", "default": { "horizontal_align": "CENTER" }, "alignment_direction": "HORIZONTAL" } ] }, { "label": "Spacing", "name": "spacing", "type": "group", "children": [ { "label": "Spacing", "name": "spacing", "type": "spacing", "visibility": { "hidden_subfields": { "padding": true } }, "default": { "margin": { "top": { "value": "10", "units": "px" }, "bottom": { "value": "10", "units": "px" } } } } ] } ] } ] ``` ```html {# v1 #} {# Module styles #} {% require_css %} <style> {% scope_css %} hr { border: 0 none; border-bottom-width: {{ module.styles.line.thickness ~ "px" }}; border-bottom-style: {{ module.styles.line.style }}; border-bottom-color: rgba({{ module.styles.line.color.color|convert_rgb }}, {{ module.styles.line.color.opacity / 100 }}); margin-left: {{ "auto" if (module.styles.alignment.alignment.horizontal_align == "CENTER" or module.styles.alignment.alignment.horizontal_align == "RIGHT") else "0" }}; margin-right: {{ "auto" if (module.styles.alignment.alignment.horizontal_align == "CENTER" or module.styles.alignment.alignment.horizontal_align == "LEFT") else "0" }}; {% if module.styles.spacing.spacing.css %} {{ module.styles.spacing.spacing.css }} {% endif %} width: {{ module.styles.size.width ~ "%" }}; } {% end_scope_css %} </style> {% end_require_css %} {# Divider #} <hr /> ``` ### v0 ```json // v0 [ { "label": "Height", "name": "height", "id": "f25c7200-c134-2cd0-ebac-2f2e7152c0a9", "type": "number", "display": "text", "max": 8, "min": 1, "required": true, "step": 1, "suffix": "px", "default": 1 }, { "label": "Width", "name": "width", "id": "69957b03-2442-9cc8-3666-c67e96f37645", "type": "number", "help_text": "Percentage of the area the divider is placed into.", "display": "slider", "max": 100, "min": 1, "required": true, "step": 1, "suffix": "%", "default": 50 }, { "label": "Color", "name": "color", "id": "a3f3e26c-5bca-611e-ea99-c91d4f1c88bb", "type": "color", "required": true, "default": { "color": "#000000", "opacity": 100 } }, { "label": "Line type", "name": "line_type", "id": "5dfe1bf6-99b3-5339-3a05-db1aee413317", "type": "choice", "choices": [ ["solid", "Solid"], ["dotted", "Dotted"], ["dashed", "Dashed"] ], "display": "select", "required": true, "default": "solid" }, { "label": "Alignment", "name": "alignment", "id": "2919e8d6-68f4-9cfc-5253-8e36d3f54807", "type": "choice", "choices": [ ["left", "Left"], ["center", "Center"], ["right", "Right"] ], "display": "select", "required": true, "default": "center" }, { "label": "Show padding?", "name": "show_padding", "id": "c2849a01-3d7b-a5b7-8d80-969f2a3e65f0", "type": "boolean", "default": false }, { "label": "Padding", "name": "padding", "id": "14154310-667e-8128-0394-96c83342c964", "type": "number", "inline_help_text": "Max of 20 pixels", "visibility": { "controlling_field": "c2849a01-3d7b-a5b7-8d80-969f2a3e65f0", "controlling_value_regex": "true", "operator": "EQUAL" }, "display": "text", "max": 20, "min": 1, "required": true, "step": 1, "suffix": "px", "default": 5 } ] ``` ```html {# v0 #} {% macro buildStyles() %} width: {{ module.width ~ "%" }}; border: 0 none; border-bottom-width: {{ module.height ~ "px" }}; border-bottom-style: {{ module.line_type }}; border-bottom-color: rgba({{ module.color.color|convert_rgb }}, {{ module.color.opacity / 100 }}); margin-left: {{ "auto" if (module.alignment == "center" or module.alignment == "right") else "0" }}; margin-right: {{ "auto" if (module.alignment == "center" or module.alignment == "left") else "0" }}; margin-top: {{ module.padding ~ "px" if module.show_padding else "0" }}; margin-bottom: {{ module.padding ~ "px" if module.show_padding else "0" }}; {% endmacro %} <hr style="{{ buildStyles() }}" /> ``` ## Language switcher A new version of the [language switcher module](/reference/cms/modules/default-modules#language-switcher) has been released. This version update impacts the following module files: - `meta.json` - `fields.json` - `module.html` - `module.css` - `module.js` Below, learn more about the new `v1` version updates in each file, along with the original `v0` version for comparison. ### v1 notes A `placeholder` field has been added with the following options: - `show_module_icon` (boolean): when set to `false`, no icon will appear. - `title` (string): the placeholder title. - `description` (string): the placeholder descriptive text. ### v1 ```json // meta.json { "label": "Language Switcher", "host_template_types": ["PAGE", "BLOG_POST", "BLOG_LISTING"], "icon": "../img/icon/edited-language.svg", "extra_classes": "widget-type-language_switcher", "smart_type": "NOT_SMART", "master_language": "en", "placeholder": { "show_module_icon": true, "title": "No language variant setup", "description": "This page does not have a language variant setup." }, "categories": ["functionality"], "content_tags": [ { "name": "OTHER_INDUSTRY", "source": "MARKETPLACE" }, { "name": "COMPATIBLE_PAGE", "source": "MARKETPLACE" } ], "editable_contexts": ["TEMPLATE"] } ``` ```json // fields.json [ { "label": "Add chevron down", "name": "add_chevron_down", "type": "boolean", "default": true }, { "label": "Display mode", "name": "display_mode", "type": "choice", "help_text": "Allows you to choose in which language the languages of your pages are displayed on your website. <a href='https://knowledge.hubspot.com/website-pages/create-pages-in-multiple-languages#add-a-language-switcher-to-your-template' target='blank' rel='noopener noreferrer'>Learn more</a>", "choices": [ ["pagelang", "Page language"], ["localized", "Localized"], ["hybrid", "Hybrid"] ], "display": "select", "placeholder": "Search", "required": true, "default": "localized" }, { "label": "Icon options", "name": "icon_options", "type": "choice", "choices": [ ["none", "None"], ["icon", "Icon"], ["custom_icon", "Custom icon"] ], "display": "select", "placeholder": "", "default": "icon" }, { "label": "Icon", "name": "icon", "type": "icon", "visibility": { "controlling_field_path": "icon_options", "controlling_value_regex": "icon", "operator": "EQUAL" }, "default": { "name": "globe", "type": "SOLID", "unicode": "f0ac" } }, { "label": "Custom Icon", "name": "custom_icon", "type": "image", "visibility": { "controlling_field_path": "icon_options", "controlling_value_regex": "custom_icon", "operator": "EQUAL" }, "resizable": false, "responsive": false, "show_loading": true, "default": { "size_type": "auto", "src": "", "alt": null, "loading": "lazy" } }, { "label": "Styles", "name": "styles", "type": "group", "tab": "STYLE", "children": [ { "label": "Button", "name": "group_button", "type": "group", "children": [ { "label": "Icon", "name": "group_icon", "type": "group", "children": [ { "label": "Size", "name": "size", "type": "number", "display": "text", "max": 100, "min": 1, "step": 1, "suffix": "px", "default": 22 }, { "label": "Spacing", "name": "spacing", "type": "number", "help_text": "Spacing between the icon and button text.", "display": "text", "max": 100, "step": 1, "suffix": "px", "default": 15 }, { "label": "Color", "name": "color", "type": "color", "visibility": { "controlling_field_path": "icon_options", "controlling_value_regex": "icon", "operator": "EQUAL" }, "default": { "color": "#000000", "opacity": 100 } } ] }, { "label": "Chevron", "name": "group_chevron", "type": "group", "visibility": { "controlling_field_path": "add_chevron_down", "controlling_value_regex": "true", "operator": "EQUAL" }, "children": [ { "label": "Size", "name": "size", "type": "number", "help_text": "Size of the chevron icon.", "display": "text", "max": 100, "min": 1, "step": 1, "suffix": "px", "default": 13 }, { "label": "Spacing", "name": "spacing", "type": "number", "help_text": "Spacing between the chevron and button text.", "display": "text", "max": 100, "step": 1, "suffix": "px", "default": 15 }, { "label": "Color", "name": "color", "type": "color", "default": { "color": "#000000", "opacity": 100 } } ], "default": {} }, { "label": "Text", "name": "group_text", "type": "group", "children": [ { "label": "Font", "name": "font", "type": "font", "default": { "size_unit": "px" } }, { "label": "Transform", "name": "transform", "type": "choice", "choices": [ ["capitalize", "Capitalize"], ["uppercase", "Uppercase"], ["lowercase", "Lowercase"] ], "display": "select", "placeholder": "None" } ] }, { "label": "Background", "name": "group_background", "type": "group", "children": [ { "label": "Color", "name": "color", "type": "color" } ] }, { "label": "Border", "name": "group_border", "type": "group", "children": [ { "label": "Border", "name": "border", "type": "border" } ] }, { "label": "Corner", "name": "group_corner", "type": "group", "children": [ { "label": "Radius", "name": "radius", "type": "number", "display": "text", "max": 100, "min": 0, "step": 1, "suffix": "px" } ] }, { "label": "Spacing", "name": "group_spacing", "type": "group", "children": [ { "label": "Spacing", "name": "spacing", "type": "spacing" } ] }, { "label": "Hover", "name": "group_hover", "type": "group", "children": [ { "label": "Icon", "name": "group_icon", "type": "group", "children": [ { "label": "Color", "name": "color", "type": "color", "visibility": { "controlling_field_path": "icon_options", "controlling_value_regex": "icon", "operator": "EQUAL" } } ] }, { "label": "Chevron", "name": "group_chevron", "type": "group", "children": [ { "label": "Color", "name": "color", "type": "color", "visibility": { "controlling_field_path": "add_chevron_down", "controlling_value_regex": "true", "operator": "EQUAL" } } ], "default": {} }, { "label": "Text", "name": "group_text", "type": "group", "children": [ { "label": "Font", "name": "font", "type": "font", "visibility": { "hidden_subfields": { "size": true, "font": true } } } ] }, { "label": "Background", "name": "group_background", "type": "group", "children": [ { "label": "Color", "name": "color", "type": "color" } ] }, { "label": "Border", "name": "group_border", "type": "group", "children": [ { "label": "Border", "name": "border", "type": "border" } ] } ] }, { "label": "Alignment", "name": "group_alignment", "type": "group", "children": [ { "label": "Alignment", "name": "alignment", "type": "alignment", "alignment_direction": "HORIZONTAL", "default": { "horizontal_align": "CENTER" } } ] } ] }, { "label": "Dropdown", "name": "group_dropdown", "type": "group", "children": [ { "label": "Text", "name": "group_text", "type": "group", "children": [ { "label": "Font", "name": "font", "type": "font", "default": { "size_unit": "px" } }, { "label": "Transform", "name": "transform", "type": "choice", "choices": [ ["capitalize", "Capitalize"], ["uppercase", "Uppercase"], ["lowercase", "Lowercase"] ], "display": "select", "placeholder": "None" } ] }, { "label": "Background", "name": "group_background", "type": "group", "children": [ { "label": "Color", "name": "color", "type": "color" } ] }, { "label": "Border", "name": "group_border", "type": "group", "children": [ { "label": "Border", "name": "border", "type": "border", "default": { "top": { "width": { "value": 1, "units": "px" }, "opacity": 100, "style": "solid", "color": "#000000" }, "bottom": { "width": { "value": 1, "units": "px" }, "opacity": 100, "style": "solid", "color": "#000000" }, "left": { "width": { "value": 1, "units": "px" }, "opacity": 100, "style": "solid", "color": "#000000" }, "right": { "width": { "value": 1, "units": "px" }, "opacity": 100, "style": "solid", "color": "#000000" } } } ] }, { "label": "Box shadow", "name": "group_box_shadow", "type": "group", "children": [ { "label": "Add box shadow", "name": "add_box_shadow", "type": "boolean", "default": false }, { "label": "Offset x", "name": "offset_x", "type": "number", "help_text": "Specifies the horizontal position of the shadow. Negative values shift the shadow left. Positive values shift the shadow right.", "visibility": { "controlling_field_path": "styles.group_dropdown.group_box_shadow.add_box_shadow", "controlling_value_regex": "true", "operator": "EQUAL" }, "display": "text", "max": 100, "min": -100, "step": 1, "default": 2 }, { "label": "Offset y", "name": "offset_y", "type": "number", "help_text": "Specifies the vertical position of the shadow. Negative values shift the shadow up. Positive values shift the shadow down.", "visibility": { "controlling_field_path": "styles.group_dropdown.group_box_shadow.add_box_shadow", "controlling_value_regex": "true", "operator": "EQUAL" }, "display": "text", "max": 100, "min": -100, "step": 1, "default": 2 }, { "label": "Blur radius", "name": "blur_radius", "type": "number", "help_text": "Controls the blur of the shadow. A larger value makes the shadow bigger and lighter. Negative values are not allowed.", "visibility": { "controlling_field_path": "styles.group_dropdown.group_box_shadow.add_box_shadow", "controlling_value_regex": "true", "operator": "EQUAL" }, "display": "text", "max": 100, "min": 0, "step": 1, "default": 6 }, { "label": "Spread radius", "name": "spread_radius", "type": "number", "help_text": "Adjusts the overall size of the shadow. Positive values will cause the shadow to expand. Negative values will cause the shadow to shrink.", "visibility": { "controlling_field_path": "styles.group_dropdown.group_box_shadow.add_box_shadow", "controlling_value_regex": "true", "operator": "EQUAL" }, "display": "text", "max": 100, "min": -100, "step": 1, "default": 1 }, { "label": "Color", "name": "color", "type": "color", "help_text": "Controls the color of the shadow.", "visibility": { "controlling_field_path": "styles.group_dropdown.group_box_shadow.add_box_shadow", "controlling_value_regex": "true", "operator": "EQUAL" }, "default": { "color": "#000000", "opacity": 10 } } ], "default": {} }, { "label": "Corner", "name": "group_corner", "type": "group", "children": [ { "label": "Radius", "name": "radius", "type": "number", "display": "text", "max": 100, "min": 0, "step": 1, "suffix": "px" } ] }, { "label": "Spacing", "name": "group_spacing", "type": "group", "children": [ { "label": "Top margin", "name": "margin", "type": "number", "help_text": "Adjusts the margin between the dropdown and the button.", "display": "text", "max": 100, "step": 1, "suffix": "px" }, { "label": "", "name": "item_spacing", "type": "spacing", "visibility": { "hidden_subfields": { "margin": true } } } ] }, { "label": "Hover", "name": "group_hover", "type": "group", "children": [ { "label": "Text", "name": "group_text", "type": "group", "children": [ { "label": "Font", "name": "font", "type": "font", "visibility": { "hidden_subfields": { "size": true, "font": true } } } ] }, { "label": "Background", "name": "group_background", "type": "group", "children": [ { "label": "Color", "name": "color", "type": "color" } ] } ] }, { "label": "Alignment", "name": "group_alignment", "type": "group", "children": [ { "label": "Alignment", "name": "alignment", "type": "alignment", "alignment_direction": "HORIZONTAL", "default": { "horizontal_align": "LEFT" } } ] } ] } ] }, { "label": "Default Text", "name": "default_text", "type": "group", "locked": true, "children": [ { "label": "Select your language", "name": "select_your_language", "type": "text", "locked": true, "default": "Select your language" } ] } ] ``` ```html {# Language switcher - version 1 #} {% set button = module.styles.group_button %} {% set menu = module.styles.group_dropdown %} {% set languages = language_variants(module.display_mode) %} {% set activeLanguage = languages|selectattr('isActive', true)|first %} {% macro build_module_styles() %} {% require_css %} <style> {% scope_css %} .hs-language-switcher { text-align: {{ button.group_alignment.alignment.horizontal_align|lower }}; } .hs-language-switcher__button { {{ button.group_text.font.css }} {% if button.group_text.transform %} text-transform: {{ button.group_text.transform }}; {% endif %} {% if button.group_background.color.color %} background-color: rgba({{ button.group_background.color.color|convert_rgb }}, {{ button.group_background.color.opacity / 100 }}); {% endif %} {{ button.group_border.border.css }} {% if button.group_corner.radius %} border-radius: {{ button.group_corner.radius ~ 'px' }}; {% endif %} {% if button.group_spacing.spacing.padding.left.value or button.group_spacing.spacing.padding.right.value or button.group_spacing.spacing.padding.top.value or button.group_spacing.spacing.padding.bottom.value %} padding-left: {{ button.group_spacing.spacing.padding.left.value|default(0, true) ~ "px" }}; padding-bottom: {{ button.group_spacing.spacing.padding.bottom.value|default(0, true) ~ "px" }}; padding-top: {{ button.group_spacing.spacing.padding.top.value|default(0, true) ~ "px" }}; padding-right: {{ button.group_spacing.spacing.padding.right.value|default(0, true) ~ "px" }}; {% endif %} } .hs-language-switcher__inner-wrapper { margin-top: {{ button.group_spacing.spacing.margin.top.value|default(0, true) ~ "px"}}; margin-bottom: {{ button.group_spacing.spacing.margin.bottom.value|default(0, true) ~ "px" }}; } .hs-language-switcher__button:hover, .hs-language-switcher.menu-open .hs-language-switcher__button { {{ button.group_hover.group_text.font.css }} {% if button.group_hover.group_background.color.color %} background-color: rgba({{ button.group_hover.group_background.color.color|convert_rgb }}, {{ button.group_hover.group_background.color.opacity / 100 }}); {% endif %} {{ button.group_hover.group_border.border.css }} } {% if module.add_chevron_down %} .hs-language-switcher__icon--dropdown { margin-left: {{ button.group_chevron.spacing ~ "px" }}; margin-right: 0; display: block; width: {{ button.group_chevron.size ~ "px" }}; height: auto; fill: rgba({{ button.group_chevron.color.color|convert_rgb }}, {{ button.group_chevron.color.opacity / 100 }}); transition: transform .2s ease; } .hs-language-switcher__icon--dropdown.active { transform: rotate(180deg); } {% if button.group_hover.group_chevron.color.color %} .hs-language-switcher__button:hover .hs-language-switcher__icon--dropdown { fill: rgba({{ button.group_hover.group_chevron.color.color|convert_rgb }}, {{ button.group_hover.group_chevron.color.opacity / 100 }}); } {% endif %} {% endif %} {% if module.icon_options == "custom_icon" and module.custom_icon.src %} .hs-language-switcher__icon--custom { width: {{ button.group_icon.size ~ "px" }}; max-width: 40px; height: auto; margin-right: {{ button.group_icon.spacing ~ "px" }}; } {% endif %} {% if (module.icon_options == "icon" and module.icon) or (module.icon_options == "custom_icon" and module.custom_icon.src )%} .hs-language-switcher__icon { display: flex; height: auto; width: {{ button.group_icon.size ~ "px" }}; fill: rgba({{ button.group_icon.color.color|convert_rgb }}, {{ button.group_icon.color.opacity / 100 }}); margin-right: {{ button.group_icon.spacing ~ "px" }}; } .hs-language-switcher__inner-wrapper:hover .hs-language-switcher__icon, .hs-language-switcher.menu-open .hs-language-switcher__icon { {% if button.group_hover.group_icon.color.color %} fill: rgba({{ button.group_hover.group_icon.color.color|convert_rgb }}, {{ button.group_hover.group_icon.color.opacity / 100 }}); {% endif %} } {% endif %} {# Menu Options #} .hs-language-switcher__menu { {% if menu.group_background.color.color %} background-color: rgba({{ menu.group_background.color.color|convert_rgb }}, {{ menu.group_background.color.opacity / 100 }}); {% endif %} {{ menu.group_border.border.css }} {% if menu.group_box_shadow.add_box_shadow %} box-shadow: {{ menu.group_box_shadow.offset_x ~ "px" }} {{ menu.group_box_shadow.offset_y ~ "px" }} {{ menu.group_box_shadow.blur_radius ~ "px" }} {{ menu.group_box_shadow.spread_radius ~ "px" }} rgba({{ menu.group_box_shadow.color.color|convert_rgb }}, {{ menu.group_box_shadow.color.opacity / 100 }}); {% endif %} {% if menu.group_corner.radius %} border-radius: {{ menu.group_corner.radius ~ 'px' }}; {% endif %} margin-top: {{ menu.group_spacing.margin }}px; {% if menu.group_alignment.alignment.horizontal_align == "RIGHT" %} right: 0; {% elif menu.group_alignment.alignment.horizontal_align == "CENTER" %} left: 50%; transform: translateX(-50%); {% else %} left: 0; {% endif %} } .hs-language-switcher__item { {% if menu.group_spacing.item_spacing.css %} {{ menu.group_spacing.item_spacing.css }} {% endif %} } .hs-language-switcher__item a { text-align: {{ menu.group_alignment.alignment.horizontal_align|lower }}; {{ menu.group_text.font.css }} {% if menu.group_text.transform %} text-transform: {{ menu.group_text.transform }}; {% endif %} } .hs-language-switcher__item:focus, .hs-language-switcher__item.active, .hs-language-switcher__menu.mousemove .hs-language-switcher__item:hover { {% if menu.group_hover.group_background.color.color %} background: rgba({{ menu.group_hover.group_background.color.color|convert_rgb }}, {{ menu.group_hover.group_background.color.opacity / 100 }}); {% endif %} } .hs-language-switcher__item:focus a, .hs-language-switcher__item.active a, .hs-language-switcher__menu.mousemove .hs-language-switcher__item:hover a { {{ menu.group_hover.group_text.font.css }} } {% end_scope_css %} </style> {% end_require_css %} {% endmacro %} {% macro build_custom_icon() %} {% set sizeAttrs = 'width="{{ button.group_icon.size }}" height="{{ button.group_icon.size }}"' %} {% set image_src = resize_image_url(module.custom_icon.src, 0, 0, 50) %} {% set loadingAttr = module.custom_icon.loading != 'disabled' ? 'loading="{{ module.custom_icon.loading }}"' : '' %} <img class="hs-language-switcher__icon--custom" src="{{ image_src }}" alt="{{ module.custom_icon.alt }}" {{ loadingAttr }} {{ sizeAttrs }} /> {% endmacro %} {% macro build_chevron_down() %} <svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" class="hs-language-switcher__icon--dropdown" aria-hidden="true" > <g> <path d="M207.029 381.476L12.686 187.132c-9.373-9.373-9.373-24.569 0-33.941l22.667-22.667c9.357-9.357 24.522-9.375 33.901-.04L224 284.505l154.745-154.021c9.379-9.335 24.544-9.317 33.901.04l22.667 22.667c9.373 9.373 9.373 24.569 0 33.941L240.971 381.476c-9.373 9.372-24.569 9.372-33.942 0z" ></path> </g> </svg> {% endmacro %} {# When page is first created, languages will be null. After the language for the page is set, languages will contain one language, so both checks are required #} {% if is_in_editor && (!languages || languages|length <= 1) %} {% editor_placeholder %} {% elif languages|length >= 2 %} {{- build_module_styles() -}} <nav class="hs-language-switcher" aria-label="{{ module.default_text.select_your_language }}" > </nav> {% endif %} ``` ```css .hs-language-switcher { display: block; } .hs-language-switcher__inner-wrapper { position: relative; display: inline-block; } /* Button */ .hs-language-switcher__button { cursor: pointer; } .hs-language-switcher__label { display: flex; align-items: center; font-size: 1em; line-height: 1; } .hs-language-switcher__icon { height: 20px; width: 20px; } /* Menu */ .hs-language-switcher__menu { position: absolute; top: 100%; display: none; overflow: hidden; box-sizing: border-box; min-width: 100%; padding: 0; margin: 0; background: #fff; list-style-type: none; white-space: nowrap; } .hs-language-switcher__menu.visible { display: block; } .hs-language-switcher__item { width: 100%; padding: 10px 20px; margin: 0; text-align: left; } .hs-language-switcher__item:focus, .hs-language-switcher__item.active, .hs-language-switcher__menu.mousemove .hs-language-switcher__item:hover { background: #efefef; } .hs-language-switcher__item a { display: block; height: 100%; color: #000; text-decoration: none; } ``` ```js /* eslint-disable no-use-before-define */ const KEYS = { SPACE: ' ', ENTER: 'Enter', TAB: 'Tab', ESCAPE: 'Escape', UP: 'Up', // IE11 & Edge 16 value for Arrow Up ARROW_UP: 'ArrowUp', DOWN: 'Down', // IE11 & Edge 16 value for Arrow Down ARROW_DOWN: 'ArrowDown', }; const HsLanguageSwitcher = (element) => { const button = element.querySelector('.hs-language-switcher__button'); const menu = element.querySelector('.hs-language-switcher__menu'); const options = Array.from(menu.children); const chevron = element.querySelector( '.hs-language-switcher__icon--dropdown' ); let selectedIndex = -1; let optionMousemoveHandlers = []; function onDocumentClick(e) { const target = e.target; if (element === target || !element.contains(target)) { hideMenu(); } } function onKeyDown(e) { menu.classList.remove('mousemove'); switch (e.key) { case KEYS.SPACE: case KEYS.ENTER: if (selectedIndex >= 0) { e.preventDefault(); const selectedOption = options[selectedIndex]; window.location.href = selectedOption.children[0].href; } break; case KEYS.DOWN: case KEYS.ARROW_DOWN: e.preventDefault(); selectNextOption(); break; case KEYS.UP: case KEYS.ARROW_UP: e.preventDefault(); selectPreviousOption(); break; case KEYS.TAB: case KEYS.ESCAPE: hideMenu(); break; default: // nothing } } function onOptionMousemove(index) { menu.classList.add('mousemove'); selectedIndex = index; updateSelectedOption(); } function showMenu() { selectedIndex = 0; menu.classList.add('visible'); button.setAttribute('aria-expanded', true); element.addEventListener('keydown', onKeyDown); element.classList.add('menu-open'); options.forEach((option, index) => { /* * The mousemove handler has custom arguments, given via bind * Bind returns a new instance of the function, so in order to remove the event listener handler from the element, * the references must be stored in an array for removal later */ const handler = onOptionMousemove.bind(null, index); optionMousemoveHandlers.push(handler); option.addEventListener('mousemove', handler); }); if (chevron) { rotateChevron(); } document.addEventListener('click', onDocumentClick); updateSelectedOption(); } function hideMenu() { // Hide the dropdown menu.classList.remove('visible'); button.setAttribute('aria-expanded', false); element.classList.remove('menu-open'); options.forEach((option, index) => { // Remove class for option background highlight option.classList.remove('active'); // Remove mousemove event handlers stored in array option.removeEventListener('mousemove', optionMousemoveHandlers[index]); }); if (chevron) { rotateChevron(); } optionMousemoveHandlers = []; element.removeEventListener('keydown', onKeyDown); document.removeEventListener('click', onDocumentClick); button.focus(); } function toggleMenu() { if (menu.classList.contains('visible')) { hideMenu(); } else { showMenu(); } } function selectNextOption() { if (selectedIndex + 1 === options.length) { selectedIndex = 0; } else { selectedIndex += 1; } updateSelectedOption(); } function selectPreviousOption() { if (selectedIndex - 1 < 0) { selectedIndex = options.length - 1; } else { selectedIndex -= 1; } updateSelectedOption(); } function updateSelectedOption() { options.forEach((option, index) => { if (index === selectedIndex) { option.classList.add('active'); option.children[0].focus(); } else { option.classList.remove('active'); } }); } function rotateChevron() { chevron.classList.contains('active') ? chevron.classList.remove('active') : chevron.classList.add('active'); } // Allows clicking & enter key to open the menu button.addEventListener('click', () => { toggleMenu(); }); // Allows the arrow down key to open the menu button.addEventListener('keydown', (e) => { if (e.key === KEYS.ARROW_DOWN) { showMenu(); e.preventDefault(); // Prevent the page from scrolling down e.stopPropagation(); // Prevent the wrapper element from handling this keydown after the menu opens } }); }; document.addEventListener('DOMContentLoaded', () => { Array.from(document.querySelectorAll('.hs-language-switcher')).forEach( (languageSwitcher) => HsLanguageSwitcher(languageSwitcher) ); }); ``` ### v0 ```json // meta.json { "label": "Language Switcher", "host_template_types": ["PAGE", "BLOG_POST", "BLOG_LISTING"], "icon": "../img/icon/edited-language.svg", "extra_classes": "widget-type-language_switcher", "smart_type": "NOT_SMART", "master_language": "en", "categories": ["functionality"], "content_tags": [ { "name": "OTHER_INDUSTRY", "source": "MARKETPLACE" }, { "name": "COMPATIBLE_PAGE", "source": "MARKETPLACE" } ], "editable_contexts": ["TEMPLATE"] } ``` ```json // fields.json [ { "id": "2e71f343-859b-8c20-1f17-587a747ebc21", "name": "display_mode", "label": "Display mode", "help_text": "The language of the text in the language switcher. PageLang means the names of languages will display in the language of the page the switcher is on. Localized means the name of each language will display in that language. Hybrid is a combination of the two.", "sortable": false, "required": true, "locked": false, "display": "select", "placeholder": "Search", "choices": [ ["localized", "Localized"], ["pagelang", "PageLang"], ["hybrid", "Hybrid"] ], "type": "choice", "default": "localized" } ] ``` ```html {% language_switcher display_mode='{{ module.display_mode }}' %} ``` ## Post listing A new version of the [default post_listing](/reference/cms/modules/default-modules#site-search-input) module was released at the end of March 2024. The new version of this module is built with JavaScript and React as opposed to HubL, and module code is now rendered server-side to improve performance. It preserves all of the previous module fields while bringing in some modern styling options for the user with new fields for text and layout. Below, learn more about the new `v1` version updates in each file, along with the original `v0` version for comparison. ### v1 notes The `fields.json` is file replaced by `fields.tsx`. **Existing fields** The new version of the module includes all existing module fields. - `selectBlog`: a field for selecting the blog to display posts from (aliased to the existing `select_blog` field). - `listingType`: a field for selecting the attribute to list posts by (aliased to the existing `listing_type` field). - `postsHeading`: a field for setting the heading text at the top of the listing (aliased to the existing `list_title` field). - `maxLinks`: a field for setting the maximum number of blog posts to display (aliased to the existing `max_links` field). **New fields** - A `displayForEachListItem` choice field has been added to enable selecting the elements that will display for each blog post, including: - Featured image (`image`) - Post title (`title`) - Author name (`authorName`) - Publish date (`publishDate`) - A `headingLevel` choice field has been added to enable selecting the `postsHeading` level (`h1` - `h6`). **Style fields** The following styling fields have been added: - **Layout styling:** a `groupLayout` field group contains a `style` choice field for selecting whether the posts will display as rows or 2-column tiles. - **Heading styling:** a `groupHeading` field group contains a `font` field for styling `postsHeading`. - **Post title styling:** a `groupTitle` field group contains `font`, `spacing`, and `hoverFont` fields for styling post titles. - **Author styling:** a `groupAuthor` field group contains a `font` field for styling the post author name text. - **Publish date styling:** a `groupPublishDate` field group contains a `font` field for styling the publish date text. - **Background styling:** a background field group contains `backgroundColor` and `spacing` fields to style the module background. ### v1 ```js import { FieldGroup, ModuleFields, TextField, FontField, SpacingField, BlogField, ChoiceField, NumberField, } from '@hubspot/cms-components/fields'; export const fields = ( <ModuleFields> <BlogField label="Select blog to display" name="selectBlog" propertyAliasesPaths={{ selectBlog: ['select_blog'] }} /> <ChoiceField label="Sort posts by" name="listingType" display="select" placeholder="Search" choices={[ ['recent', 'Most recent'], ['popular_all_time', 'Most popular - all time'], ['popular_past_year', 'Most popular - past year'], ['popular_past_six_months', 'Most popular - past six months'], ['popular_past_month', 'Most popular - past month'], ]} propertyAliasesPaths={{ listingType: ['listing_type'] }} default="recent" /> <ChoiceField label="Choose what shows in your feed" name="displayForEachListItem" display="checkbox" multiple={true} reorderingEnabled={false} choices={[ ['image', 'Image'], ['title', 'Title'], ['authorName', 'Author name'], ['publishDate', 'Publish date'], ]} default={['title', 'authorName', 'publishDate']} /> <NumberField label="Maximum blog posts to list" name="maxLinks" min={1} max={20} step={1} propertyAliasesPaths={{ maxLinks: ['max_links'] }} default={5} /> <TextField label="Posts heading" name="postsHeading" default="Featured posts" propertyAliasesPaths={{ heading: ['list_title'] }} /> <ChoiceField name="headingLevel" label="Heading level" display="select" choices={[ ['h1', 'H1'], ['h2', 'H2'], ['h3', 'H3'], ['h4', 'H4'], ['h5', 'H5'], ['h6', 'H6'], ]} default="h2" /> <FieldGroup name="groupStyle" label="Styles" tab="STYLE"> <FieldGroup name="groupLayout" label="Layout"> <ChoiceField name="style" label="Style" display="select" choices={[ ['tiles', 'Tiles'], ['minimal', 'Minimal'], ]} default="minimal" /> </FieldGroup> <FieldGroup label="Heading" name="groupHeading"> <FontField label="Font" name="font" /> </FieldGroup> <FieldGroup label="Title" name="groupTitle"> <FontField label="Font" name="font" default={{ font: '', styles: { 'font-weight': 'bold' }, }} /> <SpacingField label="Spacing" name="spacing" visibility={{ hidden_subfields: { padding: 'true' } }} default={{ margin: { bottom: { value: 20, units: 'px', }, }, }} /> <FontField label="Hover font" name="hoverFont" default={{ font: '', styles: { 'font-weight': 'bold' }, }} /> </FieldGroup> <FieldGroup label="Author" name="groupAuthor"> <FontField label="Font" name="font" /> </FieldGroup> <FieldGroup label="Publish date" name="groupPublishDate"> <FontField label="Font" name="font" /> </FieldGroup> </FieldGroup> </ModuleFields> ); ``` ```js import { Island } from '@hubspot/cms-components'; import blogIcon from './assets/blog.svg'; import StyledComponentsRegistry from '../../StyledComponentRegistry.jsx'; import PostListingIsland from './islands/PostListingIsland.js?island'; import { styled } from 'styled-components'; import { ModuleMeta } from '../../../types/modules.js'; import { StyleFields } from './types.js'; import { FontFieldType } from '@hubspot/cms-components/fields'; type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; interface FontProps { $fontStyle: string; $fontHoverStyle?: string; } const HeadingWrapper = styled.span<FontProps>` ${props => props.$fontStyle} `; interface BlogPostHeadingProps { headingLevel: HeadingLevel; postsHeading: string; headingStyle: { font: FontFieldType; }; } const BlogPostHeading = ({ headingLevel, postsHeading, headingStyle, }: BlogPostHeadingProps) => { const HeadingLevel = headingLevel; return ( <HeadingLevel> <HeadingWrapper $fontStyle={headingStyle.font.css}> {postsHeading} </HeadingWrapper> </HeadingLevel> ); }; function stripPublicFromUrl(signedUrl) { if (signedUrl) { return signedUrl.replace(/^public/, ''); } } interface ComponentProps { hublData: string; headingLevel: HeadingLevel; postsHeading: string; groupStyle: StyleFields; displayForEachListItem: string[]; } export function Component(props: ComponentProps) { const { hublData, headingLevel, postsHeading, groupStyle, displayForEachListItem, } = props; return ( <StyledComponentsRegistry> <section> <BlogPostHeading headingLevel={headingLevel} postsHeading={postsHeading} headingStyle={groupStyle.groupHeading} /> <Island wrapperTag="article" groupStyle={groupStyle} module={PostListingIsland} layout={groupStyle.groupLayout.style} signedUrl={stripPublicFromUrl(hublData)} displayForEachListItem={displayForEachListItem} headingLevel={headingLevel} /> </section> </StyledComponentsRegistry> ); } export const defaultModuleConfig = { moduleName: 'post_listing', version: 1, }; export { fields } from './fields.jsx'; export const meta: ModuleMeta = { label: 'Post listing', host_template_types: ['BLOG_LISTING', 'BLOG_POST', 'PAGE'], icon: blogIcon, categories: ['blog'], }; export { default as hublDataTemplate } from './hubl_data.hubl.html?raw'; ``` ```css .hsPostListingWrapper--tiles { display: grid; justify-content: center; grid-template-columns: repeat(auto-fit, minmax(250px, calc(100% / 3 - 20px))); gap: 20px; } .hsPostListingWrapper--minimal { display: grid; grid-template-columns: 1fr; gap: 20px; } .hsPostListingHeading { display: block; } .hsPostListing--image { max-width: 250px; } .hsPostListingFeaturedImage { height: 10rem; max-height: 100%; margin-bottom: 1rem; } .hsPostListingImage { height: 100%; width: 100%; -o-object-fit: cover; object-fit: cover; } .hsPostListingBody { display: flex; flex-direction: column; } .hsPostListingTitle { display: block; } .hsPostListingAuthor { display: block; } .hsPostListingPublishDate { display: block; } .hsPostListingAuthorDate { display: flex; justify-content: space-between; } ``` ### v0 ```json [ { "id": "5c3b2c23-6585-a913-cabe-1d5ba025a1b9", "name": "select_blog", "label": "Select blog to display", "help_text": "Default will use the current blog when used in a blog template or the primary blog when used elsewhere.", "sortable": false, "required": false, "locked": false, "type": "blog", "default": null }, { "id": "f245b59c-d2ea-08ab-a40a-e545bc71d2cf", "name": "listing_type", "label": "List blog posts by", "sortable": false, "required": true, "locked": false, "display": "select", "placeholder": "Search", "choices": [ ["recent", "Most recent"], ["popular_all_time", "Most popular - all time"], ["popular_past_year", "Most popular - past year"], ["popular_past_six_months", "Most popular - past six months"], ["popular_past_month", "Most popular - past month"] ], "type": "choice", "default": "recent" }, { "id": "643831b9-0aa6-a0ef-f3db-b5a71143168e", "name": "list_title", "label": "List title to display", "sortable": false, "required": false, "locked": false, "validation_regex": "", "allow_new_line": false, "show_emoji_picker": false, "type": "text", "default": "Recent Posts" }, { "id": "1417ad86-21ca-17b6-09a8-65ca54def75b", "name": "max_links", "label": "Maximum blog posts to list", "sortable": false, "required": false, "locked": false, "display": "text", "min": 1, "step": 1, "type": "number", "default": 5 } ] ``` ```html {% if module.select_blog is number %} {% set select_blog = module.select_blog %} {% else %} {% set select_blog = 'default' %} {% endif %} {% post_listing select_blog='{{ select_blog }}', listing_type='{{module.listing_type}}', list_title='{{ module.list_title }}', max_links={{ module.max_links }} %} ``` ## Search input A new version of the [default search input](/reference/cms/modules/default-modules#site-search-input) module has been released. This version update impacts the following module files: - `fields.json` - `module.html` - `module.css` - `module.js` Below, learn more about the new `v1` version updates in each file, along with the original `v0` version for comparison. ### v1 notes **Existing fields** - The `field_label` and `placeholder` text fields have been moved into the `search` group. - The `include_search_button` toggle has been removed. - The `content_types` group has been moved into the `results` group. **New fields** - A `button` group has been added, which enables controls for: - `icon`: the search button's icon. - `button_label`: the search button's text label. - A `results` group has been added, which enables controls for: - `use_custom_search_results_template`: toggle to choose which template is used for the search results page. - `path_id`: the ID of the page that will be used for search results. The referenced page must contain the search results module. - A `default_text` group has been added, which allows for the translation of default content. **Style fields** The following style field groups have been added: - `container`: fields for styling the module container's background color, border radius, and spacing. - `field_label`: fields for styling the module's label font and spacing. - `input`: fields for styling the search input's font, spacing, background color, border, and corner radius. - `button`: fields for styling the button's font, background color, border, corner radius, and spacing. Also includes options for hover styling and button positioning. - `autosuggest_results`: fields for styling the auto-suggested results box's background color, border, border radius, spacing, header font, suggestion link font, hover font, and hover background color. ### v1 ```json [ { "label": "Search input field", "name": "input", "type": "group", "children": [ { "label": "Label text", "name": "field_label", "type": "text", "aliases": ["field_label"] }, { "label": "Placeholder text", "name": "placeholder", "type": "text", "aliases": ["placeholder"], "default": "Search" } ] }, { "label": "Button", "name": "button", "type": "group", "children": [ { "label": "Icon", "name": "icon", "type": "icon", "default": { "name": "search", "unicode": "f002", "type": "SOLID" } }, { "label": "Button text", "name": "button_label", "type": "text" } ] }, { "label": "Results", "name": "results", "type": "group", "children": [ { "label": "Use custom search results page", "name": "use_custom_search_results_template", "id": "results.user_custom_search_results_template", "type": "boolean", "display": "toggle", "inline_help_text": "Turn this setting on to use a custom search results page instead of the default global search results page.", "default": false }, { "label": "Search results page", "name": "path_id", "type": "page", "visibility": { "operator": "EQUAL", "controlling_field": "results.user_custom_search_results_template", "controlling_value_regex": "true" }, "inline_help_text": "This is where people will go when they click to search their search term. Make sure to choose a page that contains the search results module." }, { "label": "Search results include", "name": "content_types", "type": "group", "children": [ { "label": "Website pages", "name": "website_pages", "type": "boolean", "visibility": { "access": { "operator": "HAS_ALL", "scopes": ["sitepages-access"] }, "operator": "NOT_EMPTY" }, "display": "checkbox", "aliases": ["content_types.website_pages"], "default": true }, { "label": "Landing pages", "name": "landing_pages", "type": "boolean", "display": "checkbox", "aliases": ["content_types.landing_pages"], "default": false }, { "label": "Blog posts", "name": "blog_posts", "type": "boolean", "display": "checkbox", "aliases": ["content_types.blog_posts"], "default": true }, { "label": "Knowledge articles", "name": "knowledge_articles", "type": "boolean", "visibility": { "access": { "operator": "HAS_ALL", "scopes": ["service-knowledge-access"] } }, "display": "checkbox", "aliases": ["content_types.knowledge_articles"], "default": false } ], "default": { "use_custom_search_results_template": false } } ] }, { "label": "Default text", "name": "default_text", "type": "group", "children": [ { "label": "Autosuggest results message", "name": "autosuggest_results_message", "type": "text", "default": "Results for “[[search_term]]”" }, { "label": "Autosuggest no results message", "name": "autosuggest_no_results_message", "type": "text", "default": "There are no autosuggest results for “[[search_term]]”" }, { "label": "Screen reader empty search field message", "name": "sr_empty_search_field_message", "type": "text", "default": "There are no suggestions because the search field is empty." }, { "label": "Screen reader autosuggest results message", "name": "sr_autosuggest_results_message", "type": "text", "default": "There are currently [[number_of_results]] auto-suggested results for [[search_term]]. Navigate to the results list by pressing the down arrow key, or press return to search for all results." }, { "label": "Screen reader search field aria-label", "name": "sr_search_field_aria_label", "type": "text", "default": "This is a search field with an auto-suggest feature attached." }, { "label": "Screen reader search button aria-label", "name": "sr_search_button_aria_label", "type": "text", "default": "Search" }, { "label": "Auto suggest results for example search term", "name": "as_example_search_results", "type": "text", "default": "Results for “example search term”" }, { "label": "Auto suggest line 1 for example search term", "name": "as_example_line_1", "type": "text", "default": "This is where suggested results appear as your visitor types their search term" }, { "label": "Auto suggest line 2 for example search term", "name": "as_example_line_2", "type": "text", "default": "Here's another suggested search result" }, { "label": "Auto suggest line 3 for example search term", "name": "as_example_line_3", "type": "text", "default": "Configure the type of content that appears in your search results using the “search results include” option" } ], "locked": true }, { "label": "Styles", "name": "styles", "type": "group", "tab": "STYLE", "children": [ { "label": "Container", "name": "container", "type": "group", "children": [ { "label": "Background color", "name": "color", "type": "color" }, { "label": "Corner", "name": "radius", "type": "number", "display": "text", "max": 100, "step": 1, "suffix": "px" }, { "label": "Spacing", "name": "spacing", "type": "group", "children": [ { "label": "Spacing", "name": "spacing", "type": "spacing" } ] } ] }, { "label": "Label", "name": "field_label", "type": "group", "children": [ { "label": "Text", "name": "text", "type": "group", "children": [ { "label": "Font", "name": "font", "type": "font", "default": { "size_unit": "px" } } ] }, { "label": "Spacing", "name": "spacing", "type": "group", "children": [ { "label": "Spacing", "name": "spacing", "type": "spacing" } ] } ] }, { "label": "Field", "name": "input", "type": "group", "children": [ { "label": "Text", "name": "text", "type": "group", "children": [ { "label": "Font", "name": "font", "type": "font", "default": { "size_unit": "px" } } ] }, { "label": "Spacing", "name": "spacing", "type": "group", "children": [ { "label": "Spacing", "name": "spacing", "type": "spacing" } ] }, { "label": "Background", "name": "background", "type": "group", "children": [ { "label": "Color", "name": "color", "type": "color" } ] }, { "label": "Border", "name": "border", "type": "group", "children": [ { "label": "Border", "name": "border", "type": "border" } ] }, { "label": "Corner", "name": "corner", "type": "group", "children": [ { "label": "Radius", "name": "radius", "type": "number", "display": "text", "max": 100, "step": 1, "suffix": "px" } ] } ] }, { "label": "Button", "name": "button", "type": "group", "children": [ { "label": "Text", "name": "text", "type": "group", "children": [ { "label": "Font", "name": "font", "type": "font", "default": { "size_unit": "px" } }, { "label": "Transform", "name": "transform", "type": "choice", "choices": [ ["none", "None"], ["capitalize", "Capitalize"], ["uppercase", "Uppercase"], ["lowercase", "Lowercase"] ], "display": "select" } ] }, { "label": "Background", "name": "background", "type": "group", "children": [ { "label": "Color", "name": "color", "type": "color" } ] }, { "label": "Border", "name": "border", "type": "group", "children": [ { "label": "Border", "name": "border", "type": "border" } ] }, { "label": "Corner", "name": "corner", "type": "group", "children": [ { "label": "Radius", "name": "radius", "type": "number", "display": "text", "max": 100, "step": 1, "suffix": "px" } ] }, { "label": "Spacing", "name": "spacing", "type": "group", "children": [ { "label": "Spacing", "name": "spacing", "type": "spacing" } ] }, { "label": "Hover", "name": "hover", "type": "group", "children": [ { "label": "Text", "name": "text", "type": "group", "children": [ { "label": "Font", "name": "font", "type": "font", "visibility": { "hidden_subfields": { "size": true } } } ] }, { "label": "Background", "name": "background", "type": "group", "children": [ { "label": "Color", "name": "color", "type": "color" } ] }, { "label": "Border", "name": "border", "type": "group", "children": [ { "label": "Border", "name": "border", "type": "border" } ] } ] }, { "label": "Position", "name": "position", "type": "group", "children": [ { "label": "Button position", "name": "button_position", "id": "button_position", "type": "choice", "choices": [ ["inline", "In line with search field"], ["beneath", "Beneath search field"] ], "display": "radio", "default": "inline" }, { "label": "Button alignment", "name": "button_alignment", "type": "choice", "visibility": { "operator": "EQUAL", "controlling_field": "button_position", "controlling_value_regex": "beneath" }, "choices": [ ["left", "Left"], ["right", "Right"] ], "display": "radio", "default": "left" } ] } ] }, { "label": "Suggested results", "name": "autosuggest_results", "type": "group", "children": [ { "label": "Background", "name": "background", "type": "group", "children": [ { "label": "Color", "name": "color", "type": "color" } ] }, { "label": "Border", "name": "border", "type": "group", "children": [ { "label": "Border", "name": "border", "type": "border" } ] }, { "label": "Corner", "name": "corner", "type": "group", "children": [ { "label": "Radius", "name": "radius", "type": "number", "display": "text", "max": 100, "step": 1, "suffix": "px" } ] }, { "label": "Spacing", "name": "spacing", "type": "group", "children": [ { "label": "Spacing", "name": "spacing", "type": "spacing" } ] }, { "label": "Text", "name": "text", "type": "group", "children": [ { "label": "Header font", "name": "header_font", "type": "font", "default": { "size_unit": "px" } }, { "label": "Suggestion font", "name": "suggestion_font", "type": "font", "default": { "size_unit": "px" } } ] }, { "label": "Hover", "name": "hover", "type": "group", "children": [ { "label": "Text", "name": "text", "type": "group", "children": [ { "label": "Font", "name": "font", "type": "font", "visibility": { "hidden_subfields": { "size": true } } } ] }, { "label": "Background", "name": "background", "type": "group", "children": [ { "label": "Color", "name": "color", "type": "color" } ] } ] } ] } ] } ] ``` ```html {% set hide_search_label = module.input.field_label is truthy ? false : true %} {% set button_position_class = "hs-search-field__bar--button-" ~ module.styles.button.position.button_position %} {% set button_alignment_class = module.styles.button.position.button_position == "beneath" ? "hs-search-field__bar--button-align-" ~ module.styles.button.position.button_alignment : "" %} {% set has_search_icon = module.button.icon.name is truthy ? true : false %} {% set show_suggest_in_editor_class = is_in_editor ? "hs-search-field--open": "" %} {% set button_label_class = module.button.button_label is truthy ? "hs-search-field__button--labelled" : "" %} {% set search_page = module.results.use_custom_search_results_template is truthy and module.results.path_id ? content_by_id(module.results.path_id).absolute_url : site_settings.content_search_results_page_path %} {% unless (search_page is string_containing "//") %} {% set search_page = "/" ~ search_page %} {% endunless %} {% set search_page = search_page|regex_replace("http:", "") %} {% require_css %} <style> {% scope_css %} .hs-search-field__bar > form { {% if module.styles.container.color.color %} background-color: {{ module.styles.container.color.css }}; {% endif %} {% if module.styles.container.radius >= 0 %} border-radius: {{ module.styles.container.radius ~ "px" }}; {% endif %} {{ module.styles.container.spacing.spacing.css }} } .hs-search-field__bar > form > label { {{ module.styles.field_label.spacing.spacing.css }} {{ module.styles.field_label.text.font.css }} } .hs-search-field__bar > form > .hs-search-field__input { {{ module.styles.input.spacing.spacing.css }} {{ module.styles.input.text.font.css }} {% if module.styles.input.background.color.css %} background-color: {{ module.styles.input.background.color.css }}; {% endif %} {{ module.styles.input.border.border.css }} {% if !module.styles.input.border.border.css and module.styles.input.border.border %} {% for side, border in module.styles.input.border.border.items() %} border-{{ side }}: 1px {{ border.style }} {{ theme.colors.primary.color }}; {% endfor %} {% endif %} {% if module.styles.input.corner.radius >= 0 %} border-radius: {{ module.styles.input.corner.radius ~ "px" }}; {% endif %} } .hs-search-field__button { {% if module.styles.button.background.color.css %} background-color: {{ module.styles.button.background.color.css }}; {% endif %} {{ module.styles.button.border.border.css }} {% if !module.styles.button.border.border.css and module.styles.button.border.border %} {% for side, border in module.styles.button.border.border.items() %} border-{{ side }}: 1px {{ border.style }} {{ theme.colors.primary.color }}; {% endfor %} {% endif %} {% if module.styles.button.corner.radius >= 0 %} border-radius: {{ module.styles.button.corner.radius ~ "px" }}; {% endif %} {{ module.styles.button.spacing.spacing.css }} {{ module.styles.button.text.font.css }} {% if module.styles.button.text.transform %} text-transform: {{ module.styles.button.text.transform }}; {% endif %} } .hs-search-field__button:hover, .hs-search-field__button:focus { {% if module.styles.button.hover.background.color.css %} background-color: {{ module.styles.button.hover.background.color.css }}; {% endif %} {{ module.styles.button.hover.text.font.css }} {{ module.styles.button.hover.border.border.css }} {% if !module.styles.button.hover.border.border.css and module.styles.button.hover.border.border %} {% for side, border in module.styles.button.hover.border.border.items() %} border-{{ side }}: 1px {{ border.style }} {{ theme.colors.primary.color }}; {% endfor %} {% endif %} } {% if has_search_icon and module.styles.button.hover.text.font.color %} .hs-search-field__button:hover svg, .hs-search-field__button:focus svg { fill: {{ module.styles.button.hover.text.font.color }}; } {% endif %} .hs-search-field__button:active { {% if module.styles.button.hover.background.color.color %} background-color: rgba({{ color_variant(module.styles.button.hover.background.color.color, 80)|convert_rgb }}, {{ module.styles.button.hover.background.color.opacity / 100 }}); {% endif %} {{ module.styles.button.hover.border.border.css }} {% if module.styles.button.hover.border.border %} border-color: {{ color_variant(module.styles.button.hover.border.border.top.color, 80) }}; {% endif %} {% if !module.styles.button.hover.border.border.css and module.styles.button.hover.border.border %} {% for side, border in module.styles.button.hover.border.border.items() %} border-{{ side }}: 1px {{ border.style }} {{ theme.colors.primary.color }}; {% endfor %} {% endif %} {% if module.styles.button.hover.text.font.color %} color: {{ module.styles.button.hover.text.font.color }}; {% endif %} {{ module.styles.button.hover.text.font.css }} } {% if has_search_icon and module.styles.button.hover.text.font.color %} .hs-search-field__button:active svg { fill: {{ module.styles.button.hover.text.font.color }}; } {% endif %} .hs-search-field--open .hs-search-field__suggestions { {% if module.styles.autosuggest_results.background.color.css %} background-color: {{ module.styles.autosuggest_results.background.color.css }}; {% endif %} {{ module.styles.autosuggest_results.spacing.spacing.css }} {{ module.styles.autosuggest_results.border.border.css }} {% if !module.styles.autosuggest_results.border.border.css and module.styles.autosuggest_results.border.border %} {% for side, border in module.styles.autosuggest_results.border.border.items() %} border-{{ side }}: 1px {{ border.style }} {{ theme.colors.primary.color }}; {% endfor %} {% endif %} {% if module.styles.autosuggest_results.corner.radius >= 0 %} border-radius: {{ module.styles.autosuggest_results.corner.radius ~ "px" }}; {% endif %} {{ module.styles.autosuggest_results.text.header_font.css }} } .hs-search-field--open .hs-search-field__suggestions a { {{ module.styles.autosuggest_results.text.suggestion_font.css }} } .hs-search-field--open .hs-search-field__suggestions a:hover { {% if module.styles.autosuggest_results.hover.text.font.color %} color: {{ module.styles.autosuggest_results.hover.text.font.color }}; {% endif %} {{ module.styles.autosuggest_results.hover.text.suggestion_font.css }} {% if module.styles.autosuggest_results.hover.background.color.color %} background-color: rgba({{ color_variant(module.styles.autosuggest_results.hover.background.color.color, 80)|convert_rgb }}, {{ module.styles.autosuggest_results.hover.background.color.opacity / 100 }}); {% endif %} } {% end_scope_css %} </style> {% end_require_css %} </form> </div> <div id="sr-messenger" class="hs-search-sr-message-container show-for-sr" role="status" aria-live="polite" aria-atomic="true"> </div> </div> {% require_js position="head" %} <script data-search_input-config="config_{{ name }}" type="application/json"> { "autosuggest_results_message": "{{ module.default_text.autosuggest_results_message }}", "autosuggest_no_results_message": "{{ module.default_text.autosuggest_no_results_message }}", "sr_empty_search_field_message": "{{ module.default_text.sr_empty_search_field_message }}", "sr_autosuggest_results_message": "{{ module.default_text.sr_autosuggest_results_message }}", "sr_search_field_aria_label": "{{ module.default_text.sr_search_field_aria_label }}", "sr_search_button_aria_label": "{{ module.default_text.sr_search_button_aria_label }}" } </script> {% end_require_js %} ``` ```css .hs-editor-hide-until-active { display: none; } .inpage-editor-active-field .hs-editor-hide-until-active { display: block; } .hs-search-field { position: relative; } .hs-search-field__input { box-sizing: border-box; width: 100%; flex: 1; } .hs-search-field__bar button svg { height: 10px; } .hs-search-field__suggestions { padding: 0; margin: 0; list-style: none; } .hs-search-field--open .hs-search-field__suggestions, .inpage-editor-active-field .hs-search-field__suggestions { position: absolute; width: 100%; border: 1px solid #cdcdcd; background-color: #fff; box-shadow: 1px 10px 16px -9px rgba(122, 122, 122, 0.75); } .hs-search-field__suggestions li { display: block; padding: 0; margin: 0; } .hs-search-field__suggestions .results-for { font-weight: bold; } .hs-search-field__suggestions a, .hs-search-field__suggestions .results-for { display: block; padding: 0 10px; line-height: 1.7rem; } .hs-search-field__suggestions a:hover, .hs-search-field__suggestions a:focus { background-color: rgba(0, 0, 0, 0.1); outline: none; } .hs-search-field__input:focus { outline-style: solid; } .hs-search-field__suggestions-container { position: relative; flex-basis: 100%; } .hs-search-field__form { display: flex; flex-wrap: wrap; } .hs-search-field__label { flex-basis: 100%; } .hs-search-field__bar--button-beneath .hs-search-field__input { flex-basis: 100%; } .hs-search-field__bar--button-beneath .hs-search-field__button { margin-top: 0.725rem; margin-right: 0.725rem; } .hs-search-field__bar--button-align-right .hs-search-field__button { margin-right: 0; margin-left: 0.725rem; order: 2; } .hs-search-field__bar--button-beneath .hs-search-field__suggestions-container { min-width: 75%; flex-basis: auto; flex-grow: 1; } .hs-search-field__button--labelled .hs_cos_wrapper_type_icon { margin-right: 0.5rem; } /* stylelint-disable declaration-no-important */ .show-for-sr { position: absolute !important; overflow: hidden !important; height: 1px !important; width: 1px !important; padding: 0 !important; border: 0 !important; clip: rect(0, 0, 0, 0) !important; white-space: nowrap !important; } /* stylelint-enable declaration-no-important */ ``` ```js // This is a fallback config in the event that the module json config is not found in the DOM. /* eslint-disable camelcase */ const DEFAULT_MODULE_CONFIG = Object.freeze({ autosuggest_results_message: 'Results for “[[search_term]]”', sr_autosuggest_results_message: 'There are currently [[number_of_results]] auto-suggested results for [[search_term]].', sr_search_field_aria_label: 'This is a search field with an auto-suggest feature attached.', sr_search_button_aria_label: 'Search', }); /* eslint-enable camelcase */ const SEARCH_URL_BASE = '/_hcms/v3/site-search/search'; // Add a variable so we can clear the autosuggest announcement timeout if the user keeps typing. let srAnnounceTimeout; /** * Grab JSON configuration for the module from the HubL data. */ const getModuleConfig = (moduleName) => { const configJSONScript = document.querySelector( `[data-${moduleName}-config]` ); if (configJSONScript) { return JSON.parse(configJSONScript.textContent); } return DEFAULT_MODULE_CONFIG; }; const moduleConfig = getModuleConfig('search_input'); const TYPEAHEAD_LIMIT = 3; const KEYS = Object.freeze({ TAB: 'Tab', ESC: 'Esc', // IE11 & Edge 16 value for Escape ESCAPE: 'Escape', UP: 'Up', // IE11 & Edge 16 value for Arrow Up ARROW_UP: 'ArrowUp', DOWN: 'Down', // IE11 & Edge 16 value for Arrow Down ARROW_DOWN: 'ArrowDown', }); const debounce = (func, wait) => { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => { func.apply(this, args); }, wait); }; }; const emptySearchSuggestions = (suggestionsResponse, searchInputElements) => { const { searchForm, searchSuggestions, searchInput, srMessageContainer } = searchInputElements; const { searchTerm } = suggestionsResponse; const noSearchResultsMessage = searchTerm ? moduleConfig.autosuggest_no_results_message.replace( '[[search_term]]', searchTerm ) : moduleConfig.sr_empty_search_field_message; const emptyResultsItem = `<li role="option" tabindex="-1" aria-posinset="1" aria-setsize="0" class="results-for ${ !searchTerm ? 'show-for-sr' : '' }">${noSearchResultsMessage}</li>`; searchSuggestions.innerHTML = emptyResultsItem; srMessageContainer.innerHTML = noSearchResultsMessage; searchInput.focus(); searchForm.classList.remove('hs-search-field--open'); }; const trapFocus = (searchInputElements) => { const { searchInput, searchSuggestions, searchInputContainer } = searchInputElements; const tabbable = []; tabbable.push(searchInput); const suggestions = searchSuggestions.querySelectorAll('a'); suggestions.forEach((suggestion) => tabbable.push(suggestion)); const firstTabbable = tabbable[0]; const lastTabbable = tabbable[tabbable.length - 1]; const tabResult = (e) => { if (e.target === lastTabbable && !e.shiftKey) { e.preventDefault(); firstTabbable.focus(); } else if (e.target === firstTabbable && e.shiftKey) { e.preventDefault(); lastTabbable.focus(); } }; const nextResult = (e) => { e.preventDefault(); if (e.target === lastTabbable) { firstTabbable.focus(); } else { tabbable.forEach((el) => { if (el === e.target) { tabbable[tabbable.indexOf(el) + 1].focus(); } }); } }; const previousResult = (e) => { e.preventDefault(); if (e.target === firstTabbable) { lastTabbable.focus(); } else { tabbable.forEach((el) => { if (el === e.target) { tabbable[tabbable.indexOf(el) - 1].focus(); } }); } }; searchInputContainer.removeEventListener('keydown', window.captureKeyDown); window.captureKeyDown = (e) => { switch (e.key) { case KEYS.TAB: tabResult(e); break; case KEYS.ESC: case KEYS.ESCAPE: emptySearchSuggestions({}, searchInputElements); break; case KEYS.UP: case KEYS.ARROW_UP: previousResult(e); break; case KEYS.DOWN: case KEYS.ARROW_DOWN: nextResult(e); break; default: break; } }; searchInputContainer.addEventListener('keydown', window.captureKeyDown); }; const announceSearchSuggestions = (suggestionsResponse, srMessageContainer) => { const { results, searchTerm } = suggestionsResponse; if (srMessageContainer) { srMessageContainer.innerHTML = `${moduleConfig.sr_autosuggest_results_message .replace('[[number_of_results]]', results.length) .replace('[[search_term]]', searchTerm)}`; } }; const renderSearchSuggestions = (suggestionsResponse, searchInputElements) => { const { searchSuggestions, srMessageContainer, searchInputContainer } = searchInputElements; const { results, searchTerm } = suggestionsResponse; const searchResultsMessage = moduleConfig.autosuggest_results_message.replace( '[[search_term]]', searchTerm ); const items = [ `<li role="option" tabindex="-1" aria-posinset="1" aria-setsize="${results.length}" class="results-for">${searchResultsMessage}</li>`, ...results.map((result, index) => { return `<li role="option" aria-posinset="${index + 2}" tabindex="${index + 1}"><a href="${result.url}">${ result.title }</a></li>`; }), ]; emptySearchSuggestions({}, searchInputElements); searchSuggestions.innerHTML = items.join(''); searchInputContainer.classList.add('hs-search-field--open'); srAnnounceTimeout = setTimeout(() => { announceSearchSuggestions(suggestionsResponse, srMessageContainer); }, 1500); }; const getSearchSuggestions = (searchInputElements) => { const { searchForm } = searchInputElements; const data = new FormData(searchForm); data.set('limit', TYPEAHEAD_LIMIT); data.set('autocomplete', true); data.set('analytics', true); const queryString = new URLSearchParams(data).toString(); const searchUrl = `${SEARCH_URL_BASE}?${queryString}`; const request = new XMLHttpRequest(); request.open('GET', searchUrl, true); request.onload = function () { if (request.status >= 200 && request.status < 400) { const resultData = JSON.parse(request.responseText); if (srAnnounceTimeout) { clearTimeout(srAnnounceTimeout); } if (resultData.results.length > 0) { renderSearchSuggestions(resultData, searchInputElements); trapFocus(searchInputElements); } else { emptySearchSuggestions(resultData, searchInputElements); } } else { console.error('Server reached, error retrieving results.'); // eslint-disable-line no-console } }; request.onerror = function () { console.error('Could not reach the server.'); // eslint-disable-line no-console }; request.send(); }; const initializeSearchInput = (searchInputContainer) => { let searchTerm = ''; if (searchInputContainer.classList.contains('hs-search-field--initialized')) { return; } searchInputContainer.classList.add('hs-search-field--initialized'); const searchForm = searchInputContainer.querySelector('form'); const searchInput = searchForm.querySelector('.hs-search-field__input'); const searchSuggestions = searchInputContainer.querySelector( '.hs-search-field__suggestions' ); const srMessageContainer = searchInputContainer.querySelector( '.hs-search-sr-message-container' ); const searchInputElements = { searchInputContainer, searchForm, searchSuggestions, srMessageContainer, searchInput, }; const isSearchTermPresent = debounce(() => { searchTerm = searchInput.value; if (searchTerm.length > 2) { getSearchSuggestions(searchInputElements); } else if (searchTerm.length === 0) { emptySearchSuggestions({}, searchInputElements); } }, 250); searchInput.addEventListener('input', () => { if (searchTerm !== searchInput.value) { isSearchTermPresent(); } }); }; document.addEventListener('DOMContentLoaded', () => { const searchInputContainers = document.querySelectorAll('.hs-search-field'); if (searchInputContainers.length > 0) { searchInputContainers.forEach((searchInputContainer) => { initializeSearchInput(searchInputContainer); }); } }); ``` ### v0 ```json [ { "id": "d7644a33-944a-3b21-7faa-133195962602", "name": "field_label", "label": "Label text", "required": false, "locked": false, "validation_regex": "", "allow_new_line": false, "show_emoji_picker": false, "type": "text", "default": null }, { "id": "e75bef76-6568-6550-8840-1d97bfc93c0b", "name": "placeholder", "label": "Placeholder text", "required": false, "locked": false, "validation_regex": "", "allow_new_line": false, "show_emoji_picker": false, "type": "text", "default": "Search" }, { "id": "132b5655-eccf-c251-9ed4-d04901e91987", "name": "include_search_button", "label": "Include search button", "required": false, "locked": false, "type": "boolean", "default": false }, { "id": "346180f5-0d36-e8df-aca9-a74bfa2d79a4", "name": "content_types", "label": "Search results include", "required": false, "locked": false, "children": [ { "id": "71d525ff-37c4-6c64-1f96-d99dbb529816", "name": "website_pages", "label": "Website pages", "required": false, "locked": false, "visibility": { "controlling_field": null, "controlling_value_regex": null, "operator": "NOT_EMPTY", "access": { "operator": "HAS_ALL", "scopes": ["sitepages-access"] }, "hidden_subfields": null }, "type": "boolean", "default": true }, { "id": "d6212113-19b5-c9b9-2f92-12dfc122d00c", "name": "landing_pages", "label": "Landing pages", "required": false, "locked": false, "type": "boolean", "default": false }, { "id": "7535615c-ed26-2e34-87ef-21c5c87ccbcb", "name": "blog_posts", "label": "Blog posts", "required": false, "locked": false, "type": "boolean", "default": true }, { "id": "307d0eed-0cf9-7465-5715-a75c378ec61f", "name": "knowledge_articles", "label": "Knowledge articles", "required": false, "locked": false, "visibility": { "controlling_field": null, "controlling_value_regex": null, "operator": null, "access": { "operator": "HAS_ALL", "scopes": ["service-knowledge-access"] }, "hidden_subfields": null }, "type": "boolean", "default": false } ], "type": "group", "default": { "website_pages": true, "landing_pages": false, "blog_posts": true, "knowledge_articles": false } } ] ``` ```html <ul class="hs-search-field__suggestions"></ul> </div> ``` ```css .hs-search-field { position: relative; } .hs-search-field__input { box-sizing: border-box; width: 100%; } .hs-search-field__bar button svg { height: 10px; } .hs-search-field__suggestions { margin: 0; padding: 0; list-style: none; } .hs-search-field--open .hs-search-field__suggestions { border: 1px solid #000; } .hs-search-field__suggestions li { display: block; margin: 0; padding: 0; } .hs-search-field__suggestions #results-for { font-weight: bold; } .hs-search-field__suggestions a, .hs-search-field__suggestions #results-for { display: block; } .hs-search-field__suggestions a:hover, .hs-search-field__suggestions a:focus { background-color: rgba(0, 0, 0, 0.1); outline: none; } ``` ```js var hsSearch = function (_instance) { var TYPEAHEAD_LIMIT = 3; var KEYS = { TAB: 'Tab', ESC: 'Esc', // IE11 & Edge 16 value for Escape ESCAPE: 'Escape', UP: 'Up', // IE11 & Edge 16 value for Arrow Up ARROW_UP: 'ArrowUp', DOWN: 'Down', // IE11 & Edge 16 value for Arrow Down ARROW_DOWN: 'ArrowDown', }; var searchTerm = '', searchForm = _instance, searchField = _instance.querySelector('.hs-search-field__input'), searchResults = _instance.querySelector('.hs-search-field__suggestions'), searchOptions = function () { var formParams = []; var form = _instance.querySelector('form'); for ( var i = 0; i < form.querySelectorAll('input[type=hidden]').length; i++ ) { var e = form.querySelectorAll('input[type=hidden]')[i]; if (e.name !== 'limit') { formParams.push( encodeURIComponent(e.name) + '=' + encodeURIComponent(e.value) ); } } var queryString = formParams.join('&'); return queryString; }; var debounce = function (func, wait, immediate) { var timeout; return function () { var context = this, args = arguments; var later = function () { timeout = null; if (!immediate) { func.apply(context, args); } }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait || 200); if (callNow) { func.apply(context, args); } }; }, emptySearchResults = function () { searchResults.innerHTML = ''; searchField.focus(); searchForm.classList.remove('hs-search-field--open'); }, fillSearchResults = function (response) { var items = []; items.push( "<li id='results-for'>Results for \"" + response.searchTerm + '"</li>' ); response.results.forEach(function (val, index) { items.push( "<li id='result" + index + "'><a href='" + val.url + "'>" + val.title + '</a></li>' ); }); emptySearchResults(); searchResults.innerHTML = items.join(''); searchForm.classList.add('hs-search-field--open'); }, getSearchResults = function () { var request = new XMLHttpRequest(); var requestUrl = '/_hcms/search?&term=' + encodeURIComponent(searchTerm) + '&limit=' + encodeURIComponent(TYPEAHEAD_LIMIT) + '&autocomplete=true&analytics=true&' + searchOptions(); request.open('GET', requestUrl, true); request.onload = function () { if (request.status >= 200 && request.status < 400) { var data = JSON.parse(request.responseText); if (data.total > 0) { fillSearchResults(data); trapFocus(); } else { emptySearchResults(); } } else { console.error('Server reached, error retrieving results.'); } }; request.onerror = function () { console.error('Could not reach the server.'); }; request.send(); }, trapFocus = function () { var tabbable = []; tabbable.push(searchField); var tabbables = searchResults.getElementsByTagName('A'); for (var i = 0; i < tabbables.length; i++) { tabbable.push(tabbables[i]); } var firstTabbable = tabbable[0], lastTabbable = tabbable[tabbable.length - 1]; var tabResult = function (e) { if (e.target == lastTabbable && !e.shiftKey) { e.preventDefault(); firstTabbable.focus(); } else if (e.target == firstTabbable && e.shiftKey) { e.preventDefault(); lastTabbable.focus(); } }, nextResult = function (e) { e.preventDefault(); if (e.target == lastTabbable) { firstTabbable.focus(); } else { tabbable.forEach(function (el) { if (el == e.target) { tabbable[tabbable.indexOf(el) + 1].focus(); } }); } }, lastResult = function (e) { e.preventDefault(); if (e.target == firstTabbable) { lastTabbable.focus(); } else { tabbable.forEach(function (el) { if (el == e.target) { tabbable[tabbable.indexOf(el) - 1].focus(); } }); } }; searchForm.addEventListener('keydown', function (e) { switch (e.key) { case KEYS.TAB: tabResult(e); break; case KEYS.ESC: case KEYS.ESCAPE: emptySearchResults(); break; case KEYS.UP: case KEYS.ARROW_UP: lastResult(e); break; case KEYS.DOWN: case KEYS.ARROW_DOWN: nextResult(e); break; } }); }, isSearchTermPresent = debounce(function () { searchTerm = searchField.value; if (searchTerm.length > 2) { getSearchResults(); } else if (searchTerm.length == 0) { emptySearchResults(); } }, 250), init = (function () { searchField.addEventListener('input', function (e) { if (searchTerm != searchField.value) { isSearchTermPresent(); } }); })(); }; if ( document.attachEvent ? document.readyState === 'complete' : document.readyState !== 'loading' ) { var searchResults = document.querySelectorAll('.hs-search-field'); Array.prototype.forEach.call(searchResults, function (el) { var hsSearchModule = hsSearch(el); }); } else { document.addEventListener('DOMContentLoaded', function () { var searchResults = document.querySelectorAll('.hs-search-field'); Array.prototype.forEach.call(searchResults, function (el) { var hsSearchModule = hsSearch(el); }); }); } ``` ## Search results A new version of the [default search results module](/reference/cms/modules/default-modules#search-results) module has been released. This version update impacts the following module files: - `fields.json` - `module.html` - `module.css` - `module.js` Below, learn more about the new `v1` version updates in each file, along with the original `v0` version for comparison. ### v1 notes **Existing fields** - The `display_featured_images` field has been moved to the `results` group and renamed to `display_for_each_result`. **New fields** - A `title` group has been added, which enables controls for: - `show_title`: a toggle for whether the title displays. - `heading_tag`: the search results heading level (`h1` by default). - A `pagination` group has been added, which enables controls for pagination, such as showing the number of pages and the previous/next arrows and labels. - A `default_text` group has been added, which allows for the translation of default content. **Style fields** The following style field groups have been added: - `container`: a field for styling the module container's spacing. - `title`: fields for styling the page's title font and capitalization options. - `results_count_message`: fields for styling the page's result count font and capitalization. - `featured_image`: fields for styling the featured image's size, aspect ratio, border, radius, and spacing. - `results`: fields for styling the returned result title and description fonts, as well as spacing between results. - `pagination`: fields for styling the pagination fonts, icon size, background colors, spacing, border, and corner radius. Includes options for hover styling. ### v1 ```json [ { "label": "Title", "name": "title", "type": "group", "children": [ { "label": "Show title", "name": "show_title", "type": "boolean", "display": "toggle", "default": false }, { "label": "Heading tag", "name": "heading_tag", "type": "choice", "help_text": "Choose a heading level. H1 is the largest, followed by H2, and so on.", "visibility": { "controlling_field": "title.show_title", "controlling_value_regex": "true", "operator": "MATCHES_REGEX" }, "choices": [ ["h1", "H1"], ["h2", "H2"], ["h3", "H3"], ["h4", "H4"], ["h5", "H5"], ["h6", "H6"] ], "display": "select", "placeholder": "Select a heading type", "default": "h1" } ] }, { "label": "Results", "name": "results", "type": "group", "children": [ { "label": "", "name": "display_for_each_result", "id": "results.display_for_each_result", "type": "choice", "choices": [["image", "Display an image preview for each result"]], "display": "checkbox", "multiple": true, "reordering_enabled": false } ] }, { "label": "Pagination", "name": "pagination", "type": "group", "children": [ { "label": "Numbers", "name": "numbers", "id": "pagination.numbers", "type": "choice", "choices": [["show_numbers", "Show numbers"]], "display": "checkbox", "multiple": true, "reordering_enabled": false, "default": ["show_numbers"] }, { "label": "First and last", "name": "first_and_last", "id": "first_and_last", "type": "choice", "choices": [ ["show_arrows", "Show arrows"], ["show_labels", "Show labels"] ], "display": "checkbox", "multiple": true, "reordering_enabled": false, "default": ["show_arrows", "show_labels"] }, { "label": "First label", "name": "first_label", "type": "text", "visibility": { "controlling_field": "first_and_last", "controlling_value_regex": "show_labels", "operator": "MATCHES_REGEX" }, "default": "First" }, { "label": "Last label", "name": "last_label", "type": "text", "visibility": { "controlling_field": "first_and_last", "controlling_value_regex": "show_labels", "operator": "MATCHES_REGEX" }, "default": "Last" }, { "label": "Previous and next", "name": "previous_and_next", "id": "previous_and_next", "type": "choice", "choices": [ ["show_arrows", "Show arrows"], ["show_labels", "Show labels"] ], "display": "checkbox", "multiple": true, "reordering_enabled": false, "default": ["show_arrows", "show_labels"] }, { "label": "Previous label", "name": "previous_label", "type": "text", "visibility": { "controlling_field": "previous_and_next", "controlling_value_regex": "show_labels", "operator": "MATCHES_REGEX" }, "default": "Previous" }, { "label": "Next label", "name": "next_label", "type": "text", "visibility": { "controlling_field": "previous_and_next", "controlling_value_regex": "show_labels", "operator": "MATCHES_REGEX" }, "default": "Next" } ], "inline_help_text": "Pagination helps users browse long lists of results by organizing them into pages." }, { "label": "Default text", "name": "default_text", "type": "group", "children": [ { "label": "Results title", "name": "results_title", "type": "text", "help_text": "Enter the title you would like to display above the search results.", "placeholder": "Search results for “{{ searched_term|escape }}”", "default": "Search results for “{{ searched_term|escape }}”" }, { "label": "Search results count", "name": "results_count_message", "type": "text", "placeholder": "Displaying [[offset]] – [[limit]] of [[total]] results", "default": "Displaying [[offset]] – [[limit]] of [[total]] results" }, { "label": "Featured Image", "name": "featured_image", "type": "text", "default": "Featured Image" }, { "label": "Search Results Article Title", "name": "search_results_article_title", "type": "text", "default": "Search Results Article Title" }, { "label": "Search result article description.", "name": "search_results_article_description", "type": "text", "default": "Some preview text from each page will be displayed here to help your visitors browse through the search results quickly and find the page they need." }, { "label": "Navigation aria label", "name": "navigation_aria_label", "type": "text", "default": "Paging navigation" }, { "label": "No results message", "name": "no_results_message", "type": "richtext", "default": "<p>Sorry. There were no results for [[search_term]].</p><p>Try rewording your query, or browse through our site.</p>" }, { "label": "First page link text", "name": "first_page_link_text", "type": "text", "default": "First Page" }, { "label": "Current page aria label", "name": "current_page_aria_label", "type": "text", "default": "Current Page" }, { "label": "Page number aria label", "name": "page_number_aria_label", "type": "text", "default": "Page" }, { "label": "Last page link text", "name": "last_page_link_text", "type": "text", "default": "Last Page" }, { "label": "Previous page link text", "name": "previous_page_link_text", "type": "text", "default": "Previous" }, { "label": "Next page link text", "name": "next_page_link_text", "type": "text", "default": "Next" } ], "locked": true }, { "label": "Styles", "name": "styles", "type": "group", "tab": "STYLE", "children": [ { "label": "Container", "name": "container", "type": "group", "children": [ { "label": "Spacing", "name": "spacing", "type": "group", "children": [ { "label": "Spacing", "name": "spacing", "type": "spacing" } ] } ] }, { "label": "Title", "name": "title", "type": "group", "visibility": { "operator": "EQUAL", "controlling_field": "title.show_title", "controlling_value_regex": "true" }, "children": [ { "label": "Font", "name": "font", "type": "font", "default": { "size_unit": "px" } }, { "label": "Transform", "name": "transform", "type": "choice", "choices": [ ["none", "None"], ["capitalize", "Capitalize"], ["uppercase", "Uppercase"], ["lowercase", "Lowercase"] ], "display": "select" } ] }, { "label": "Results count message", "name": "results_count_message", "type": "group", "children": [ { "label": "Font", "name": "font", "type": "font", "default": { "size_unit": "px" } }, { "label": "Transform", "name": "transform", "type": "choice", "choices": [ ["none", "None"], ["capitalize", "Capitalize"], ["uppercase", "Uppercase"], ["lowercase", "Lowercase"] ], "display": "select" } ] }, { "label": "Results", "name": "results", "type": "group", "children": [ { "label": "Image", "name": "featured_image", "type": "group", "visibility": { "controlling_field": "results.display_for_each_result", "controlling_value_regex": "image", "operator": "MATCHES_REGEX" }, "children": [ { "label": "Size", "name": "size", "type": "group", "children": [ { "label": "Aspect ratio", "name": "aspect_ratio", "type": "choice", "choices": [ ["1/1", "1:1"], ["3/2", "3:2"], ["2/3", "2:3"], ["4/3", "4:3"], ["3/4", "3:4"], ["16/9", "16:9"] ], "display": "select" }, { "label": "Width", "name": "width", "type": "number", "display": "text", "max": 75, "min": 25, "step": 5, "suffix": "%" } ] }, { "label": "Corner", "name": "corner", "type": "group", "children": [ { "label": "Radius", "name": "radius", "type": "number", "display": "text", "max": 100, "step": 1, "suffix": "px" } ] }, { "label": "Spacing", "name": "spacing", "type": "group", "children": [ { "label": "Spacing", "name": "spacing", "type": "spacing", "visibility": { "hidden_subfields": { "padding": true } } }, { "label": "Between image and content", "name": "between_image_and_content", "type": "number", "display": "text", "max": 50, "min": 0, "step": 1, "suffix": "px" } ] } ] }, { "label": "Title", "name": "title", "type": "group", "children": [ { "label": "Font", "name": "font", "type": "font", "default": { "size_unit": "px" } } ] }, { "label": "Preview text", "name": "description", "type": "group", "children": [ { "label": "Font", "name": "font", "type": "font", "default": { "size_unit": "px" } } ] }, { "label": "Spacing", "name": "spacing", "type": "group", "children": [ { "label": "Space between results", "name": "space_between_results", "type": "number", "display": "text", "max": 48, "min": 12, "step": 2, "suffix": "px" } ] } ] }, { "label": "Pagination", "name": "pagination", "type": "group", "children": [ { "label": "Numbers", "name": "numbers", "type": "group", "visibility": { "controlling_field": "pagination.numbers", "controlling_value_regex": "show_numbers", "operator": "MATCHES_REGEX" }, "children": [ { "label": "Text", "name": "text", "type": "group", "children": [ { "label": "Font", "name": "font", "type": "font", "default": { "size_unit": "px" } } ] }, { "label": "Background", "name": "background", "type": "group", "children": [ { "label": "Color", "name": "color", "type": "color" } ] }, { "label": "Spacing", "name": "spacing", "type": "group", "children": [ { "label": "Spacing", "name": "spacing", "type": "spacing", "visibility": { "hidden_subfields": { "margin": true } } } ] }, { "label": "Border", "name": "border", "type": "group", "children": [ { "label": "Border", "name": "border", "type": "border" } ] }, { "label": "Corner", "name": "corner", "type": "group", "children": [ { "label": "Radius", "name": "radius", "type": "number", "display": "text", "max": 100, "step": 1, "suffix": "px" } ] }, { "label": "Hover", "name": "hover", "type": "group", "children": [ { "label": "Text color", "name": "text_color", "type": "color", "visibility": { "hidden_subfields": { "opacity": true } } }, { "label": "Background color", "name": "background_color", "type": "color" }, { "label": "Border color", "name": "border_color", "type": "color", "visibility": { "hidden_subfields": { "opacity": true } } } ] }, { "label": "Active", "name": "active", "type": "group", "help_text": "Styles the numbered link matching the page that you're currently on.", "children": [ { "label": "Text color", "name": "text_color", "type": "color", "visibility": { "hidden_subfields": { "opacity": true } } }, { "label": "Background color", "name": "background_color", "type": "color" }, { "label": "Border color", "name": "border_color", "type": "color", "visibility": { "hidden_subfields": { "opacity": true } } } ] } ] }, { "label": "Previous and next", "name": "previous_and_next", "type": "group", "advanced_visibility": { "boolean_operator": "OR", "criteria": [ { "controlling_field": "previous_and_next", "controlling_value_regex": "show_arrows", "operator": "MATCHES_REGEX" }, { "controlling_field": "previous_and_next", "controlling_value_regex": "show_labels", "operator": "MATCHES_REGEX" } ] }, "children": [ { "label": "Text", "name": "text", "type": "group", "visibility": { "controlling_field": "previous_and_next", "controlling_value_regex": "show_labels", "operator": "MATCHES_REGEX" }, "children": [ { "label": "Font", "name": "font", "type": "font", "default": { "size_unit": "px" } } ] }, { "label": "Icon", "name": "icon", "type": "group", "visibility": { "controlling_field": "previous_and_next", "controlling_value_regex": "show_arrows", "operator": "MATCHES_REGEX" }, "children": [ { "label": "Size", "name": "size", "type": "number", "display": "text", "max": 100, "step": 1, "suffix": "px" } ] }, { "label": "Background", "name": "background", "type": "group", "children": [ { "label": "Color", "name": "color", "type": "color" } ] }, { "label": "Spacing", "name": "spacing", "type": "group", "children": [ { "label": "Space between text and icon", "name": "space_between_text_and_icon", "type": "number", "advanced_visibility": { "boolean_operator": "AND", "criteria": [ { "controlling_field": "previous_and_next", "controlling_value_regex": "show_arrows", "operator": "MATCHES_REGEX" }, { "controlling_field": "previous_and_next", "controlling_value_regex": "show_labels", "operator": "MATCHES_REGEX" } ] }, "display": "slider", "max": 25, "step": 1, "suffix": "px", "visibility_rules": "ADVANCED" }, { "label": "Spacing", "name": "spacing", "type": "spacing", "visibility": { "hidden_subfields": { "margin": true } } } ] }, { "label": "Border", "name": "border", "type": "group", "children": [ { "label": "Border", "name": "border", "type": "border" } ] }, { "label": "Corner", "name": "corner", "type": "group", "children": [ { "label": "Radius", "name": "radius", "type": "number", "display": "text", "max": 100, "step": 1, "suffix": "px" } ] }, { "label": "Hover", "name": "hover", "type": "group", "children": [ { "label": "Text color", "name": "text_color", "type": "color", "visibility": { "hidden_subfields": { "opacity": true }, "controlling_field": "previous_and_next", "controlling_value_regex": "show_labels", "operator": "MATCHES_REGEX" } }, { "label": "Background color", "name": "background_color", "type": "color" }, { "label": "Border color", "name": "border_color", "type": "color", "visibility": { "hidden_subfields": { "opacity": true } } } ] } ], "visibility_rules": "ADVANCED" }, { "label": "First and last", "name": "first_and_last", "type": "group", "advanced_visibility": { "boolean_operator": "OR", "criteria": [ { "controlling_field": "first_and_last", "controlling_value_regex": "show_arrows", "operator": "MATCHES_REGEX" }, { "controlling_field": "first_and_last", "controlling_value_regex": "show_labels", "operator": "MATCHES_REGEX" } ] }, "children": [ { "label": "Text", "name": "text", "type": "group", "visibility": { "controlling_field": "first_and_last", "controlling_value_regex": "show_labels", "operator": "MATCHES_REGEX" }, "children": [ { "label": "Font", "name": "font", "type": "font", "default": { "size_unit": "px" } } ] }, { "label": "Icon", "name": "icon", "type": "group", "visibility": { "controlling_field": "first_and_last", "controlling_value_regex": "show_arrows", "operator": "MATCHES_REGEX" }, "children": [ { "label": "Size", "name": "size", "type": "number", "display": "text", "max": 100, "step": 1, "suffix": "px" } ] }, { "label": "Background", "name": "background", "type": "group", "children": [ { "label": "Color", "name": "color", "type": "color" } ] }, { "label": "Spacing", "name": "spacing", "type": "group", "children": [ { "label": "Space between text and icon", "name": "space_between_text_and_icon", "type": "number", "advanced_visibility": { "boolean_operator": "AND", "criteria": [ { "controlling_field": "first_and_last", "controlling_value_regex": "show_arrows", "operator": "MATCHES_REGEX" }, { "controlling_field": "first_and_last", "controlling_value_regex": "show_labels", "operator": "MATCHES_REGEX" } ] }, "display": "slider", "max": 25, "step": 1, "suffix": "px", "visibility_rules": "ADVANCED" }, { "label": "Spacing", "name": "spacing", "type": "spacing", "visibility": { "hidden_subfields": { "margin": true } } } ] }, { "label": "Border", "name": "border", "type": "group", "children": [ { "label": "Border", "name": "border", "type": "border" } ] }, { "label": "Corner", "name": "corner", "type": "group", "children": [ { "label": "Radius", "name": "radius", "type": "number", "display": "text", "max": 100, "step": 1, "suffix": "px" } ] }, { "label": "Hover", "name": "hover", "type": "group", "children": [ { "label": "Text color", "name": "text_color", "type": "color", "visibility": { "hidden_subfields": { "opacity": true }, "controlling_field": "first_and_last", "controlling_value_regex": "show_labels", "operator": "MATCHES_REGEX" } }, { "label": "Background color", "name": "background_color", "type": "color" }, { "label": "Border color", "name": "border_color", "type": "color", "visibility": { "hidden_subfields": { "opacity": true } } } ] } ], "visibility_rules": "ADVANCED" }, { "label": "Spacing", "name": "spacing", "type": "group", "children": [ { "label": "Space between links", "name": "space_between_links", "type": "number", "display": "slider", "max": 50, "min": 0, "step": 5, "suffix": "px" }, { "label": "Spacing", "name": "spacing", "type": "spacing", "visibility": { "hidden_subfields": { "padding": true } } } ] } ] } ] } ] ``` ```html {% set heading_tag = module.heading_tag %} {% set searched_term = request.query_dict.term ? request.query_dict.term : request.query_dict.q %} {% set results_title = module.default_text.results_title %} {% set results_count_message = module.default_text.results_count_message %} {% if module.pagination.numbers.index("show_numbers") >= 0 %} {% set show_numbers = true %} {% endif %} {% if module.pagination.previous_and_next.index("show_labels") >= 0 %} {% set show_next_and_previous_labels = true %} {% endif %} {% if module.pagination.previous_and_next.index("show_arrows") >= 0 %} {% set show_next_and_previous_arrows = true %} {% endif %} {% if module.pagination.first_and_last.index("show_labels") >= 0 %} {% set show_first_and_last_labels = true %} {% endif %} {% if module.pagination.first_and_last.index("show_arrows") >= 0 %} {% set show_first_and_last_arrows = true %} {% endif %} {% if module.results.display_for_each_result.index("image") >= 0 %} {% set show_featured_images = true %} {% endif %} {# Module styles #} {% require_css %} <style> {% scope_css %} {# Search results container #} .hs-search-results { {{ module.styles.container.spacing.spacing.css }} } {# Search results title #} .hs-search-results-title { {{ module.styles.title.font.css }} {% if module.styles.title.transform %} text-transform: {{ module.styles.title.transform }}; {% endif %} } {# Results #} {# Results count message #} .hs-search-results__message { {{ module.styles.results_count_message.font.css }} {% if module.styles.results_count_message.transform %} text-transform: {{ module.styles.results_count_message.transform }}; {% endif %} } {% if module.styles.results.spacing.space_between_results %} .hs-search-results__listing { gap: {{ module.styles.results.spacing.space_between_results }}px; } {% endif %} {# Image #} .hs-search-results__featured-image-wrapper { {{ module.styles.results.featured_image.spacing.spacing.css }} {% if module.styles.results.featured_image.size.width %} flex-basis: {{ module.styles.results.featured_image.size.width ~ "%" }}; min-width: {{ module.styles.results.featured_image.size.width ~ "%" }}; {% endif %} {% if module.styles.results.featured_image.spacing.between_image_and_content > 0 %} margin-right: {{ module.styles.results.featured_image.spacing.between_image_and_content ~ "px" }}; {% endif %} } .hs-search-results__featured-image { {% if module.styles.results.featured_image.size.aspect_ratio %} aspect-ratio: {{ module.styles.results.featured_image.size.aspect_ratio }}; {% endif %} {% if module.styles.results.featured_image.corner.radius >= 0 %} border-radius: {{ module.styles.results.featured_image.corner.radius ~ "px" }}; {% endif %} } {# Results title #} .hs-search-results__title { {{ module.styles.results.title.font.css }} } {# Results body copy #} .hs-search-results__description { {{ module.styles.results.description.font.css }} } {# Pagination #} .hs-search-results__pagination { {{ module.styles.pagination.spacing.spacing.css }} } .hs-search-results__pagination__link { {{ module.styles.pagination.text.font.css }} {% if module.styles.pagination.spacing.space_between_links %} margin-right: {{ module.styles.pagination.spacing.space_between_links ~ "px" }}; {% endif %} } {# Pagination links #} {% if module.styles.pagination.spacing.space_between_links %} .hs-search-results__pagination__link { margin-right: {{ module.styles.pagination.spacing.space_between_links ~ "px" }}; } {% endif %} {% if module.pagination.first_and_last || module.pagination.previous_and_next %} .hs-search-results__pagination__link-icon svg { {% if module.styles.pagination.text.font.size %} height: {{ module.styles.pagination.text.font.size ~ module.styles.pagination.text.font.size_unit }}; width: {{ module.styles.pagination.text.font.size ~ module.styles.pagination.text.font.size_unit }}; {% endif %} } {% endif %} {% if module.styles.pagination.text.font.color %} .hs-search-results__pagination__link--number:hover, .hs-search-results__pagination__link--number:focus, .hs-search-results__pagination__link:hover .hs-search-results__pagination__link-text, .hs-search-results__pagination__link:focus .hs-search-results__pagination__link-text { color: {{ color_variant(module.styles.pagination.text.font.color, -80) }}; } .hs-search-results__pagination__link:hover .hs-search-results__pagination__link-icon svg, .hs-search-results__pagination__link:focus .hs-search-results__pagination__link-icon svg { fill: {{ color_variant(module.styles.pagination.text.font.color, -80) }}; } .hs-search-results__pagination__link--number:active, .hs-search-results__pagination__link:active .hs-search-results__pagination__link-text { color: {{ color_variant(module.styles.pagination.text.font.color, 80) }}; } .hs-search-results__pagination__link:active .hs-search-results__pagination__link-icon svg { fill: {{ color_variant(module.styles.pagination.text.font.color, 80) }}; } {% endif %} {# Active pagination link #} {% if show_numbers %} .hs-search-results__pagination__link--active { {{ module.styles.pagination.active.border.border.css }} {% if module.styles.pagination.active.corner.radius >= 0 %} border-radius: {{ module.styles.pagination.active.corner.radius ~ "px" }}; {% endif %} } {% endif %} {# Numbers #} {% if show_numbers %} .hs-search-results__pagination__link--number { {% if module.styles.pagination.numbers.background.color.color %} background-color: rgba({{ module.styles.pagination.numbers.background.color.color|convert_rgb }}, {{ module.styles.pagination.numbers.background.color.opacity / 100 }}); {% endif %} {{ module.styles.pagination.numbers.border.border.css }} {% if module.styles.pagination.numbers.corner.radius >= 0 %} border-radius: {{ module.styles.pagination.numbers.corner.radius ~ "px" }}; {% endif %} {{ module.styles.pagination.numbers.text.font.css }} {{ module.styles.pagination.numbers.spacing.spacing.css }} } .hs-search-results__pagination__link--ellipsis { {{ module.styles.pagination.numbers.text.font.css }} } .hs-search-results__pagination__link--active { {% if module.styles.pagination.numbers.active.background.color.color %} background-color: rgba({{ module.styles.pagination.numbers.active.background.color.color|convert_rgb }}, {{ module.styles.pagination.numbers.active.background.color.opacity / 100 }}); {% endif %} {% if module.styles.pagination.numbers.active.border.color.color %} border-color: {{ module.styles.pagination.numbers.active.border.color.color }}; {% endif %} {% if module.styles.pagination.numbers.active.text.color.color %} color: {{ module.styles.pagination.numbers.active.text.color.color }}; {% endif %} } .hs-search-results__pagination__link--number:hover, .hs-search-results__pagination__link--number:focus { {% if module.styles.pagination.numbers.hover.background_color.color %} background-color: rgba({{ module.styles.pagination.numbers.hover.background_color.color|convert_rgb }}, {{ module.styles.pagination.numbers.hover.background.color.opacity / 100 }}); {% endif %} {% if module.styles.pagination.numbers.hover.border_color.color %} border-color: {{ module.styles.pagination.numbers.hover.border_color.color }}; {% endif %} {% if module.styles.pagination.numbers.hover.text_color.color %} color: {{ module.styles.pagination.numbers.hover.text_color.color }}; {% endif %} } {% endif %} {# Next/previous #} {% if show_next_and_previous_arrows or show_next_and_previous_labels %} .hs-search-results__pagination__link--prev, .hs-search-results__pagination__link--next { {% if module.styles.pagination.previous_and_next.background.color.color %} background-color: rgba({{ module.styles.pagination.previous_and_next.background.color.color|convert_rgb }}, {{ module.styles.pagination.previous_and_next.background.color.opacity / 100 }}); {% endif %} {{ module.styles.pagination.previous_and_next.border.border.css }} {% if module.styles.pagination.previous_and_next.corner.radius >= 0 %} border-radius: {{ module.styles.pagination.previous_and_next.corner.radius ~ "px" }}; {% endif %} {{ module.styles.pagination.previous_and_next.spacing.spacing.css }} } .hs-search-results__pagination__link--prev:hover, .hs-search-results__pagination__link--prev:focus, .hs-search-results__pagination__link--next:hover, .hs-search-results__pagination__link--next:focus { {% if module.styles.pagination.previous_and_next.hover.background_color.color %} background-color: rgba({{ module.styles.pagination.previous_and_next.hover.background_color.color|convert_rgb }}, {{ module.styles.pagination.previous_and_next.hover.background_color.opacity / 100 }}); {% endif %} {% if module.styles.pagination.previous_and_next.hover.border_color.color %} border-color: {{ module.styles.pagination.previous_and_next.hover.border_color.color }}; {% endif %} } {% if show_next_and_previous_labels %} .hs-search-results__pagination__link--prev > .hs-search-results__pagination__link-text, .hs-search-results__pagination__link--next > .hs-search-results__pagination__link-text { {{ module.styles.pagination.previous_and_next.text.font.css }} } {% if module.styles.pagination.previous_and_next.hover.text_color.color %} .hs-search-results__pagination__link--prev:hover > .hs-search-results__pagination__link-text, .hs-search-results__pagination__link--prev:focus > .hs-search-results__pagination__link-text, .hs-search-results__pagination__link--next:hover > .hs-search-results__pagination__link-text, .hs-search-results__pagination__link--next:focus > .hs-search-results__pagination__link-text { color: {{ module.styles.pagination.previous_and_next.hover.text_color.color }}; } {% endif %} {% endif %} {% if show_next_and_previous_arrows and show_next_and_previous_labels %} {% if module.styles.pagination.previous_and_next.spacing.space_between_text_and_icon >= 0 %} .hs-search-results__pagination__link--text-and-icon.hs-search-results__pagination__link--prev > .hs-search-results__pagination__link-icon { margin-right: {{ module.styles.pagination.previous_and_next.spacing.space_between_text_and_icon ~ "px" }}; } {% endif %} {% if module.styles.pagination.previous_and_next.spacing.space_between_text_and_icon >= 0 %} .hs-search-results__pagination__link--text-and-icon.hs-search-results__pagination__link--next > .hs-search-results__pagination__link-icon { margin-left: {{ module.styles.pagination.previous_and_next.spacing.space_between_text_and_icon ~ "px" }}; } {% endif %} {% endif %} {% if show_next_and_previous_arrows %} .hs-search-results__pagination__link--prev > .hs-search-results__pagination__link-icon svg, .hs-search-results__pagination__link--next > .hs-search-results__pagination__link-icon svg { {% if module.styles.pagination.previous_and_next.icon.size %} height: {{ module.styles.pagination.previous_and_next.icon.size ~ "px" }}; width: {{ module.styles.pagination.previous_and_next.icon.size ~ "px" }}; {% endif %} {% if module.styles.pagination.previous_and_next.text.font.color %} fill: {{ module.styles.pagination.previous_and_next.text.font.color }}; {% endif %} } {% if module.styles.pagination.previous_and_next.hover.text.color.color %} .hs-search-results__pagination__link--prev:hover > .hs-search-results__pagination__link-icon svg, .hs-search-results__pagination__link--prev:focus > .hs-search-results__pagination__link-icon svg, .hs-search-results__pagination__link--next:hover > .hs-search-results__pagination__link-icon svg, .hs-search-results__pagination__link--next:focus > .hs-search-results__pagination__link-icon svg { fill: {{ module.styles.pagination.previous_and_next.hover.text.color.color }}; } {% endif %} {% endif %} {% endif %} {# First/last #} {% if show_first_and_last_arrows or show_first_and_last_labels %} .hs-search-results__pagination__link--first, .hs-search-results__pagination__link--last { {% if module.styles.pagination.first_and_last.background.color.color %} background-color: rgba({{ module.styles.pagination.first_and_last.background.color.color|convert_rgb }}, {{ module.styles.pagination.first_and_last.background.color.opacity / 100 }}); {% endif %} {{ module.styles.pagination.first_and_last.border.border.css }} {% if module.styles.pagination.first_and_last.corner.radius >= 0 %} border-radius: {{ module.styles.pagination.first_and_last.corner.radius ~ "px" }}; {% endif %} {{ module.styles.pagination.first_and_last.spacing.spacing.css }} } .hs-search-results__pagination__link--first:hover, .hs-search-results__pagination__link--first:focus, .hs-search-results__pagination__link--last:hover, .hs-search-results__pagination__link--last:focus { {% if module.styles.pagination.first_and_last.hover.background_color.color %} background-color: rgba({{ module.styles.pagination.first_and_last.hover.background_color.color|convert_rgb }}, {{ module.styles.pagination.first_and_last.hover.background_color.opacity / 100 }}); {% endif %} {% if module.styles.pagination.first_and_last.hover.border_color.color %} border-color: {{ module.styles.pagination.first_and_last.hover.border_color.color }}; {% endif %} } {% if show_first_and_last_labels %} .hs-search-results__pagination__link--first > .hs-search-results__pagination__link-text, .hs-search-results__pagination__link--last > .hs-search-results__pagination__link-text { {{ module.styles.pagination.first_and_last.text.font.css }} } {% if module.styles.pagination.first_and_last.hover.text_color.color %} .hs-search-results__pagination__link--first:hover > .hs-search-results__pagination__link-text, .hs-search-results__pagination__link--first:focus > .hs-search-results__pagination__link-text, .hs-search-results__pagination__link--last:hover > .hs-search-results__pagination__link-text, .hs-search-results__pagination__link--last:focus > .hs-search-results__pagination__link-text { color: {{ module.styles.pagination.first_and_last.hover.text_color.color }}; } {% endif %} {% endif %} {% if show_first_and_last_arrows and show_first_and_last_labels %} {% if module.styles.pagination.first_and_last.spacing.space_between_text_and_icon >= 0 %} .hs-search-results__pagination__link--text-and-icon.hs-search-results__pagination__link--first > .hs-search-results__pagination__link-icon { margin-right: {{ module.styles.pagination.first_and_last.spacing.space_between_text_and_icon ~ "px" }}; } {% endif %} {% if module.styles.pagination.first_and_last.spacing.space_between_text_and_icon >= 0 %} .hs-search-results__pagination__link--text-and-icon.hs-search-results__pagination__link--last > .hs-search-results__pagination__link-icon { margin-left: {{ module.styles.pagination.first_and_last.spacing.space_between_text_and_icon ~ "px" }}; } {% endif %} {% endif %} {% if show_first_and_last_arrows %} .hs-search-results__pagination__link--first > .hs-search-results__pagination__link-icon svg, .hs-search-results__pagination__link--last > .hs-search-results__pagination__link-icon svg { {% if module.styles.pagination.first_and_last.icon.size %} height: {{ module.styles.pagination.first_and_last.icon.size ~ "px" }}; width: {{ module.styles.pagination.first_and_last.icon.size ~ "px" }}; {% endif %} {% if module.styles.pagination.first_and_last.text.font.color %} fill: {{ module.styles.pagination.first_and_last.text.font.color }}; {% endif %} } {% if module.styles.pagination.first_and_last.hover.text_color.color %} .hs-search-results__pagination__link--first:hover > .hs-search-results__pagination__link-icon svg, .hs-search-results__pagination__link--first:focus > .hs-search-results__pagination__link-icon svg, .hs-search-results__pagination__link--last:hover > .hs-search-results__pagination__link-icon svg, .hs-search-results__pagination__link--last:focus > .hs-search-results__pagination__link-icon svg { fill: {{ module.styles.pagination.first_and_last.hover.text_color.color }}; } {% endif %} {% endif %} {% endif %} {% end_scope_css %} </style> {% end_require_css %} {% macro first_page_icon_markup() %} {% icon "first_icon" extra_classes="hs-search-results__pagination__link-icon", name="angle-double-left", purpose="decorative", style="SOLID", unicode="f100" %} {% endmacro %} {% macro previous_page_icon_markup() %} {% icon "previous_icon" extra_classes="hs-search-results__pagination__link-icon", name="angle-left", purpose="decorative", style="SOLID", unicode="f104" %} {% endmacro %} {% macro next_page_icon_markup() %} {% icon "next_icon" extra_classes="hs-search-results__pagination__link-icon", name="angle-right", purpose="decorative", style="SOLID", unicode="f105" %} {% endmacro %} {% macro last_page_icon_markup() %} {% icon "last_icon" extra_classes="hs-search-results__pagination__link-icon", name="angle-double-right", purpose="decorative", style="SOLID", unicode="f101" %} {% endmacro %} {% if module.title.show_title and ( request.query_dict.term or request.query_dict.q or is_in_editor ) %} {% if !heading_tag %} {% set heading_tag = 'h1' %} {% endif %} <{{ heading_tag }} class="hs-search-results-title">{{ results_title }}</{{ heading_tag }}> {% endif %} {% endif %} </li> <li class="hs-search-results__listing__item"> {% if "image" in module.results.display_for_each_result %} {% endif %} </li> <li class="hs-search-results__listing__item"> {% if "image" in module.results.display_for_each_result %} {% endif %} </li> </ul> <nav class="hs-search-results__pagination" role="navigation" aria-label="{{ module.default_text.navigation_aria_label }}"> {% if module.pagination.first_and_last == true %} <a class="hs-search-results__pagination__link hs-search-results__pagination__link--text-and-icon hs-search-results__pagination__link--first" href="#"> {% if 'show_arrows' in module.pagination.first_and_last %} {{ first_page_icon_markup() }} {% endif %} {% if 'show_labels' in module.pagination.first_and_last %} <span class="hs-search-results__pagination__link-text show-for-sr--mobile">{{ module.pagination.first_label }}</span> {% endif %} </a> {% endif %} {% if module.pagination.previous_and_next == true %} <a class="hs-search-results__pagination__link hs-search-results__pagination__link--text-and-icon hs-search-results__pagination__link--prev" href="#"> {% if 'show_arrows' in module.pagination.previous_and_next %} {{ previous_page_icon_markup() }} {% endif %} {% if 'show_labels' in module.pagination.previous_and_next %} <span class="hs-search-results__pagination__link-text show-for-sr--mobile">{{ module.pagination.previous_label }}</span> {% endif %} </a> {% endif %} {% if show_numbers %} <a class="hs-search-results__pagination__link hs-search-results__pagination__link--number hs-search-results__pagination__link--active" href="#"> 1 </a> <a class="hs-search-results__pagination__link hs-search-results__pagination__link--number" href="#"> 2 </a> <a class="hs-search-results__pagination__link hs-search-results__pagination__link--number" href="#"> 3 </a> <a class="hs-search-results__pagination__link hs-search-results__pagination__link--number" href="#"> 4 </a> <span class="hs-search-results__pagination__link hs-search-results__pagination__link--ellipsis">...</span> <a class="hs-search-results__pagination__link hs-search-results__pagination__link--number" href="#"> 12 </a> {% endif %} {% if module.pagination.previous_and_next == true %} <a class="hs-search-results__pagination__link hs-search-results__pagination__link--text-and-icon hs-search-results__pagination__link--next" href="#"> {% if 'show_labels' in module.pagination.previous_and_next %} <span class="hs-search-results__pagination__link-text show-for-sr--mobile">{{ module.pagination.next_label }}</span> {% endif %} {% if 'show_arrows' in module.pagination.previous_and_next %} {{ next_page_icon_markup() }} {% endif %} </a> {% endif %} {% if module.pagination.first_and_last == true %} <a class="hs-search-results__pagination__link hs-search-results__pagination__link--text-and-icon hs-search-results__pagination__link--last" href="#"> {% if 'show_labels' in module.pagination.first_and_last %} <span class="hs-search-results__pagination__link-text show-for-sr--mobile">{{ module.pagination.last_label }}</span> {% endif %} {% if 'show_arrows' in module.pagination.first_and_last %} {{ last_page_icon_markup() }} {% endif %} </a> {% endif %} </nav> {% endif %} <!-- Search Results Title --> <!-- Search Results Listing --> <!-- Search Results Pagination --> <!-- Populated By JS --> </div> {% require_js position="head" %} <script data-search_results-config="config_{{ name }}" type="application/json"> { "search_results_heading_tag": "{{ heading_tag }}", "search_results_page_path": "{{ request.path }}", "search_results_count_message": "{{ results_count_message }}", "search_results_display_featured_images": "{{ show_featured_images }}", "featured_image_alt": "{{ module.default_text.featured_image }}", "show_first_and_last_links": "{{ module.pagination.first_and_last }}", "show_previous_and_next_links": "{{ module.pagination.previous_and_next }}", "show_numbers": "{{ show_numbers }}", "current_page_aria_label": "{{ module.default_text.current_page_aria_label }}", "page_number_aria_label": "{{ module.default_text.page_number_aria_label }}", "first_page_link_text": "{{ module.default_text.first_page_link_text }}", "first_page_icon_markup": "{{ first_page_icon_markup() | escapejson }}", "previous_page_link_text":"{{ module.default_text.previous_page_link_text }}", "previous_page_icon_markup": "{{ previous_page_icon_markup() | escapejson }}", "next_page_link_text":"{{ module.default_text.next_page_link_text }}", "next_page_icon_markup": "{{ next_page_icon_markup() | escapejson }}", "last_page_link_text":"{{ module.default_text.last_page_link_text }}", "last_page_icon_markup": "{{ last_page_icon_markup() | escapejson }}", "no_results_message": "{{ module.default_text.no_results_message }}", "navigation_aria_label": "{{ module.default_text.navigation_aria_label }}" } </script> {% end_require_js %} ``` ```css .hs-search-results__listing { display: grid; padding: 0; margin: 0; gap: 2rem; list-style: none; } .hs-search-results__listing__item { display: flex; padding: 0; margin: 0; } .hs-search-results__link, .hs-search-results__link:hover { text-decoration: none; } .hs-search-highlight { font-weight: bold; } .hs-search-results__pagination { display: flex; align-items: center; justify-content: center; } .hs-search-results__pagination__link { display: flex; margin: 0 0.35rem; align-items: center; } .hs-search-results__pagination__link, .hs-search-results__pagination__link:hover, .hs-search-results__pagination__link:focus, .hs-search-results__pagination__link:active { text-decoration: none; } .hs-search-results__pagination__link-icon { display: flex; height: 16px; width: 16px; justify-content: center; } .hs-search-results__pagination__link-icon svg { fill: currentColor; } .hs-search-results__pagination__link--first, .hs-search-results__pagination__link--prev { margin-right: 0.7rem; } .hs-search-results__pagination__link--first { margin-left: 0; } .hs-search-results__pagination__link--first .hs-search-results__pagination__link-icon, .hs-search-results__pagination__link--prev .hs-search-results__pagination__link-icon { margin-right: 0.35rem; } .hs-search-results__pagination__link--number { padding: 0.35rem 0.7rem; } .hs-search-results__pagination__link--last, .hs-search-results__pagination__link--next { margin-left: 0.7rem; } .hs-search-results__pagination__link--last { margin-right: 0; } .hs-search-results__pagination__link--last .hs-search-results__pagination__link-icon, .hs-search-results__pagination__link--next .hs-search-results__pagination__link-icon { margin-left: 0.35rem; } .hs-search-results__pagination__link--active { border-style: solid; border-width: 1px; } .hs-search-results__featured-image-wrapper { min-width: 33.3333%; margin-right: 1.25rem; flex-basis: 33.3333%; flex-shrink: 0; } .hs-search-results__featured-image { height: auto; max-width: 100%; -o-object-fit: cover; object-fit: cover; } .hs-search-results__featured-image--empty { display: flex; height: 191px; width: 255px; min-width: 255px; margin-right: 1.25rem; align-items: center; justify-content: center; background-color: #d7d7d7; } .hs-search-results__featured-image--empty p { padding: 0; margin: 0; } @media (max-width: 767px) { .hs-search-results__listing li { flex-flow: row wrap; } .hs-search-results__featured-image-wrapper { max-width: 100%; margin-right: 0; flex-basis: 100%; } .hs-search-results__featured-image--empty { height: auto; width: 100%; min-width: 100%; margin-right: 0; } } /* stylelint-disable declaration-no-important */ .show-for-sr { position: absolute !important; overflow: hidden !important; height: 1px !important; width: 1px !important; padding: 0 !important; border: 0 !important; clip: rect(0, 0, 0, 0) !important; white-space: nowrap !important; } @media (max-width: 767px) { .show-for-sr--mobile { position: absolute !important; overflow: hidden !important; height: 1px !important; width: 1px !important; padding: 0 !important; border: 0 !important; clip: rect(0, 0, 0, 0) !important; white-space: nowrap !important; } } /* stylelint-enable declaration-no-important */ ``` ```js /** * URL query parameters are passed through to the HubSpot content search API. * * q=<search term> term to be searched for * * Content type to search can be specified with the following types. * type=SITE_PAGE, LANDING_PAGE, BLOG_POST, LISTING_PAGE, and KNOWLEDGE_ARTICLE * Multiple content types can be searched by additional "type" query params. * ex. type=BLOG_POST&type=LISTING_PAGE * * offset=<offset> beginning offset of the result set * limit=<limit> page size * * See: https://developers.hubspot.com/docs/api/cms/site-search * */ const SEARCH_URL_BASE = '/_hcms/v3/site-search/search'; const SURROUNDING_PAGE_NUMBERS = 1; // how many page numbers to attempt to surround the current page with. const searchResultsContainer = document.querySelectorAll('.hs-search-results'); /** * Grab JSON configuration for the module from the HubL data. */ const getModuleConfig = (moduleName) => { const configJSONScript = document.querySelector( `[data-${moduleName}-config]` ); if (configJSONScript) { return JSON.parse(configJSONScript.textContent); } return { /* eslint-disable camelcase */ search_results_heading_tag: 'h1', search_results_page_path: '', search_results_count_message: 'Displaying [[offset]] – [[limit]] of [[total]] results', search_results_display_featured_images: false, featured_image_alt: 'Featured Image', first_page_link_text: 'First page', previous_page_link_text: 'Previous', next_page_link_text: 'Next', last_page_link_text: 'Last', no_results_message: 'No results message', navigation_aria_label: 'Paging Navigation', /* eslint-enable camelcase */ }; }; const moduleConfig = getModuleConfig('search_results'); const getSearchPageUrl = (pageOffset) => { const currentPageParams = new URLSearchParams(window.location.search); currentPageParams.set('offset', pageOffset); return `${ moduleConfig.search_results_page_path }?${currentPageParams.toString()}`; }; /** * Given a total number of results, an offset, and a page size, returns an object full of pagination data for display. * * @param {number} totalItems the total number of items to be paginated * @param {number} currentOffset the current offset of the page * @param {number} pageSize the number of items per page * @returns {object} the paginator object with data for rendering pagination */ const getPaginator = (totalItems, currentOffset, pageSize) => { // default page size is 10 pageSize = pageSize || 10; /** * Setting up variables */ const maxPageNumbersToDisplay = SURROUNDING_PAGE_NUMBERS * 2 + 1; const centerPageSlot = Math.ceil(maxPageNumbersToDisplay / 2); // calculate total pages of items const totalPagesOfItems = Math.ceil(totalItems / pageSize); const currentPage = currentOffset / pageSize + 1; // calculate previous page url based on offset and page size const prevPageUrl = currentPage > 1 ? getSearchPageUrl(currentOffset - pageSize) : ''; // calculate next page url based on offset and page size const nextPageUrl = currentPage < totalPagesOfItems ? getSearchPageUrl(currentOffset + pageSize) : ''; /** * Setting up functions. */ // Page numbers are a rolling window of the current page, and attempt to center the current page in the navigation. const calculateBeginAndEndPageNumbers = () => { let firstDisplayedPageNumber; let lastDisplayedPageNumber; // There are less total pages than the maximum number of pages to display, so we can just display all of them. if (totalPagesOfItems <= maxPageNumbersToDisplay) { firstDisplayedPageNumber = 1; lastDisplayedPageNumber = totalPagesOfItems; return { firstDisplayedPageNumber, lastDisplayedPageNumber }; } // The current page is less than the center page slot, so we start at 1. if (currentPage <= centerPageSlot) { firstDisplayedPageNumber = 1; lastDisplayedPageNumber = centerPageSlot + SURROUNDING_PAGE_NUMBERS; return { firstDisplayedPageNumber, lastDisplayedPageNumber }; } // The current page is one away from the last, so we can end at the total number of pages. if (currentPage + 1 >= totalPagesOfItems) { firstDisplayedPageNumber = totalPagesOfItems - centerPageSlot; lastDisplayedPageNumber = totalPagesOfItems; return { firstDisplayedPageNumber, lastDisplayedPageNumber }; } // The current page is somewhere in the middle, so we calculate to try and keep it in the middle. firstDisplayedPageNumber = currentPage - SURROUNDING_PAGE_NUMBERS; lastDisplayedPageNumber = currentPage + SURROUNDING_PAGE_NUMBERS; return { firstDisplayedPageNumber, lastDisplayedPageNumber }; }; const maybeAddLastPageLink = (pages) => { // if the number of pages is less than the total pages, add an ellipsis, and link to the last page. if ( totalPagesOfItems > currentPage + SURROUNDING_PAGE_NUMBERS && totalPagesOfItems > pages.length ) { const lastPageOffset = totalPagesOfItems * pageSize - pageSize; return [ ...pages, { display: '...' }, { display: totalPagesOfItems, offset: lastPageOffset, url: getSearchPageUrl(lastPageOffset), }, ]; } return pages; }; const maybeAddFirstPageLink = (pages) => { if ( currentPage > centerPageSlot && totalPagesOfItems > maxPageNumbersToDisplay ) { return [ { display: '1', offset: 0, url: getSearchPageUrl(0) }, { display: '...' }, ...pages, ]; } return pages; }; /** * Putting variables and functions together to generate the paginator. */ const { firstDisplayedPageNumber, lastDisplayedPageNumber } = calculateBeginAndEndPageNumbers(currentPage); const pageNumberSlots = Array( lastDisplayedPageNumber - firstDisplayedPageNumber + 1 ).keys(); // Fill the page number slots with display data. let pages = [...pageNumberSlots].map((i) => { const pageOffset = (firstDisplayedPageNumber + i - 1) * pageSize; return { display: firstDisplayedPageNumber + i, offset: pageOffset, url: getSearchPageUrl(pageOffset), }; }); pages = maybeAddLastPageLink(pages); pages = maybeAddFirstPageLink(pages); const firstPageUrl = getSearchPageUrl(0); const lastPageUrl = getSearchPageUrl(totalPagesOfItems * pageSize - pageSize); // return object with all paginator properties required by the view return { totalItems, currentPage, prevPageUrl, nextPageUrl, firstPageUrl, lastPageUrl, pageSize, totalPagesOfItems, firstDisplayedPageNumber, lastDisplayedPageNumber, pages, }; }; const renderLabelledPageLink = (aria, label, classes, href, show) => { if (!show) { return ''; } return `<a class="hs-search-results__pagination__link hs-search-results__pagination__link--text-and-icon ${classes}" aria-label="${aria}" href="${href}"> ${label} </a>`; }; const renderPageNumberLinks = (pages, currentPage, show) => { if (!show) { return ''; } return pages .map((page) => { const activeClass = currentPage === page.display ? 'hs-search-results__pagination__link--active' : ''; const currentPageAriaLabel = currentPage === page.display ? `${moduleConfig.current_page_aria_label}.` // add a period to the end of the aria label to have screen readers pause. : ''; if (page.url) { return `<a class="hs-search-results__pagination__link hs-search-results__pagination__link--number ${activeClass}" aria-label="${currentPageAriaLabel} ${moduleConfig.page_number_aria_label} ${page.display}" href="${page.url}">${page.display}</a>`; } return `<span class="hs-search-results__pagination__link">${page.display}</span>`; }) .join(''); }; const renderPageNavigation = (paginator) => { const disabledClass = paginator.currentPage === 1 || paginator.currentPage === paginator.totalPagesOfItems ? 'hs-search-results__pagination__link--disabled' : ''; const firstPageLinkAriaLabel = moduleConfig.first_page_link_text; const firstPageLinkLabel = `${moduleConfig.first_page_icon_markup} ${moduleConfig.first_page_link_text}`; const firstPageLinkClass = `hs-search-results__pagination__link--first ${disabledClass}`; const prevPageLinkAriaLabel = moduleConfig.previous_page_link_text; const prevPageLinkLabel = `${moduleConfig.previous_page_icon_markup} ${moduleConfig.previous_page_link_text}`; const prevPageLinkClass = `hs-search-results__pagination__link--prev ${disabledClass}`; const nextPageLinkAriaLabel = moduleConfig.next_page_link_text; const nextPageLinkLabel = `${moduleConfig.next_page_link_text} ${moduleConfig.next_page_icon_markup}`; const nextPageLinkClass = `hs-search-results__pagination__link--next ${disabledClass}`; const lastPageLinkAriaLabel = moduleConfig.last_page_link_text; const lastPageLinkLabel = `${moduleConfig.last_page_link_text} ${moduleConfig.last_page_icon_markup}`; const lastPageLinkClass = `hs-search-results__pagination__link--last ${disabledClass}`; return ` <nav class="hs-search-results__pagination" role="navigation" aria-label="${ moduleConfig.navigation_aria_label }"> ${renderLabelledPageLink( firstPageLinkAriaLabel, firstPageLinkLabel, firstPageLinkClass, paginator.prevPageUrl, moduleConfig.show_first_and_last_links )} ${renderLabelledPageLink( prevPageLinkAriaLabel, prevPageLinkLabel, prevPageLinkClass, paginator.prevPageUrl, moduleConfig.show_previous_and_next_links )} ${renderPageNumberLinks( paginator.pages, paginator.currentPage, moduleConfig.show_numbers )} ${renderLabelledPageLink( nextPageLinkAriaLabel, nextPageLinkLabel, nextPageLinkClass, paginator.nextPageUrl, moduleConfig.show_previous_and_next_links )} ${renderLabelledPageLink( lastPageLinkAriaLabel, lastPageLinkLabel, lastPageLinkClass, paginator.lastPageUrl, moduleConfig.show_first_and_last_links )} </nav> `; }; const renderSearchResultHeading = (searchResultTitle) => { const headingLevel = ( moduleConfig.search_results_heading_tag || 'h2' ).replace('h', ''); const resultHeadingLevel = parseInt(headingLevel, 10) + 1; return ` <h${resultHeadingLevel} class="hs-search-results__title">${searchResultTitle}</h${resultHeadingLevel}> `; }; const renderFeaturedImage = (featuredImageUrl) => { if (!featuredImageUrl) { return ''; } // Alt intentionally left blank as it is not needed for decorative images. `; }; const renderIndividualResult = (result) => { const searchResultHeading = renderSearchResultHeading(result.title); const featuredImage = moduleConfig.search_results_display_featured_images ? renderFeaturedImage( result.featuredImageUrl, `${moduleConfig.featured_image_alt} - ${result.title}` ) : ''; return `<li class="hs-search-results__listing__item"> ${featuredImage} </li> `; }; const renderEmptyResults = (resultData) => { [...searchResultsContainer].forEach((resultContainer) => { const noResultsMessage = moduleConfig.no_results_message.replace( '[[search_term]]', `“${resultData.searchTerm}”` ); `; }); }; const renderSearchResults = (resultData) => { const paginator = getPaginator( resultData.total, resultData.offset, resultData.limit ); const offsetString = parseInt(resultData.offset, 10) + 1; const limitString = parseInt(resultData.results.length, 10) + parseInt(resultData.offset, 10); const totalString = resultData.total; const results = resultData.results .map((result) => { return renderIndividualResult(result); }) .join(''); [...searchResultsContainer].forEach((resultContainer) => { const searchResultsCountMessage = moduleConfig.search_results_count_message .replace('[[offset]]', offsetString) .replace('[[limit]]', limitString) .replace('[[total]]', totalString); resultContainer.innerHTML = ` <p class="hs-search-results__message" aria-role="status">${searchResultsCountMessage}</p> <ul class="hs-search-results__listing"> ${results} </ul> ${renderPageNavigation(paginator)} `; }); }; const search = (searchParams) => { const searchUrl = `${SEARCH_URL_BASE}?${searchParams.toString()}`; const request = new XMLHttpRequest(); request.open('GET', searchUrl, true); request.onload = function () { if (request.status >= 200 && request.status < 400) { const resultData = JSON.parse(request.responseText); if (resultData.results.length > 0) { renderSearchResults(resultData); } else { renderEmptyResults(resultData); } } else { console.error('Server reached, error retrieving results.'); // eslint-disable-line no-console } }; request.onerror = function () { console.error('Could not reach the server.'); // eslint-disable-line no-console }; request.send(); }; document.addEventListener('DOMContentLoaded', () => { const currentSearchParams = new URLSearchParams(window.location.search); currentSearchParams.set('analytics', 'true'); if (currentSearchParams.has('term')) { currentSearchParams.set('q', currentSearchParams.get('term')); currentSearchParams.delete('term'); } if (currentSearchParams.get('q')?.length) { search(currentSearchParams); } }); ``` ### v0 ```json [ { "id": "cbd47343-8232-3ff0-5757-c0c104454ca2", "name": "display_featured_images", "label": "Display featured images", "sortable": false, "required": false, "locked": false, "type": "boolean", "default": false } ] ``` ```html {% endif %} <a href="#" class="hs-search-results__title">Content Title</a> <p class="hs-search-results__description">Description</p> </li> </template> <ul id="hsresults" class="hs-search-results__listing"></ul> <div class="hs-search-results__pagination" data-search-path="{{ site_settings.content_search_results_page_path }}" > <a href="" class="hs-search-results__prev-page"></a> <a href="" class="hs-search-results__next-page"></a> </div> </div> ``` ```css .hs-search-results__listing { margin: 0; padding: 0; list-style: none; } .hs-search-results__listing li { margin: 0; padding: 0; } .hs-search-highlight { font-weight: bold; } .hs-search-results__prev-page { float: left; } .hs-search-results__next-page { float: right; } ``` ```js var hsResultsPage = function (_resultsClass) { function buildResultsPage(_instance) { var resultTemplate = _instance.querySelector( '.hs-search-results__template' ); var resultsSection = _instance.querySelector('.hs-search-results__listing'); var searchPath = _instance .querySelector('.hs-search-results__pagination') .getAttribute('data-search-path'); var prevLink = _instance.querySelector('.hs-search-results__prev-page'); var nextLink = _instance.querySelector('.hs-search-results__next-page'); var searchParams = new URLSearchParams(window.location.search.slice(1)); /** * v1 of the search input module uses the `q` param for the search query. * This check is a fallback for a mixed v0 of search results and v1 of search input. */ if (searchParams.has('q')) { searchParams.set('term', searchParams.get('q')); searchParams.delete('q'); } function getTerm() { return searchParams.get('term') || ''; } function getOffset() { return parseInt(searchParams.get('offset')) || 0; } function getLimit() { return parseInt(searchParams.get('limit')); } function addResult(title, url, description, featuredImage) { var newResult = document.importNode(resultTemplate.content, true); function isFeaturedImageEnabled() { if ( newResult.querySelector('.hs-search-results__featured-image > img') ) { return true; } } newResult.querySelector('.hs-search-results__title').innerHTML = title; newResult.querySelector('.hs-search-results__title').href = url; newResult.querySelector('.hs-search-results__description').innerHTML = description; if (typeof featuredImage !== 'undefined' && isFeaturedImageEnabled()) { newResult.querySelector( '.hs-search-results__featured-image > img' ).src = featuredImage; } resultsSection.appendChild(newResult); } function fillResults(results) { results.results.forEach(function (result, i) { addResult( result.title, result.url, result.description, result.featuredImageUrl ); }); } function emptyPagination() { prevLink.innerHTML = ''; nextLink.innerHTML = ''; } function emptyResults(searchedTerm) { resultsSection.innerHTML = '; } function setSearchBarDefault(searchedTerm) { var searchBars = document.querySelectorAll('.hs-search-field__input'); Array.prototype.forEach.call(searchBars, function (el) { el.value = searchedTerm; }); } function httpRequest(term, offset) { var SEARCH_URL = '/_hcms/search?'; var requestUrl = SEARCH_URL + searchParams + '&analytics=true'; var request = new XMLHttpRequest(); request.open('GET', requestUrl, true); request.onload = function () { if (request.status >= 200 && request.status < 400) { var data = JSON.parse(request.responseText); setSearchBarDefault(data.searchTerm); if (data.total > 0) { fillResults(data); paginate(data); } else { emptyResults(data.searchTerm); emptyPagination(); } } else { console.error('Server reached, error retrieving results.'); } }; request.onerror = function () { console.error('Could not reach the server.'); }; request.send(); } function paginate(results) { var updatedLimit = getLimit() || results.limit; function hasPreviousPage() { return results.page > 0; } function hasNextPage() { return results.offset <= results.total - updatedLimit; } if (hasPreviousPage()) { var prevParams = new URLSearchParams(searchParams.toString()); prevParams.set( 'offset', results.page * updatedLimit - parseInt(updatedLimit) ); prevLink.href = '/' + searchPath + '?' + prevParams; prevLink.innerHTML = '< Previous page'; } else { prevLink.parentNode.removeChild(prevLink); } if (hasNextPage()) { var nextParams = new URLSearchParams(searchParams.toString()); nextParams.set( 'offset', results.page * updatedLimit + parseInt(updatedLimit) ); nextLink.href = '/' + searchPath + '?' + nextParams; nextLink.innerHTML = 'Next page >'; } else { nextLink.parentNode.removeChild(nextLink); } } var getResults = (function () { if (getTerm()) { httpRequest(getTerm(), getOffset()); } else { emptyPagination(); } })(); } (function () { var searchResults = document.querySelectorAll(_resultsClass); Array.prototype.forEach.call(searchResults, function (el) { buildResultsPage(el); }); })(); }; if ( document.attachEvent ? document.readyState === 'complete' : document.readyState !== 'loading' ) { var resultsPages = hsResultsPage('div.hs-search-results'); } else { document.addEventListener('DOMContentLoaded', function () { var resultsPages = hsResultsPage('div.hs-search-results'); }); } ``` # Default web modules @ Learn about the default modules that HubSpot provides for creating website page, blog post, and blog listing templates. These modules are separate from default email modules, which are for email templates. $ reference & cms/modules --- # Default web modules Below, learn about the default modules that HubSpot provides for building templates for website pages, blog posts, and blog listing pages. You'll also find default modules that can be used to build quote templates. When developing locally, you can [fetch](/guides/cms/tools/local-development-cli#fetch-files) a specific default module using the module path (e.g. `hs fetch @hubspot/linked_image.module`). To view a default module's code, you can view and clone the module within the `@hubspot` folder of the design manager. In the code, some default modules use the default account ID 7052064, rather than the one belonging to the current account. Default web modules are separate from [default email modules](/reference/cms/modules/default-email-modules), which are for email templates. If your email templates include any of the following default web modules, you should [replace them with the corresponding email-specific module](/guides/cms/content/templates/default-email-modules): - `cta` - `header` - `linked_image` - `logo` - `post_filter` - `post_listing` - `section_header` - `social_sharing` - `text` ## Blog comments Supported in blog posts and blog listings. ```hubl {% module "blog_comments" path="@hubspot/blog_comments" %} ``` ## Blog email subscription Supported in pages, blog posts, and blog listings. ```hubl {% module "blog_subscribe" path="@hubspot/blog_subscribe" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `select_blog` | Blog | The blog to use for the module. | | | `title` | String | Title for the module (wrapped in a h3 tag) | `"Subscribe Here!"` | | `response_message` | Rich Text | The message that is shown upon submitting the form. | `Thanks for subscribing!` | ## Blog posts Add this module to blog listing pages to display blog post previews containing each post's title, featured image, author, publish date, and more with a clickable button that navigates to the post. This default module has built using React, and you can [view its source code on GitHub](https://github.com/HubSpot/cms-react/tree/main/default-react-modules/src/components/modules/BlogPosts-V0). - This module cannot be accessed from the design manager. The module can be referenced with HubL in coded templates and added within the [blog listing page editor](https://knowledge.hubspot.com/blog/edit-a-blog-listing-page). - This module replaces the previous `blog_listing` module, which has been deprecated. ```hubl {% module "blog_posts" path="@hubspot/blog_posts" layout="grid", columns=4, displayForEachListItem=[ "image", "title", "authorName", "tags", "publishDate", "description", "button" ] %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `layout` | `grid` | | `columns` | Number | When using the `grid` layout, the number of posts per row. Can be `2`, `3`, or `4`. | `3` | | `alternateImage` | Boolean | When using the `sideBySide` layout, set to `true` to align the featured image on the right and left side of the post preview, alternating. | `false` | | `fullImage` | Boolean | When using the `grid` or `singleColumn` layouts, set this field to `true` to make the featured image the background of the post preview. | `false` | | `displayForEachListItem` | Array | The content to include in each blog post preview. Choices include:<ul><li>`image`: the post's featured image.</li><li>`title`: the post's title.</li><li>`authorImage`: the post author's image.</li><li>`authorName`: the post author's name.</li><li>`tags`: the post's blog tags.</li><li>`publishDate`: the post's publish date.</li><li>`description`: the post's meta description.</li><li>`button`: the read more button that links to the blog post.</li></ul> | `[ 'image', 'title', 'authorImage', 'authorName', 'tags', 'publishDate', 'description', 'button' ]` | | `buttonText` | String | The text that displays on the read more button, if included. | `Read more` | ## Blog post filter Supported in pages, blog listings, and blog posts. ```hubl {% module "post_filter" path="@hubspot/post_filter" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `select_blog` | Blog | Select a blog to display. Default will use the current blog when used in a blog template or the primary blog when used elsewhere. | | | `filter_type` | Choice | Type of filtering links to show. Choices include:<ul><li>`tag`</li><li>`month`</li><li>`author`</li></ul> | `tag` | | `order_by` | Choice | Ordering for the values of filter links. Choices include:<ul><li>`post_count`</li><li>`name`</li></ul> | `post_count` | | `list_title` | Text | An H3 heading. | `"Posts by Tag"` | | `max_links` | Number | Number of filter links to show. Leave blank to show all. | `5` | | `expand_link_text` | Text | Text to display if more than the `max_links` value to display are available. | `"See all"` | ## Blog post listing Supported in pages, blog listings, and blog posts. A new version of this module was release at the end of March 2024. Learn more about the [new default post listing module](/reference/cms/modules/default-module-versioning#post-listing). ```hubl {% module "post_listing" path="@hubspot/post_listing" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `select_blog` | Blog | Select a blog to display. Default will use the current blog when used in a blog template or the primary blog when used elsewhere. | | | `listing_type` | Choice | The type of listing for your posts. Choices include:<ul><li>`recent`: most recent.</li><li>`popular_all_time`: most popular of all time.</li><li>`popular_past_year`: most popular the past year.</li><li>`popular_past_six_months`: most popular the past six months.</li><li>`popular_past_month`: most popular the past month.</li></ul> | `recent` | | `list_title` | Text | An H3 heading. | `"Recent Posts"` | | `max_links` | Number | Maximum number of posts to display. | `5` | ## Button Supported in pages, blog posts, and blog listings. ```hubl {% module "button" path="@hubspot/button" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `link` | Link | The URL that the button redirects to. | `{ "url": { "type": "EXTERNAL", "href": "www.hubspot.com", "content_id": null }, "open_in_new_tab": false, "no_follow": false }` | | `button_text` | Text | Text that will be displayed on the button. | `"Add a button link here"` | | `style` | Object | <ul><li>`override_default_style` (Boolean)<br /></li><li>`button_font` (Font)<br /></li><li>`button_color` (Color)<br /></li><li>`text_hover_color` (Color)<br /></li><li>`button_hover_color` (Color)</li></ul> | `{ "override_default_style": false, "button_font": { "color": "#FFFFFF", "size_unit": "px" }, "button_color": { "color": "#000000", "opacity": 100 }, "text_hover_color": { "color": "#000000", "opacity": 100 }, "button_hover_color": { "color": "#CCCCCC", "opacity": 100 } }` | ## Call-to-Action Supported in pages, blog listings, and blog posts. ```hubl {% module "cta" path="@hubspot/cta" %} ``` | Parameter | Type | Description | | --------- | ------ | -------------------------------------- | | `guid` | String | Globally Unique Identifier of the CTA. | ## Delete data You can add this module to your subscription preferences page to allow contacts to request that their data be deleted. This function is required under certain data privacy laws. Once a contact requests that their data be deleted, they have 30 minutes to confirm in an email that will automatically be sent. Users with [super admin permissions](https://developers.hubspot.com/settings/hubspot-user-permissions-guide) will receive a notification email about these requests. Learn how to [allow contacts to request a download of their data](#download-data). ```hubl {% module "delete_data" path="@hubspot/delete_data", label="delete_data.module" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `content` | Rich Text | Message displayed above the button. | ` ### Delete Data Permanently delete your personal data stored by {{ site_settings.company_name}}. Personal data is information that can be used to identify you and doesn't include anonymized data. You'll get a follow-up email where you'll need to verify your request. ` | | `button_text` | Text | Button text. | `Request data deletion` | | `group_alerts` | Field group | The success and fail alert field group. Including the following field groups:<br /><ul><li>`group_success_alert`:<ul><li>`content`: rich text content of success alert.</li></ul></li><li>`group_fail_alert`:<ul><li>`content`: rich text content of fail alert.</li></ul></li><li>`group_close_icon`:<ul><li>`icon`: the icon to close the alert.</li></ul></li></ul> | | ## Divider Supported in pages. There's a [new version of this module](/reference/cms/modules/default-module-versioning#divider) available in accounts created after August 25th, 2022. [Learn more about this change](https://developers.hubspot.com/changelog/divider-module-v1). ```hubl {% module "divider" path="@hubspot/divider" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `height` | Number | Pixel (px) height of the divider line. | `1` | | `width` | Number | Percentage (%) width of the divider line. | `50` | | `color` | Color | Color (hex) and opacity (number) of the divider line. | `{ "color": "#000000", "opacity": 100 }` | | `line_type` | Choice | Line type. Choices include:<ul><li>`solid`</li><li>`dotted`</li><li>`dashed`</li></ul> | `solid` | | `alignment` | Choice | Horizontal alignment of divider line. Choices include:<ul><li>`left`</li><li>`center`</li><li>`right`</li></ul> | `center` | | `show_padding` | Boolean | Show/hide top and bottom margining on the divider. | `false` | | `padding` | Number | Pixel (px) value for the margining on top and bottom of divider line. Option available when `show_padding` equals `true`. | `5` | ## Download data You can add this module to your subscription preferences page to allow contacts to request a copy of their data. This function is required under certain data privacy laws. Users with [super admin permissions](https://developers.hubspot.com/settings/hubspot-user-permissions-guide) will receive a notification email about these requests. Learn how to [allow contacts to request that their data be deleted](#delete-data). ```hubl {% module "download_data" path="@hubspot/download_data", label="download_data.module" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `content` | Rich Text | Message displayed above the button. | ` ### Download Data Download your personal data stored by {{ site_settings.company_name}}. Personal data is information that can be used to identify you and doesn't include anonymized data. ` | | `button_text` | Text | Button text. | `Request data download` | | `group_alerts` | Field group | The success and fail alert field group. Including the following field groups:<br /><ul><li>`group_success_alert`:<ul><li>`content`: rich text content of success alert.</li></ul></li><li>`group_fail_alert`:<ul><li>`content`: rich text content of fail alert.</li></ul></li><li>`group_close_icon`:<ul><li>`icon`: the icon to close the alert.</li></ul></li></ul> | | ## Form Supported in pages, blog posts, and blog listings. ```hubl {% module "form" path="@hubspot/form" form={ "form_id": "9e633e9f-0634-498e-917c-f01e355e83c6", "response_type": "redirect", "message": "Thanks for submitting the form.", "redirect_id": null, "redirect_url": "http://www.google.com" } %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `title` | Text | H3 heading | | | `form` | Object | Form object including:<ul><li>`form_id`: ID for form to use</li><li>`response_type`: what the visitor will see after submitting the form:<ul><li>`inline`</li><li>`redirect`</li></ul></li><li>`message`: inline message if `response_type` is set to `inline`</li><li>`redirect_id`: ID of page to be redirected to if `response_type` is set to `redirect`.</li><li>`redirect_url`: URL to be redirected to if `response_type` is set to `redirect`</li></ul> | | `{ "form_id": "", "response_type": "redirect", "message": "Thanks for submitting the form.", "redirect_id": null, "redirect_url": "http://www.google.com" }` | | `notifications_are_overridden` | Boolean | Email to send form notification to instead of form defaults. | `false` | | `notifications_override_email_addresses` | Email | Comma-separated list of emails to send to when `notifications_are_overridden` equals `true`. | | | `follow_up_type_simple` | Boolean | Enabled sending a follow up email. | `false` | | `simple_email_for_live_id` | Followupemail | ID of the follow-up email. Available when `follow_up_type_simple` equals `true`. | | | `sfdc_campaign` | Salesforcecampaign | When Salesforce integration is active, the campaign ID. | | ## Header Supported in pages, blog listings, and blog posts. ```hubl {% module "header" path="@hubspot/header" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `value` | Text | Text for the heading. | `"A clear and bold header"` | | `header_tag` | Choice | Choose a heading level. Choice include `h1` through `h6`. | `h1` | ## Horizontal spacer Supported in pages, blog listings, blog posts, and emails. ```hubl {% module "horizontal_spacer" path="@hubspot/horizontal_spacer" %} ``` ## Icon Supported in pages, blog listings, and blog posts. Icons can be pulled from the Font Awesome 5.0.10 and 5.14.0 icon sets. ```hubl {% module "icon" path="@hubspot/icon" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `name` | String | The name of the icon. | `"hubspot"` | | `purpose` | Choice | An accessibility option that categorizes the purpose of the icon for screen readers. Available values are:<ul><li>`decorative:` ignored by the screen reader.</li><li>`semantic:` read by the screen reader.</li></ul> | `"decorative"` | | `title` | String | An accessibility option that assigns the icon a title. | | | `style` | String | The type of icon. Can be one of `solid`, `regular`, `light`, `thin`, or `duotone`. | `"solid"` | | `unicode` | String | The icon's unicode value. | `f3b2` | ## Image Supported in pages, blog listings, and blog posts. ```hubl {% module "linked_image" path="@hubspot/linked_image" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `img` | Image | Image object containing:<ul><li>`src`: image url</li><li>`alt`: alt text for image</li><li>`loading`: lazy loading options include:<ul><li>`disabled`</li><li>`lazy`</li></ul></li><li>`width`: px value</li><li>`height`: px value</li></ul> | `{ "src": "https://static.hubspot.com/final/img/content/email-template-images/placeholder_200x200.png", "alt": "placeholder_200x200", "loading": "disabled", "width": 200, "height": 200 }` | | `link` | Text | Optional link for the image. | | | `target` | Boolean | Opens link in a new tab. | `false` | ## Image grid Supported in pages, blog listings, and blog posts. ```hubl {% module "image_grid" path="@hubspot/image_grid", label="image_grid.module" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `slides` | Object | Object containing information for each slide. Object contains:<ul><li>`img`: image URL.</li><li>`link_url`: URL where the slide should link.</li><li>`hover_messages`: text that overlays on the image on hover.</li></ul> | `[ { "show_caption": false, "open_in_new_tab": false } ]` | | `display_mode` | Choice | Display mode of the Image Gallery. Choices include:<ul><li>`standard`: standard slider.</li><li>`thumbnail`: thumbnail navigator.</li><li>`lightbox`: lightbox gallery.</li></ul> | `standard` | | `lightboxRows` | Number | Number of rows in the Lightbox gallery when `display_mode` equals `lightbox`. | `3` | | `loop_slides` | Boolean | Enables looping through the slides with next/prev when `display_mode` equals `standard` or `thumbnail`. | `true` | | `auto_advance` | Boolean | Automatically advances to the next slide when `display_mode` equals `standard` or `thumbnail`. | `true` | | `num_seconds` | Number | Amount of time (seconds) between advancing to the next slide when `display_mode` equals `standard` or `thumbnail`. | `5` | | `show_pagination` | Boolean | Show navigation buttons when `display_mode` equals `standard` or `thumbnail`. | `true` | | `sizing` | Choice | Sets the height of the slides when `display_mode` equals `standard` or `thumbnail`. Choices include:<ul><li>`static`: fixed height.</li><li>`resize`: variable height.</li></ul> | `static` | | `transition` | Choice | Slide transition styles when `display_mode` equals `standard` or `thumbnail`. Choices include:<ul><li>`slide`: slide transition.</li><li>`fade`: fade transition.</li></ul> | `slide` | | `caption_position` | Choice | Position of the slide captions when `display_mode` equals `standard` or `thumbnail`. Choices include:<ul><li>`below`: always keep captions below the image.</li><li>`superimpose`: superimpose captions on top of images.</li></ul> | `below` | ## Image gallery Supported in pages, blog posts, and blog listings. ```hubl {% module "gallery" path="@hubspot/gallery" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `slides` | Object | Object containing information for each slide. Object contains:<ul><li>`img`: image URL.</li><li>`show_caption`: boolean to show slide caption.</li><li>`caption`: rich text for caption.</li><li>`link_url`: URL where the slide should link.</li><li>`open_in_new_tab`: boolean to direct if the link should open in a new tab.</li></ul> | `[ { "show_caption": false, "open_in_new_tab": false } ]` | | `display_mode` | Choice | Display mode of the Image Gallery. Choices include:<ul><li>`standard`: standard slider.</li><li>`thumbnail`: thumbnail navigator.</li><li>`lightbox`: lightbox gallery.</li></ul> | `standard` | | `lightboxRows` | Number | Number of rows in the Lightbox gallery when `display_mode` equals `lightbox`. | `3` | | `loop_slides` | Boolean | Enables looping through the slides with next/prev when `display_mode` equals `standard` or `thumbnail`. | `true` | | `auto_advance` | Boolean | Automatically advances to the next slide when `display_mode` equals `standard` or `thumbnail`. | `true` | | `num_seconds` | Number | Amount of time (seconds) between advancing to the next slide when `display_mode` equals `standard` or `thumbnail`. | `5` | | `show_pagination` | Boolean | Show navigation buttons when `display_mode` equals `standard` or `thumbnail`. | `true` | | `sizing` | Choice | Sets the height of the slides when `display_mode` equals `standard` or `thumbnail`. Choices include:<ul><li>`static`: fixed height.</li><li>`resize`: variable height.</li></ul> | `static` | | `transition` | Choice | Slide transition styles when `display_mode` equals `standard` or `thumbnail`. Choices include:<ul><li>`slide`: slide transition.</li><li>`fade`: fade transition.</li></ul> | `slide` | | `caption_position` | Choice | Position of the slide captions when `display_mode` equals `standard` or `thumbnail`. Choices include:<ul><li>`below`: always keep captions below the image.</li><li>`superimpose`: superimpose captions on top of images.</li></ul> | `below` | ## Image slider gallery Supported in pages, blog posts, and blog listings. ```hubl {% module "unique_name" path="@hubspot/image_slider_gallery", slides=[ { "img": { "src": "", "alt": "Default image alt text" }, "caption": "<strong>1</strong> An optional caption for the image that will be added to the gallery. Enter any descriptive text for this image that you would like visitors to be able to read.", "link_url": "" }, { "img": { "src": "", "alt": "Default image alt text" }, "caption": "<strong>2</strong> An optional caption for the image that will be added to the gallery. Enter any descriptive text for this image that you would like visitors to be able to read.", "link_url": "" } ] slideshow_settings={ "slides": { "per_page": 1, "sizing": "natural", "aspect_ratio": "16/9", "show_captions": true, "caption_position": "below" }, "movement": { "transition": "slide", "loop_slides": false, "auto_advance": false, "auto_advance_speed_seconds": 5 }, "navigation": { "show_main_arrows": true, "show_thumbnails": false, "show_dots": false } } %} ``` | Parameter | Type | Description | | --- | --- | --- | | `slides` | Field group | A field group containing the following fields:<ul><li>`img`: image URL.</li><li>`caption`: rich text for image caption.</li><li>`link_url`: URL that the slide links to.</li></ul> | | `slideshow_settings` | Field group | The image slider settings field group. Includes the following field groups:<ul><li>`slides`: slide settings, including:<ul><li>`per_page`: number of slides per page.</li><li>`sizing`: image sizing.</li><li>`aspect_ratio`: image aspect ratio.</li><li>`show_captions`: a boolean</li></ul></li><li>`movement`: image sliding behavior settings.</li><li>`navigation`: image navigation settings.</li></ul> | | `default_text` | Field group | The module's default text elements, including:<ul><li>`default_caption`: image caption.</li><li>`default_image_alt_text`: image text.</li><li>`slider_aria_label`: the module's default aria label.</li><li>`arial_label_thumbnails`: the module's default aria thumbnail.</li><li>`slider_nav_aria_label`: the module navigation's default aria label.</li><li>`prev`: previous slide text.</li><li>`next`: next slide text.</li><li>`first`: go to first slide text.</li><li>`slideX`: go to X slide text.</li><li>`pageX`: go to X page text.</li><li>`play`: start autoplay text.</li><li>`pause`: pause autoplay text.</li><li>`carousel`: carousel text.</li><li>`select`: text for selecting a slide to show.</li><li>`slide`: slide text.</li><li>`slidelabel`: slide label.</li></ul> | ## Language switcher Supported in pages. ```hubl {% module "language_switcher" path="@hubspot/language_switcher" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `display_mode` | Choice | The language of the text in the language switcher. Options include:<ul><li>`pagelang`: the names of languages will display in the language of the page the switcher is on.</li><li>`localized`: the name of each language will display in that language.</li><li>`hybrid`: a combination of the two.</li></ul> | `localized` | ## Logo Supported in pages, blog listings, and blog posts. ```hubl {% module "logo" path="@hubspot/logo" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `img` | Image | Image object containing:<ul><li>`override_inherited_src`: override the default logo from settings</li><li>`src`: image url</li><li>`alt`: alt-text for logo</li></ul> | `{ "override_inherited_src": false, "src": null, "alt": null }` | | `link` | Text | Optional link for the logo. If no URL is specified, your logo will link to the _Logo URL_ set in your [brand settings](https://knowledge.hubspot.com/branding/edit-your-logo-favicon-and-brand-colors#edit-your-logo). | | | `open_in_new_tab` | Boolean | Opens link in a new tab. | `false` | | `suppress_company_name` | Boolean | Hide the company name when an image is not selected. | `true` | | `heading_level` | Choice | Choose a heading level when no image is selected and `suppress_company_name` equals `false`. Choices include `h1` through `h6`. | `h1` | ## Logo grid A customizable grid of containers to display logos uniformly. Supported in pages, blog listings, and blog posts. ```hubl {% module "logo grid" path="@hubspot/logo_grid" group_logos=[ { "logo": { "loading": "lazy", "alt": "company_logo", "src": "https://www.example.com/Logos/color/logo.png" } }, { "logo": { "loading": "lazy", "alt": "company2_logo", "src": "https://www.example.com/Logos/color/logo2.png" } }, { "logo": { "alt": "lorem-logo", "height": 40, "loading": "lazy", "max_height": 40, "max_width": 175, "src": "https://www.example.com/Logos/color/logo3.png", "width": 175 } } ], styles={ "group_logo": { "group_background": { "aspect_ratio": "1/1", "background_color": { "color": "#8E7CC3", "opacity": 100 } }, "group_spacing": { "padding": { "padding": { "bottom": { "units": "px", "value": 75 }, "left": { "units": "px", "value": 75 }, "right": { "units": "px", "value": 75 }, "top": { "units": "px", "value": 75 } } } }, "max_logo_height": 85 }, "group_logo_grid": { "column_count": 3, "grid_gap": 54 } } %} ``` | Parameter | Type | Description | | --- | --- | --- | | `group_logos` | Array | An array containing an object for each logo in the grid. | | `logo` | Object | In `group_logos`, an object for each logo in the grid. Each logo can include the following parameters:<ul><li>`src`: the file path of the logo.</li><li>`loading`: the [lazy load](/guides/cms/content/performance/lazy-loading) behavior of the logo.</li><li>`alt`: the logo's alt-text.</li><li>`height`: the logo's height.</li><li>`width`: the logo's width.</li><li>`max_height`: the logo's maximum height.</li><li>`max_width`: the logo's maximum width.</li></ul> | | `styles` | Array | An array containing the style fields that affect the grid layout, logo containers, and logo images. This array contains the following style groups:<ul><li>`group_logo`: styles that affect the logo containers and logo images. Contains the following:<ul><li>`group_background`: styles that set the aspect ratio and background color of the grid containers. Aspect ratios include: `1/1`, `4/3`, `16/9`.</li><li>`group_spacing`: styles that set the inner padding of the logo container. For wider logos, higher padding value may decrease logo width.</li><li>`max_logo_height`: the maximum height of each logo.</li></ul></li><li>`group_logo_grid`: styles that set the grid layout, including:<ul><li>`column_count`: the number of columns in the grid.</li><li>`grid_gap`: the spacing between grid items.</li></ul></li></ul> | ## Meetings Supported in pages, blog posts, and blog listings. ```hubl {% module "meetings" path="@hubspot/meetings" %} ``` | Parameter | Type | Description | | --------- | ------- | ---------------------- | | `meeting` | Meeting | Select a meeting link. | ## Membership social logins This module provides Google and Facebook login capability to memberships sites. The user must sign-in with an account linked to the email for the contact in the CRM. You can choose which social logins to enable. **Supported in membership login pages.** ```hubl {% module "social" path="@hubspot/membership_social_logins", clientid="" appid="" facebook_enabled=true google_enabled=true %} ``` Facebook requires having a [Facebook developer account](https://developers.facebook.com/docs/development/register), and a [facebook app created](https://developers.facebook.com/docs/development/create-an-app), with basic settings. Once you've done that your app id is what you pass to the module. Google requires a Google account, and [authorization credentials created](https://developers.google.com/identity/sign-in/web/sign-in#create_authorization_credentials), once you have that your app's client id is what you pass to the module. | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `facebook_appid` | String | Your facebook app ID. | | | `facebook_enabled` | boolean | Enable the button for Facebook login. `facebook_appid` is required**.** | `False` | | `google_clientid` | String | Your Google client ID. | | | `google_enabled` | Boolean | Enable the button for Google login. `google_clientid` is required**.** | `False` | ## Menu Supported in pages, blog posts, and blog listings. Looking to build your own custom menu? Try our [menu() function](/reference/cms/hubl/functions#menu). ```hubl {% module "menu" path="@hubspot/menu" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `id` | Menu | ID of the menu. | | | `root_type` | Choice | Advanced menu type. Choices include:<ul><li>`site_root`: always show top-level pages in menu.</li><li>`top_parent`: show pages in menu that are related to section being viewed.</li><li>`parent`: show pages in menu that are related to page being viewed.</li><li>`breadcrumb`: breadcrumb style path menu (uses horizontal flow).</li></ul> | `site_root` | | `max_levels` | Choice | Determines the number of menu tree children that can be expanded in the menu. Choices include `1` through `10` | `2` | | `flow` | Choice | Orientation of the menu. Choices include:<ul><li>`horizontal`</li><li>`vertical`</li></ul> | `horizontal` | | `flyouts` | Boolean | Enabled hover over functionality for child menu items. | `true` | ## Page footer Supported in pages, blog listings, and blog posts. ```hubl {% module "page_footer" path="@hubspot/page_footer" %} ``` ## Password prompt Supported in pages, blog posts, and blog listings. ```hubl {% module "password_prompt" path="@hubspot/password_prompt" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `submit_button_text` | Text | Text that appears on the submit button. | `"Submit"` | | `password_placeholder` | Text | Placeholder text for the password field. | `"Password"` | | `bad_password_message` | Rich Text | Message to show when a password is entered incorrectly. | `"Sorry, please try again. "` | ## Payments Supported in pages, blog posts, and blog listings. ```hubl {% module "payments" path="@hubspot/payments" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `payment` | String | To set the module to use a specific payment link, include the ID of the payment link. You can [find the ID](/reference/cms/fields/module-theme-fields#link) in the URL while editing the payment link. | | | `checkout_location` | String | Set whether the payment form opens in a new tab or in an overlay. Available values are:<ul><li>`new_tab`: opens the payment form in a new tab.</li><li>`overlay`: opens the payment form in a sliding overlay.</li></ul> | `"new_tab"` | | `button_text` | String | Sets the text of the checkout button. | `"Checkout"` | | `button_target` | Choice | Whether the button uses a HubSpot payment link or an external link. Choices include:<ul><li>`payment_link`</li><li>`custom_link`</li></ul> | `"payment_link"` | | `button_link` | Link | When `button_target` is set to `custom_link`, sets the destination of the external link. Supported link types include:<ul><li>`EXTERNAL`</li><li>`CONTENT`</li></ul> | `EXTERNAL` | ## Product Displays a product from the account's [product library](https://knowledge.hubspot.com/products/how-do-i-use-products). Supported in pages, blog posts, and blog listings. ```hubl {% module path="@hubspot/product", product={ "id" : 2124070179 }, group_button={ "button_text" : "Buy", "override_product_button" : true, "button_override" : { "no_follow" : false, "open_in_new_tab" : false, "sponsored" : false, "url" : { "href" : "https://www.test.com", "type" : "EXTERNAL" } }, group_description={ "description_override" : "Monthly gym membership with access to shared locker facilities.", "override_product_description" : true }, group_name={ "heading_level" : "h3", "name_override" : "Gym membership", "override_product_name" : true }, group_image={ "image_override" : { "alt" : "360_F_317724775_qHtWjnT8YbRdFNIuq5PWsSYypRhOmalS", "height" : 360, "loading" : "lazy", "src" : "https://2272014.fs1.hubspotusercontent-na1.net/hubfs/2272014/360_F_317724775_qHtWjnT8YbRdFNIuq5PWsSYypRhOmalS.jpg", "width" : 640 }, "override_product_image" : true } %} ``` | Parameter | Type | Description | | --- | --- | --- | | `product` | Product | The product to display, specified by product ID. | | `group_button` | Field group | By default, the module includes a button that directs users to the product's set URL. To customize the button destination, include this optional field group along with the following fields:<ul><li>`button_text`: a string that sets the button's text.</li><li>`override_product_button`: set to `true` to override the default button link settings. Then, include a `button_override` object. Learn more about `button_override` below.</li></ul> | | `button_override` | Object | In the `group_button` field group, this sets the button's URL behavior when `override_product_button` is set to `true`.Includes the following fields:<ul><li>`no_follow`: boolean field for the link's [no_follow](https://ahrefs.com/blog/nofollow-links/) behavior.</li><li>`open_link_in_new_tab`: boolean field for the link's open behavior.</li><li>`url`: an object that specifies the button's destination.</li></ul>In the `url` field, you can set the type of destination through the `type` field, which supports the following content types:<ul><li>`EXTERNAL`: a standard URL. Include the URL in an `href` field.</li><li>`CONTENT`: a HubSpot page. Include the page ID in a `content_id` field.</li><li>`PAYMENT`: a payment link. Include the payment link in an `href` field.</li><li>`EMAIL_ADDRESS`: an email address. Include the address in an `href` field.</li></ul> | | `group_name` | Field group | By default, the module will display the product name at the top of the module as an h3. To customize the name, include this optional field group along with the following fields:<ul><li>`heading_level`: the heading size. Can be `h1` - `h6`.</li><li>`override_product_name`: set to `true` to specify text rather than the product name.</li><li>`name_override`: the string that you want to display at the top of the module.</li></ul> | | `group_description` | Field group | By default, the module will display the product's set description. To customize the description, include this optional field group along with the following fields:<ul><li>`override_product_description`: set to `true` to customize the product description.</li><li>`description_override`: a string containing your new description.</li></ul> | | `group_image` | Field group | By default, the module will display the product's set image. To customize this image, include this optional field group with the following fields:<ul><li>`override_product_image`: set to `true` to specify a new image. </li><li>`image_override`: an object that specifies the new image, including the following properties:<ul><li>`alt`</li><li>`height`</li><li>`loading`</li><li>`src`</li><li>`width`</li></ul></li></ul> | ## Quote download Supported in quote templates. ```hubl {% module "download" path="@hubspot/quote_download" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `button_text` | String | The text displayed on the download button. | `Download` | | `download_error` | String | Error message displayed if the download fails. | `There was a problem downloading the quote. Please try again.` | ## Quote payment Supported in quote templates. ```hubl {% module "payment" path="@hubspot/quote_payment" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `heading_text` | String | The heading displayed above the payment section of a quote template. | `Payment` | | `heading_tag` | Choice | The type of heading used to display `heading_text`. Values include `h1`, `h2`, `h3`, `h4`, `h5`, `h6`. | `h3` | | `checkout` | String | The payment button text. | `Pay now` | | `needs_signature` | String | Button text when a signature is required. | `Payment can't be made because the quote isn't fully signed.` | | `checkout_error` | Rich text | Message that displays when there's an error during checkout. | `There was a problem setting up checkout. Please contact the person who sent you this quote.` | | `payment_status_error` | Rich text | Message that displays when there's an error when trying to make a payment. | `There was a problem loading the payment status for this quote. Please try refreshing the page.` | | `paid` | String | A successful payment status indicator. | `Paid` | | `payment_processing` | String | A payment processing status indicator. | `Payment processing` | ## Quote signature Supported in quote templates. ```hubl {% module "signature" path="@hubspot/quote_signature" %} ``` When a quote requires an e-signature, the following fields are available within an `esignature` object: | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `pretext` | Rich text | E-signature explanation text. | `Before you sign this quote, an email must be sent to you to verify your identity. Find your profile below to request a verification email.` | | `verify_button` | String | Text that displays on the verification button. | `Verify to sign` | | `failure` | String | Alert text that displays when the signature information can't be retrieved. | `There was a problem retrieving the signature information. Please reload the page.` | | `verification_sent` | String | A status indicator that appears when the verification request has been successfully sent to the quote signer. | `Verification sent` | | `signed` | String | Text that displays when the quote has been successfully signed. | `Signed` | When a quote requires a printed signature, the following fields are available within a `print_signature` object: | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `header` | Rich text | Text displayed at the top of the signature section. | `Signature` | | `signature` | String | Text that displays next to the signature field. | `Signature` | | `date` | String | Text that displays next to the date field. | `Date` | | `printed_name` | String | Text that displays next to the printed name field. | `Printed name` | | `countersignature` | Object | An object containing content for the countersignature section. This object can contain the following fields:<ul><li>`header`: text that displays at the top of the countersignature section.</li><li>`countersignature`: Text that displays next to the countersignature field.</li><li>`date`: text that displays next to the date field.</li><li>`printed_name`: text that displays next to the printed name field.</li></ul> | `Signed` | ## Line items Supported in quote templates. This module also includes the default text used on custom quotes. ```hubl {% module "module_165350106057652" path="@hubspot/line_items", label="line_items.module" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `title` | Text | The title of the line item column. | `Column description` | | `use_additional_product_property` | Boolean | Display checkbox to allow users to select additional line item columns from product properties. | `False` | | `additional_product_properties` | Choice | A product property. Choices include: <ul><li>`price`: price of line item. </li><li>`item_description`: description of line item. </li><li>`quantity`: number of line items. </li><li>`amount`: total cost of line items. </li><li>`hs_recurring_billing_start_date`: billing start date for recurring line items. </li><li>`discount`: discount amount applies to the line item. </li></ul> | | | `crm_product_property` | CRM object property | Select any product property to appear as a line item column header. | | ## Rich text Supported in all template types. ```hubl {% module "rich_text" path="@hubspot/rich_text" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `html` | Rich Text | HTML block. | ` ## Something Powerful \n ### Tell The Reader More \n The headline and subheader tells us what you're [offering](\"#\"), and the form header closes the deal. Over here you can explain why your offer is so great it's worth filling out a form for. \n Remember: \n<ul>\n<li>Bullets are great</li>\n<li>For spelling out [benefits](\"#\") and</li>\n<li>Turning visitors into leads.</li>\n</ul>` | ## RSS listing Supported in pages, blog listings, and blog posts. ```hubl {% module "rss_listing" path="@hubspot/rss_listing" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `title` | Text | An H3 heading. | `"Test"` | | `rss_feed_type` | Choice | Type of RSS feed. Choices include:<ul><li>`blog` - HubSpot hosted blog.</li><li>`external` - An external RSS feed.</li></ul> | `blog` | | `rss_url` | Text | RSS URL to use when `rss_feed_type` equals `external`. | | | `content_group_id` | Blog | Id of the blog to use when `rss_feed_type` equals `blog`. | | | `topic_id` | Tag | (optional) Id of the tag to filter by when `rss_feed_type` equals `blog`. | | | `number_of_items` | Number | Maximum number of posts to display. | `5` | | `include_featured_image` | Boolean | Include the featured image. | `false` | | `show_author` | Boolean | Show the author name. | `true` | | `attribution_text` | Text | Text that attributes an author to a post. Displayed when `show_author` equals `true`. | `"by"` | | `show_detail` | Boolean | Show post summary text. | `true` | | `limit_to_chars` | Number | Limits the length of the summary text when `show_detail` equals `true`. | `200` | | `click_through_text` | Text | The text which will be displayed for the clickthrough link at the end of a post summary when `show_detail` equals `true`. | `"Read more"` | | `show_date` | Boolean | Show publish date. | `true` | | `publish_date_format` | Choice | Format for the publish date when `show_date` equals `true`. Choices include:<ul><li>`short` (ex: 06/11/06 12:00PM)</li><li>`medium` (ex: Jun 6, 2006 12:00:00 pm)</li><li>`long` (ex: June 6, 2017 12:00:00 pm EDT)</li><li>`MMMM d, yyyy 'at' h:mm a` (ex: June 6, 2006 at 12:00 pm)</li><li>`h:mm a 'on' MMMM d, yyyy` (ex: 12:00 pm on June 6, 2006)</li></ul> | `short` | | `publish_date_text` | Text | The text that indicates when a post was published when `show_date` equals `true`. | `"posted at"` | ## Site search input Supported in pages, blog posts, and blog listings. ```hubl {% module "search_input" path="./local-search_input" placeholder="Search" include_search_button=true results={ "use_custom_search_results_template": "true", "path_id": "77977569400" } %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `field_label` | Text | Search input label text | | | `placeholder` | Text | Placeholder text for the input field. | `"Search"` | | `include_search_button` | Boolean | Include a search button. | `false` | | `content_types` | Object | Which content types to include in search results. Object contains the following keys with boolean values:<ul><li>`website_pages`</li><li>`landing_pages`</li><li>`blog_posts`</li><li>`knowledge_articles`</li></ul> | `{ "website_pages": true, "landing_pages": false, "blog_posts": true, "knowledge_articles": false }` | | `results` | Object | An object that enables controls for using a custom search results page. Includes the following properties:<br /><ul><li>`use_custom_search_results_template` **(boolean):** when set to `true`, users can select a custom search results page. Default is `false`.</li><li>`path_id` **(string):** the ID of the page that will be used for search results. The referenced page must contain the search results module.</li></ul> | | ## Search results Supported in pages, blog posts, and blog listings. ```hubl {% module "search_results" path="@hubspot/search_results" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `display_featured_images` | Boolean | Display featured images for items. | `false` | ## Section header Supported in pages, blog listings, and blog posts. ```hubl {% module "section_header" path="@hubspot/section_header" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `header` | Text | Section header content. | `"A clear and bold header"` | | `heading_level` | Choice | Heading level for the `header`. Choices include `h1` through `h6`. | `h1` | | `subheader` | Text | Subheading paragraph text for the section. | `"A more subdued subheader"` | ## Simple menu Supported in pages, blog listings, and blog posts. ```hubl {% module "simple_menu" path="@hubspot/simple_menu" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `menu_tree` | Simple Menu | Simple menu object. | `[]` | | `orientation` | Choice | Orientation of the menu. Choices include:<ul><li>`horizontal`</li><li>`vertical`</li></ul> | `horizontal` | ## Social follow Add links to your social media profiles, with drag and drop reordering in the content editor. Automatically inserts icons based on the social URL, but icons can be overridden. Supported in pages, blog listings, and blog posts. ```hubl {% module "social_follow" path="@hubspot/social_follow" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `link` | Text | The destination link for the social page added in the editor. | | | `open_in_new_window` | Boolean | When set to `false`, link will open in the same browser tab. | `true` | | `is_podcast` | Boolean | Set to `true` to display a podcast icon instead of a default or custom icon. | `false` | | `customize_alt_text` | Boolean | By default, alt-text is automatically generated. When set to `true`, you can override the default alt-text with a custom value using the `aria_label` field. | `false` | | `aria-label` | String | When `customize_alt_text` is `true`, this field sets the icon alt-text (e.g., `Follow us on Facebook`). This is used by screen readers to describe the icon to users who are visually impaired. | `False` | | `customize_icon` | Boolean | By default, icons are auto-populated based on the link URL. When set to `true`, you can use the `custom_icon` field to select one of the other provided icons (Font Awesome 6.4.2). | `false` | | `custom_icon` | Icon | When `customize_icon` is `true` and `is_podcast` is `false`, use this field to specify a custom icon from the provided set. Available icons are from Font Awesome 6.4.2. | | ## Social sharing Supported in pages, blog listings, and blog posts. ```hubl {% module "social_sharing" path="@hubspot/social_sharing" %} ``` Note: The variable `social_link_url` in the default column below is the same value as the `link` parameter. | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `link` | Text | This is the destination link that will be shortened for easier sharing on social networks. | | | `facebook` | Object | Object containing:<ul><li>`enabled`: boolean to enable social item</li><li>`custom_link_format`: custom URL for socials sharer URL</li></ul> | `{ "enabled": false, "custom_link_format": "http://www.facebook.com/share.php?u={{ social_link_url }}" }` | | `twitter` | Object | Object containing:<ul><li>`enabled`: boolean to enable social item</li><li>`custom_link_format`: custom URL for socials sharer URL</li></ul> | <code>\{ "enabled": false, "custom_link_format": "https://twitter.com/intent/tweet?original_referer=\{\{ social_link_url }}&url=\{\{ social_link_url }}&source=tweetbutton&text=\{\{ social_page_title\|urlencode }}" }</code> | | `linkedin` | Object | Object containing:<ul><li>`enabled`: boolean to enable social item</li><li>`custom_link_format`: custom URL for socials sharer URL</li></ul> | `{ "enabled": false, "custom_link_format": "http://www.linkedin.com/shareArticle?mini=true&url={{ social_link_url }}" }` | | `pinterest` | Object | Object containing:<ul><li>`enabled`: boolean to enable social item.</li><li>`custom_link_format`: custom URL for socials sharer URL.</li><li>`pinterest_media`: image object including:<ul><li>`src`: image URL.</li><li>`alt`: alt-text for the image.</li></ul></li></ul> | `{ "enabled": false, "custom_link_format": "http://pinterest.com/pin/create/button/?url={{ social_link_url }}&media={{ pinterest_media }}", "pinterest_media": { "src": "", "alt": null } }` | | `email` | Object | Object containing:<ul><li>`enabled`: boolean to enable social item</li><li>`custom_link_format`: custom URL for socials sharer URL</li></ul> | `{ "enabled": false, "custom_link_format": "mailto:?subject=Check out {{ social_link_url }} &body=Check out {{ social_link_url }}" }` | ## Tabbed card Supported in pages, blog listings, and blog posts. ```hubl {% module path="@hubspot/tabbed_card", tabs=[ { "content" : "<p>This is the descriptive text contained in tab 1.</p>", "tab_label" : "Tab 1 label" }, { "content" : "<p>This is the descriptive text contained in tab 2.</p>", "tab_label" : "Tab 2 label" } ], fixed_height=false %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `tabs` | Field group | A field group that contains the tab label and text content of each tab. Contains the following fields:<ul><li>`tab_label`: the label of the tab.</li><li>`content`: the tab's rich text content.</li></ul> | `"Some additional information in one line"` | | `fixed_height` | Boolean | By default, the tabs will be set at a fixed height so that the container height stays the same when switching between tabs. You can set this to `false` if you want the tabbed card container height to be based on the active tab's content. | `False` | ## One line of text Supported in pages, blog listings, and blog posts. ```hubl {% module "text" path="@hubspot/text" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `value` | Text | Add your text to this parameter. | `"Some additional information in one line"` | ## Video Supported in pages, blog listings, and blog posts. ```hubl {% module "video" path="@hubspot/video" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `video_type` | Choice | Type of video. Choices include:<ul><li>`embed`: embed code from an external source.</li><li>`hubspot_video`: HubSpot hosted video.</li></ul> | `embed` | | `hubspot_video` | Video Player | HubSpot hosted video. Used when `video_type` equals `hubspot_video`. | | | `embed_field` | Embed | Supports embed types:<ul><li>`oembed`<ul><li>`video`: URL for video.</li></ul></li><li>`html`: embed code for video.</li></ul> | | | `oembed_thumbnail` | Image | Override oembed thumbnail image when `video_type` equals `embed` and `embed_field` equals `oembed`. | | | `style_options` | Object | Object containing `oembed_thumbnail_play_button_color` - Color field. | `{"oembed_thumbnail_play_button_color":"#ffffff"}` | | `placeholder_fields` | Object | Object containing:<ul><li>`placeholder_title`: text field.</li><li>`placeholder_description`: text field.</li></ul> | `{"placeholder_title":"No video selected", "placeholder_description":"Select a video type in the sidebar."}` | ## Video embed (landing page) Supported in pages. ```hubl {% module "video_embed_lp" path="@hubspot/video_embed_lp" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `url` | Text | URL for video. URLs from Vimeo and YouTube are supported. | `"https://www.youtube.com/watch?v=X1Rr5BFO5rg"` | | `is_full_width` | Boolean | Sets the video to full width (`true`) or manually set width and height (`false`). | `true` | | `max_width` | Number | Max width (px) | `800` | | `max_height` | Number | Max height (px) | `450` | ## WhatsApp link Supported in pages, blog posts, and blog listing pages. Requires a [connected WhatsApp channel](https://knowledge.hubspot.com/inbox/connect-whatsapp-to-the-conversations-inbox). ```hubl {% module "whatsapp_link" path="@hubspot/whatsapp_link", label="whatsapp_link" %} ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `whatsapp_number` | URL | Select from the [WhatsApp channels connected to the account](https://knowledge.hubspot.com/inbox/connect-whatsapp-to-the-conversations-inbox). | | | `optin_text` | Choice | The [opt-in and opt-out text](https://knowledge.hubspot.com/inbox/collect-consent-for-whatsapp-conversations#subscribe-a-contact-using-opt-in-keywords-in-a-whatsapp-thread). | | | `whatsapp_display` | Choice | Select whether the button displays text, a WhatsApp icon, or both. Choices include:<ul><li>`text_and_icon`</li><li>`text`</li><li>`icon`</li></ul> | `text_and_icon` | | `icon_position` | Choice | The position of the icon on the button. Choices include `left` and `right`. | `left` | | `button_text` | String | The button's text. | `Chat on WhatsApp` | | `whatsapp_icon_title` | String | The icon text for screen readers. | `WhatsApp Icon` | # Deprecated default modules @ Learn more about HubSpot's deprecated web default modules. $ reference & cms/modules --- # Deprecated default modules Below, learn more about the [HubSpot default web modules](/reference/cms/modules/default-modules) that have been deprecated. These modules can still be cloned in the design manager, and existing instances of these modules will continue to function on live pages, but they no longer appear in the page editor for content creators to use. ## Blog listing As of February 2024, the Blog Listing module was deprecated in favor of the [Blog Posts module](/reference/cms/modules/default-modules#blog-posts). ```hubl {% module "blog_listing" path="@hubspot/blog_listing", label="blog_listing.module" %} ``` | Parameter | Type | Description | | --- | --- | --- | | `layout` | Choice | The content layout for each blog post preview.<ul><li>`basic_grid` (default): aligns posts in a basic grid.</li><li>`single_column`: aligns posts in a single column. with featured images on their own row above the rest of the post content.</li><li>`side_by_side`: aligns posts in a column with featured images aligned horizontally with the post content.</li></ul> | | `columns` | Number | When using the `basic_grid` layout, the number of posts per row. Can be `2`, `3` (default), or `4`. | | `alternate_image` | Boolean | When `layout` is set to `side_by_side` and display_for_each_item is set to `image`, set this field to `true` to align the featured image on the right and left side of the post preview, alternating. By default, this field is set to `false`. | | `full_image` | String | When `layout` is set to `basic_grid` or single_column and `display_for_each_item` is set to `image`, set this field to `true` to make the featured image the background of the post preview. By default, this field is set to `false`. | | `display_for_each_list_item` | Array | The content to include in each blog post preview. Choices include:<ul><li>`image`: the post's featured image.</li><li>`title`: the post's title.</li><li>`author_image`: the post author's image.</li><li>`author_name`: the post author's name.</li><li>`tags`: the post's blog tags.</li><li>`publish_date`: the post's publish date.</li><li>`description`: the post's meta description.</li><li>`button`: the read more button that links to the blog post.</li></ul> | | `button_text` | String | The text that displays on the read more button when `display_for_each_list_item` is set to `button`. By default, text is set to `Read more`. | | `default_text` | Field group | A field group containing default values for various text elements. Below are the default values set for each text element:<ul><li>`blog_post_summary_text`: `"Blog post summary:"`</li><li>`featured_image_text`: `"Featured image:"`</li><li>`read_full_post_text`: `"Read full post:"`</li><li>`picture_of_text`: `"Picture of"`</li><li>`author_alt_text`: `"{{ picture_of_text }} {{ content.blog_author.display_name }}"`</li><li>`read_full_post_aria_label`: `"{{ read_full_post_text }} {{ content.name }}"`</li><li>`full_featured-image_aria_label`: `"{{ featured_image_text }} {{ content.featured_image_alt_text }}"`</li><li>`full_blog_post_summary_text`: `"{{ blog_post_summary_text }} {{ content.name }}"`</li><li>`gated_content_describedby_text`: `"This content is only available to logged in members."`</li></ul> | ## Follow Me On August 7th, 2024, the Follow Me module was deprecated in favor of the [Social Follow module](/reference/cms/modules/default-modules#social-follow). ```hubl {% module path="@hubspot/follow_me", title="Follow me" %} ``` | Parameter | Type | Description | | --- | --- | --- | | `title` | String | The h3 heading displayed at the top of the module. | | `links` | Boolean | By default, clicked links will open in a new window. Set this to `false` to open clicked links in the same browser window. | ## Follow Me - LP On August 7th, 2024, the Follow Me - LP module was deprecated in favor of the [Social Follow module](/reference/cms/modules/default-modules#social-follow). ```hubl {% module "follow_me_lp" path="@hubspot/follow_me_lp" %} ``` | Parameter | Type | Description | | --- | --- | --- | | `social` | Field group | A field group containing details for each social network to include. Includes the following fields:<ul><li>`network`: a choice field for selecting the social network.</li><li>`link`: the link to the profile.</li><li>`network_image`: the social network image to display.</li><li>`supporting_text`: text to display the name of the social network, if `display` is set to `icon_text` or `text_only`.</li></ul> | | `display` | Choice | Display options for showing social networks. Choices include:<ul><li>`icon` (default)</li><li>`icon_text`</li><li>`text_only`</li></ul> | | `scale` | Number | The size of the icon in `px` value (`25` by default). | | `spacing` | Number | Left and right padding for items in `px` value (`5` by default). | | `alignment` | Choice | the alignment of the items on the page. Choices include:<ul><li>`left`</li><li>`center` (default)</li><li>`right`</li></ul> | | `color_scheme` | Choice | Color scheme to use for icons. Choices include:<ul><li>`color` (default)</li><li>`black` (black and white)</li><li>`grey`</li><li>`white`</li><li>`custom`</li></ul> | | `custom_color` | Color | Custom color to use when `color_scheme` is set to `custom` (`#000000` by default). | | `icon_shape` | Choice | Shape of the social icons. Choices include:<ul><li>`circle` (default)</li><li>`square`</li><li>`original`</li></ul> | | `font_style` | Object | Font options for the social network text, when `display` is set to `icon_text` or `text_only`.Contains the following fields:<ul><li>`size`<ul><li>`value`: the size number.</li><li>`units`: the unit of measure.</li></ul></li><li>`color`: text color in hex value.</li><li>`styles`<ul><li>`bold`: a boolean that makes font bold.</li><li>`italic`: a boolean field that makes font italicized.</li><li>`underline`: a boolean field that makes font underlined.</li></ul></li><li>`font`: the name of the font.</li></ul> | ## Gallery This module was deprecated as of [March 2023](https://developers.hubspot.com/changelog/march-2023-rollup#deprecation-of-the-gallery-default-module) in favor of the [Image Grid](/reference/cms/modules/default-modules#image-grid), [Image Slider](/reference/cms/modules/default-modules#image-slider-gallery), and [Tabbed Card](/reference/cms/modules/default-modules#tabbed-card) modules. This also applies to the Growth theme's gallery module (`@hubspot/growth/modules/image-gallery.module`). The same replacement modules listed above are recommended. ```hubl {% module "gallery" path="@hubspot/gallery", label="gallery.module" %} ``` | Parameter | Type | Description | | --- | --- | --- | | `slides` | Field group | A field group containing fields for each slide. Contains the following fields:<ul><li>`img`: the image URL.</li><li>`show_caption`: a boolean that sets whether to display the slide caption. Set to `false` by default.</li><li>`caption`: the slide caption.</li><li>`link_url`: the URL the slide leads to when clicked.</li><li>`open_in_new_tab`: a boolean that sets whether to open the link in a new tab. Set to `false` by default.</li></ul> | | `display_mode` | Choice | Display mode of the gallery. Choices include:<ul><li>`standard` (default)</li><li>`thumbnail`</li><li>`lightbox`</li></ul> | | `lightboxRows` | Number | Number of rows in the gallery when `display_mode` is set to `lightbox`. | | `loop_slides` | Boolean | Enables looping through the slides with next/prev when `display_mode` equals `standard` or `thumbnail`. | | `auto_advance` | Boolean | Automatically advances to the next slide when `display_mode` equals `standard` or `thumbnail`. | | `num_seconds` | Number | Amount of time in seconds between advancing to the next slide when `display_mode` equals `standard` or `thumbnail`. | | `show_pagination` | Boolean | Show navigation buttons when `display_mode` is set to `standard` or `thumbnail`. | | `sizing` | Choice | Sets the height of the slides when `display_mode` is set to `standard` or `thumbnail`. Choices include:<ul><li>`static`: fixed height (default).</li><li>`resize`: variable height.</li></ul> | | `transition` | Choice | Slide transition styles when `display_mode` is set to `standard` or `thumbnail`. Choices include:<ul><li>`slide`: slide transition (default).</li><li>`fade`: fade transition.</li></ul> | | `caption_position` | Choice | Position of the slide captions when `display_mode` is set to `standard` or `thumbnail`. Choices include:<ul><li>`below`: always keep captions below the image (default).</li><li>`superimpose`: superimpose captions on top of images.</li></ul> | # export_to_template_context @ export_to_template_context is a parameter that makes a module's parameters available to the template environment without actually rendering the module. This parameter can be used all HubL module tags. The widget_data tag is used to retrieve these parameters, store them in variables, and/or incorporate them into your template's logic. $ reference & cms/modules --- # export_to_template_context `export_to_template_context` is a parameter that makes a HubL tag's parameters available to the template environment without actually rendering the HubL tag. This parameter can be used with all [HubL tags](/reference/cms/hubl/tags/standard-tags). The `widget_data` dict is used to retrieve these parameters, store them in [variables](/reference/cms/hubl/variables-macros-syntax), and/or incorporate them into your template's logic. By making a HubL tag's parameters available in the template context, without actually rendering the HubL tag, you can allow users to make decisions in the content editor that affect how the template renders. For example, let's say that you want to only render a certain code block when the user gives a value to a field. This becomes possible with this parameter. First you must add `export_to_template_context=True` to the HubL tag. Then you must use a `widget_data.module.parameter_you_want_to_retreive`. ```hubl {% module "job_title" path="@hubspot/text", label="Enter a Job Title", value="Chief Morale Officer", export_to_template_context=True %} {# Makes the parameters available to be stored and used in template logic #} {{ widget_data.job_title.body.value }} {# Prints the value of the HubL tag but can also be used in template logic #} ``` Below are a few applications of this concept. ## Usage within custom modules `export_to_template_context=True` is not supported in custom modules, as it serves no real purpose for them. You do not need to use `export_to_template_context` to get the value of a module within a template, [you can already access it](#retrieving-parameters-from-modules-already-rendered-on-the-template). If you need to visually hide the module's output you could build the module to not output anything, or include a boolean field that enables or disables whether the module renders anything. ## User selectable background images In this example, an image HubL tag is created but then exported to the context of the template rather than rendered. The `src` parameter is retrieved with the `widget_data` tag and rendered as the source of a background image in a style tag. ```hubl {% module "background_image" path="@hubspot/image" label='Select a background image', src='http://cdn2.hubspot.net/hub/428357/file-2117441560-jpg/img/dev-bg-compressed.jpg', export_to_template_context=True %} <!--Sample markup --> ``` While this is possible to do in coded templates, generally it is better to build a custom module to give users in the page editor the best experience. HubL tags like this show up as individual fields, whereas you may have multiple related fields. Using a custom module all of its fields display grouped in the page editor. ## Choice field to render pre-defined markup The following example uses the `export_to_template_context` parameter in conjunction with a [choice module](/reference/cms/hubl/tags/standard-tags#choice) to change a banner message on a careers page. The user selects a department via the UI and the heading changes without the user having to actually edit content. ```hubl {% module "department" path="@hubspot/choice", label="Choose department", value="Marketing", choices="Marketing, Sales, Dev, Services" export_to_template_context=True %} {% if widget_data.department.value == "Marketing" %} <h3>Want to join our amazing Marketing team?!</h3> <h4>We have exciting career opportunities on the {{ widget_data.department.value }} team.</h4> {% elif widget_data.department.value == "Sales" %} <h3>Are you a Sales superstar?</h3> <h4>We have exciting career opportunities on the {{ widget_data.department.value }} team.</h4> {% elif widget_data.department.value == "Dev" %} <h3>Do you love to ship code?</h3> <h4>We have exciting career opportunities on the {{ widget_data.department.value }} team.</h4> {% else %} <h3>Want to work with our awesome customers?</h3> <h4>We have exciting career opportunities on the {{ widget_data.department.value }} team.</h4> {% endif %} ``` This same functionality can actually be reproduced using a choice field inside of a custom module. The custom module UI actually makes choice options with both a value and a label pretty easy. ## Retrieving parameters from modules already rendered on the template If you want to retrieve a parameter from a module or tag that is already rendering on a page, the module can be accessed within a dict named `widgets`. The `export_to_template_context` parameter is not required. The syntax is as follows: ```hubl // HubL {{ content.widgets.name_of_module.body.parameter }} {{ content.widgets.my_text.body.value }} ``` The above method does not support retrieving values from fields in global modules, as `content.widgets` won't access global modules. Because different fields store data in different formats it is often handy to make use of [developer Info](/guides/cms/debugging/troubleshooting#developer-info) to see how you access the specific data you want to display. ## Printing HubL module info on blog listing While blog templates are generally used for blogs, they can also be repurposed to create various other types of listings. You can use the techniques described above to achieve this. For example, you may want to create a listing layout of press that your company has received, but rather than linking to posts, you want the listing to link to another page. You can see this concept in action at [academy.hubspot.com/projects](https://academy.hubspot.com/projects). The projects listing page is a blog listing template, but each post links to a regular HubSpot page. The content creator specifies the destination link in the editor. Within the head of the individual blog post's code, you would define a text field. If you don't want the text to render on the post, you would use `export_to_template_context`. ```hubl {% module "custom_blog_link" path="@hubspot/text", label="Link to external press item", export_to_template_context=True %} ``` This text field is editable in each blog post. Next, we would need to define a link in our listing. But because the widget_data only exists in the context of the post, we need to use different syntax to fetch the widget data to populate the link. In this case, we would use `content.widgets.custom_blog_link.body.value`. While the `widget_data` is not available to the blog listing, the value of that field is still stored within the context of the individual content's widgets. A basic blog listing loop that renders this custom link with each post is shown below. If using this technique, you would want to ensure that you add the subdirectory automatically created for each blog post to your robots.txt file to prevent those empty posts from being crawled by Google and other crawlers. ```hubl {% for content in contents %} <a href="{{ content.widgets.custom_blog_link.body.value }}"> Click here to see this press feature! </a> {% endfor %} ``` # Coding Custom Modules @ Custom modules are reusable components you can use across your website. Build them following best practices for speed, user experience and accessibility. $ reference & cms/modules --- # Module files When building a module for pages, blogs, and [quotes](/guides/cms/content/templates/types/quotes), the module will contain three front-end related files that control the content, styling, and functionality of the module: - module.html - module.css - module.js Email modules don't support module.css and module.js. This is because email clients don't support JavaScript and support for linked CSS files is limited. These files will always be rendered to the page when an instance of the module is on the page. When a page includes multiple instances of the same module, HubSpot will only load `module.css` and `module.js` from that module once. By default, `module.css` and `module.js` do not load asynchronously, but you can change this by including [css_render_options and js_render_options](/reference/cms/modules/configuration) in the module’s meta.json. Modules can be built within the design manager or locally using [the HubSpot CLI](/guides/cms/tools/local-development-cli). In the design manager, module files are displayed in a multi-pane editor. When viewing a module locally, the files are contained within module-name.module folders. for recommendations. ## HTML + HubL (module.html) The module.html file is intended for HTML and HubL. In general, wherever a module is placed in the page editor or template file determines where the contents of the module.html file are rendered. This file acts like a [HubL include](/reference/cms/hubl/overview#including-files-in-files) in the page wherever the module is placed. The module.html file [can access the module's field values through HubL](/guides/cms/content/modules/overview#using-module-field-data-to-render-html). ## CSS (module.css) Use the `module.css` file to add CSS to a module. In general, `module.css` supports a very limited subset of HubL. However, you can use [`module_asset_url("my-image.png")`](/reference/cms/hubl/functions#module-asset-url) for images added as module linked assets. This enables linking assets such as images, packaged with the module itself. For example: ```css .testimonial-module__wrapper { background: url('{{ module_asset_url('bg-pattern.png') }}'); background-repeat: repeat; min-height: 200px; width: 100%; display: block; } ``` Below, learn how to set up a module's CSS to change dynamically based on the module's fields. ### Styling based on module field values There are a few ways you can influence the styling of your module based on the module’s fields. Choose the way that works best for your specific use case. - [CSS Classes](#css-classes) - [Require_css block](#require-css-block) - [Inline styles](#inline-styles) #### CSS Classes To set up predefined styling for the module with the option for editors to select from those options, you can add a module field to set classes in your `module.htm`l file which correspond to CSS classes in your `module.css` file. For example, you may have an image and text module. You want content creators to be able to position the image to the right or left of the text based on a choice field. To do this, you could set your `module.html` and `module.css` files as follows: ```hubl <!-- module.html --> <section class="img-text__wrapper img-text--{{ module.positioning }}" aria-label="{{ module.heading }}"> {# module.position is a choice field with two values "img-left" or "img-right". This dictates the order they appear on desktop. Controlled by CSS #} </section> ``` ```css /* module.css */ /* CSS that makes the image show adjacent to the text, and positioned based on the positioning field.*/ /* The media query ensures that on mobile the image will always appear above the text for visual consistency. */ @media (min-width: 768px) { .img-text__wrapper { display: flex; align-items: row; } .img-text__img, .img-text__text { flex: 1; padding: 10px; } .img-text--img-right { flex-direction: row-reverse; } } ``` ### require_css block When you need to give content creators direct control over specific properties and when classes are not ideal, style tags with `require_css` blocks are the best option. To give content creators direct control over specific properties without using classes, you can instead add styling to the `module.html` file within `require_css` tags. For example: ```hubl {% require_css %} <style> img { border-width:{{ module.border_width }}px; border-color:rgba({{ module.border_color.color|convert_rgb}},{{ module.border_color.opacity/100 }}); border-style: solid; } </style> {% end_require_css %} ``` Because `module.html` can render HubL, you can use module field values as CSS variables. When a content creator updates the field in the page editor, the CSS will update to match. These block move the `<style>` tags into the `<head>` of your page within the `standard_header_includes` statement. You can also set the CSS to be scoped to only the module instance by wrapping the CSS with `scope_css` tags. For example, you could update the above module code as follows: ```hubl {% require_css %} <style> {% scope_css %} img { border-width:{{ module.border_width }}px; border-color:rgba({{ module.border_color.color|convert_rgb}},{{ module.border_color.opacity/100 }}); border-style: solid; } {% end_scope_css %} </style> {% end_require_css %} ``` ### Add inline styles When you need to give content creators granular control over only a few properties and when classes are not ideal, you can directly add the values to a style attribute in the HTML. ```hubl {# Module.html #} ``` If you have many properties and the code becomes hard to read, consider switching to the `require_css` block method. ### Import specific CSS files `require_css` is a HubL function that you can add to module.html which tells HubSpot that a particular module or template requires a particular CSS file to display. A link tag pointing to the css file is added to the page's `<head>` inside of the `standard_header_includes`. The `require_css` function will only load that CSS file once, regardless of how many times that same file is required by modules and templates on a particular page. This makes it great for situations where styles may be shared across multiple modules, but where adding the CSS directly to the main stylesheets used on every page for your site may not make sense. `require_css` and linked CSS files fill the same purpose, but `require_css` can be used conditionally based on field values. This prevents loading unnecessary code. ```hubl <!-- module.html --> {{ require_css(get_asset_url("/modules/shared_layout_styles.css")) }} ``` ## JavaScript (module.js) Use the `module.js` file to add JavaScript to a module. Like the `module.css` file, the `module.js` file does not support HubL. ### Scripting based on field values There are a few ways you can build modules, where the JavaScript acts differently based on field values. Understanding which method to use and when can mean performance benefits on every page the module is used. For example, you have a custom image module, you want to give content creators the ability to make it so the image can open in a lightbox. Content creators only want that for specific images, and not all instances of the module. ### Data attributes Data attributes are HTML 5 standard custom attributes that developers add to elements. Just as all elements support `class="yourClassName"`, all elements support `data-your-attribute="yourValue"`. ```hubl <!-- module.html--> ``` You can use data attributes to pass the field values of your module instances to be handled by your module.js file. To use the values in your module.js file, you will need to loop through all of the instances of your module. Adding a module-specific class name to the outermost wrapper element of your module will give you a target to use, so that you can loop through each of your module instances. ```js // module.js let imgModules = document.getElementsByClassName('img-module'); Array.from(imgModules).forEach(function (element) { // loop through each of the instances of the module // set data attributes to variables to make it easy to work with let isLightboxEnabled = element.dataset.lightbox; let captionStyle = element.dataset.caption; if (isLightboxEnabled) { element.addEventListener('click', function () { showLightbox(captionStyle); // Execute your code for the action you want to take, you can pass your data attributes into functions from libraries. }); } }); ``` The data attributes will allow you to retrieve the field values for each module instance in your module.js. ### require_js block In advanced situations, perhaps when using a JavaScript templating library or a reactive framework like Vue.js or React.js, you may prefer outputting just the data, while the framework handles rendering. In this case, use a script tag surrounded by a [`require_js`](/reference/cms/hubl/functions#require-js) block to provide variables you can access from your templating script. ```hubl {% require_js %} <script> let myArray = [ {%- for item in module.repeating_text_field -%}"{{ item }}",{%- endfor -%} ]; </script> {% end_require_js %} ``` This technique can be useful for supplying advanced applications with an initial set of data from which to render. This eliminates an initial JavaScript call to retrieve data. ### require_js [`require_js`](/reference/cms/hubl/functions#require-js) is a HubL function that tells HubSpot that a particular module or template requires a particular JavaScript file to load properly. The function takes two parameters: the path to the file and the location the file is to be added to ("head" or "footer"). In a module `require_js` can only be added to the module.html. The JavaScript file referred to in the `require_js` statement will only be loaded once per page, regardless of how many times it is required by modules and templates within the page. This reduces the number of HTTP requests and prevents duplicate code. Some situations where this becomes handy: - If you have multiple modules or templates that require the same JavaScript, you can use `require_js` to share that script across modules. - If you're working with a JavaScript bundler like webpack, it can be easier to output your js files to one specific location. Using `require_js`, you can associate the JavaScript with your module. `require_js` and linked javascript files serve the same purpose, but `require_js` can be done conditionally based on field values. This prevents unnecessary code from being loaded. You also have the additional option of loading JavaScript in the head, should you need that. Since JavaScript is render-blocking , the default location [`require_js`](/reference/cms/hubl/functions#require-js) places JavaScript is the "footer". [Learn more about optimizing for performance.](/guides/cms/content/performance/speed) ## Related Information - [Optimize your CMS Hub site for speed](/guides/cms/content/performance/speed) - [Modules](/guides/cms/content/modules/overview) - [Module fields](/guides/cms/content/fields/overview) # Using modules in templates @ Modules are editable areas of HubSpot pages or emails. While you can add modules to a template layout using the drag and drop Template Builder, you can also create modules using HubL. HubL modules can be defined in a coded template file, a HubL template module, or a blog content module. Below is a comprehensive list of modules and their parameters. $ reference & cms/modules --- # Using Modules in Templates Modules can either be added directly to a template or added to individual pages with [drag and drop areas](/guides/cms/content/templates/drag-and-drop/overview) and flexible columns. When a module is added to a template, the module will appear in that location by default. Modules in drag and drop areas and flexible columns can be moved and removed, and other modules can be added around them. These are module instances. After a module has been defined, you can get its field values at the template level through the [content.widgets dict](/reference/cms/modules/export-to-template-context#retrieving-parameters-from-modules-already-rendered-on-the-template). ## Basic module syntax HubL module tags are delimited by `{% %}` , and must specify `module`, a unique name, and the module's design manager path. A module can also include parameters for additional settings. - **Module name:** gives the module a unique identity in the context of the template. - The name must be in quotes following the type of module, and must use underscores instead of spaces or dashes. - This name is used to match the content set within the page/email editor with the corresponding HubL module tag. For example, if you code a HubL module tag with the same name in two different areas of a template, users will only have one module to edit in the editor, but changes to that module will apply in both locations. - **Path:** depending on the tag, defines the location of where the module is in the design manager. - `/` means the root of the current drive; - `./` means the current directory; - `../` means the parent of the current directory. The path for [HubSpot default modules](/reference/cms/modules/default-modules) always start with `@hubspot/` followed by the type of module. - **Parameters:** additional settings for the module instance, specifying its behavior and how it renders. Includes template-level default values for module fields. - Parameters are comma-separated key-value pairs. - Parameters have different types, including: string, boolean, integer, enumeration, and JSON list object. String parameters values must be in single or double quotes, while boolean parameters do not require quotes around their `True` or `False` values. Learn more about the [parameters that are available for all modules](/reference/cms/modules/using-modules-in-templates#parameters-available-for-all-modules). - Note that there is no in-editor validation for field values compared to the module's field settings. For example, if module has a number field that has a minimum value set to `1`, and you pass into the parameter a `0`, you will not see a warning that the value is invalid. ```hubl {# Basic syntax #} {% module "unique_module_name" path="@hubspot/module_type", parameterString='String parameter value', parameterBoolean=True %} {# Sample of a default HubSpot text module #} {% module "job_title" path="@hubspot/text", label="Job Title", value="Chief Morale Officer" %} {# Sample of a custom module #} {% module "faqs" path="/myWebsite/modules/faq_module", label="Custom FAQ Module" faq_group_title="Commonly Asked Questions" %} ``` ## Passing dicts to module parameters For modules with fields that expect dicts, you can pass them like you would other parameters. If it's cleaner to you or you plan to re-use the values, you can set the dict to a variable, and pass the variable to the parameter instead. ```hubl {% module "social_buttons", path="@hubspot/social_sharing", email={ "default": true, "enabled": false, "img_src": "https://..." } %} ``` ### Passing fields that have dnd associated parameters [Drag and drop tags](/reference/cms/hubl/tags/dnd-areas), such as `dnd_area`, come with a set of default parameters, such as `width`. While the design manager will prevent you from creating new fields that use one of these reserved parameters, modules created before drag and drop tags were introduced may already use a reserved parameter. To fix this, you can use the `fields` parameter. Just like you would pass field data to a group, you can pass the field name as a key on the `fields` object. Its value must be consistent with the format the field type expects. ```hubl {% dnd_area "main_content"%} {% dnd_section %} {% dnd_column %} {% dnd_row %} {% dnd_module path="@hubspot/divider", fields={width: "90"} %} {% end_dnd_module %} {% end_dnd_row %} {%end_dnd_column%} {% end_dnd_section %} {% end_dnd_area %} ``` ## Setting template-level default values for fields You can set default values for module fields at the template level by including parameters in the `dnd_module` tags. Below, learn how to set default field values in nested field groups, repeating fields, repeating field groups, and style fields. ### Setting default values for nested field groups Below is an example of a custom drag and drop module with a custom `style` field group containing other nested field groups. Compare its template-level configuration with how this same grouping would appear in the design manager. ```hubl {% dnd_module path="/Nested_Module.module" style={ "group":{ "section":{ "color_field":{ "color" : "#000", "opacity" : 100 } } } } %} {% end_dnd_module %} ``` ### Setting default values for repeating fields You can set template level default values for repeating fields by passing an array to the field's parameter. The array's items must be in the format expected based on the [field type](#field-based-parameters). For example: - A simple text field only expects a string - An image repeater field expects an image field object. This applies to [all of the other field types](#field-based-parameters). ```hubl {% module path='../modules/recipe_card.module', ingredients=["Eggs","Ham","Cheese"] %} {% module "my_repeater_module" path="/img_repeater_module", label="img_repeater_module", image=[ { "src" : "https://cdn2.hubspot.net/hubfs/428357/Developer%20Site/assets/logo/Developers-LOGO.svg", "alt" : "HubSpot Developers", "width" : 254, "height" : 31 },{ "src" : "https://www.hubspot.com/hs-fs/hub/53/file-733888614-jpg/assets/hubspot.com/about/management/dharmesh-home.jpg", "alt" : "Dharmesh", "width" : 394, "height" : 394 } ] %} ``` ### Setting default values for repeating field groups Modules that contain repeating groups of fields - like you might see in a slideshow module or FAQ module - can have a template level default set for those groups. To do this you pass an array of objects to your field group's parameter. The key and value pairs of the object are the field names and their values. ```hubl {% module path='../modules/slideshow.module', slides=[ { "caption":"Cute dog looking up", "image_url":"http://example.com/image.jpg", }, { "caption":"Cuter cat not looking amused", "image_url":"http://example.com/image2.jpg", } ] %} ``` ### Setting default values for style fields you can explicitly set default values for [style fields](/guides/cms/content/fields/overview#style-fields) using the `styles` parameter. This works just like other groups do, where the parameter is the name of the group. You pass an object to that parameter with all of the fields you wish to set. ```hubl {% dnd_module path="./path/to/module", styles={ "background_color":{ "color":"#123", "opacity":50 } } %} ``` ## Block Syntax While most modules have parameters that control default content, there may be situations where you need to add large code blocks to the default content of a module. For example, you may want to include a large block of HTML as the default content for a rich text or HTML module. Rather than trying to write that code into a value parameter, you can use HubL block syntax. ```hubl {% module_block module "my_rich_text_module" path="/My Rich Text Field Module", label="My Rich Text Field Module" %} {% module_attribute "rich_text_field_variable" %} {% end_module_attribute %} {% end_module_block %} ``` Prior to the `module_block` syntax, `widget_block` was used. It follows the same pattern but the opening tags were `widget_block`, and `widget_attribute`. Closing tags were `end_widget_attribute`, `end_widget_block`. The `widget_block` syntax is deprecated but you don't need to update old code. The parameter that immediately follows `module_block` or `widget_block`(deprecated) is the `type_of_module` parameter. In nearly all of our documentation you will find we use `module`. [V2 HubSpot Modules](/reference/cms/hubl/tags/standard-tags) are normal modules, like what you can create. Therefore there's no longer a need to use a different `type_of_module`. While `widget_block` is deprecated, and you should use `module_block`. If inheriting a website from another developer it may contain old code using `widget_block` and `type_of_module`. The `type_of_module` supports V1 HubSpot module names for example: `rich_text` or `raw_html`. Additional parameters can be added to the first line of HubL. The second line defines which parameter the contents of the block will be applied to. For example, for a `rich_text` module this should be the html parameter. For a `raw_html` module, this would be the value parameter (see both examples below). ```hubl {# widget_block is deprecated, use module_block instead. This example is only to explain what type_of_module was used for, for those with legacy code. #} {% widget_block rich_text "my_rich_text_module" overrideable=True, label='My rich-text module' %} {% widget_attribute "html" %} <h2>New Module</h2> <p>Add content here.</p> {% end_widget_attribute %} {% end_widget_block %} ``` ```html <span id="hs_cos_wrapper_my_rich_text_module" class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_rich_text" style="" data-hs-cos-general-type="widget" data-hs-cos-type="rich_text" > <h2>New Module</h2> <p>Add content here.</p> </span> ``` ## content_attribute In addition to regular and block syntax, there are certain instances where you may want to specify a large block default content for a predefined content variable. The most common example of this proves to be the [content.email_body](/reference/cms/hubl/variables#email-variables) variable. This variable prints a standard email body that can be altered in the content editor. Since this isn't a standard HubL module, we use a **content_attribute** tag to specify a block of default content. The example below shows the email body variable populated with a default content code block. ```hubl {% content_attribute "email_body" %} <p>Hi {{ contact.firstname }},</p> <p>Describe what you have to offer the customer. Why should they read? What did you promise them in the subject line? Tell them something cool. Make them laugh. Make them cry. Well, maybe don't do that...</p> <p>Use a list to:</p> <ul> <li>Explain the value of your offer</li> <li>Remind the reader what they’ll get out of taking action</li> <li>Show off your skill with bullet points</li> <li>Make your content easy to scan</li> </ul> <p><a href="http://hubspot.com">LINK TO A LANDING PAGE ON YOUR SITE</a> (This is the really important part.)</p> <p>Now wrap it all up with a pithy little reminder of how much you love them.</p> <p>Aw. You silver-tongued devil, you.</p> <p>Sincerely,</p> <p>Your name</p> {% end_content_attribute %} ``` ## Parameters available for all modules While some modules have certain [special parameters](/reference/cms/hubl/tags/standard-tags), below is a list of parameters supported by all modules. | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `label` | String | The name of the module displayed in the content editor. This parameter can also be used to give users additional instructions. | | | `overrideable` | Boolean | Controls whether or not the module can be edited in the content editor, equivalent to the _Prevent editing in content editors_ setting in the design manager. | `True` | | `no_wrapper` | Boolean | When set to `True`, removes the wrapping markup from around the content of a module.On pages, modules are always wrapped in a `<div>` with special classes. This wrapping markup makes it so when you click the module in the preview pane, the editor scrolls to that module. There may be instances where you want to remove the wrapper, such as if you want to use a text module to populate the destination of an anchor tag `href` attribute. | `False` | | `extra_classes` | String | Adds classes to the module wrapper. You can add multiple classes by separating the classes with spaces. For example:`extra_classes='full-width panel'` | | | `export_to_template_context` | Boolean | When set to `True`, instead of rendering the HTML, the parameters from this widget will be available in the template context. [Learn how to use this parameter and the widget_data tag.](/reference/cms/modules/export-to-template-context) | `False` | | `unique_in_loop` | Boolean | When the module is defined within a loop, appends the module name with the loop.index. When set to `True`, a different version of the module will print within each iteration of the loop. Appends the module name with the loop.index. | `False` | To see a complete list of all module types and their parameters, [click here.](/reference/cms/hubl/tags/standard-tags) ## Field-based parameters Below, learn about the field-based module parameters you can use. | Field | Type | Example | Keys | | --- | --- | --- | --- | | Blog | Integer (blog ID) | `1234567890` | | | Boolean | True/false | `false` | | | Choice | String | `"option_1"` | | | Color | Object | `{ "color" : "#ffffff", "opacity" : 100}` | color <br />6 character hexidecimal format <br />opacity <br />integer 0 - 100 <br /> | | CTA | String (CTA ID) | `"fb9c0055-6beb-489d-8dda-3e1222458750"` | | | Date | Timestamp | `1566360000000` | | | Datetime | Timestamp | `1566360000000` | | | Email address | Array (email address strings) | `["develop@hubspot.com", "design@hubspot.com"]` | | | File | String (URL of file) | `"https://cdn2.hubspot.net/hubfs/file.pdf"` | | | Follow Up Email | Integer (follow up email ID) | `1234567890` | | | Font | Object | `{ "size" : 12, "size_unit" : "px", "color" : "#000", "styles" :{ "text-decoration" : "underline" }, "font" : "Alegreya", "fallback" : "serif", "variant" : "regular", "font_set" : "GOOGLE"}` | size <br />font size without unit type <br />size_unit <br />font size unit string<ul><li>"px"</li><li>"pt"</li><li>"em"</li><li>"rem"</li><li>"%"</li><li>"ex"</li><li>"ch"</li></ul> <br />color <br />hex color code string <br />styles <br />supported properties"font-weight" <br />"normal" / "bold" <br />"font-style" <br />"normal" / "italic" <br />"font-style" <br />"none" / "underline" <br /> <br /> | | Form | Object | `{ "form_id" : "9aa2e5f3-a46d-4774-897e-0bc37478521c", "response_type" : "redirect", "redirect_url" : "http://www.hubspot.com", "redirect_id" : null, "form_type" : "HUBSPOT"}` | form_id <br />The form's ID. [How to get a form's id.](https://knowledge.hubspot.com/forms/find-your-form-guid) <br />response_type <br />"redirect" / "inline" <br />message <br />Message displayed if using response_type "inline". String supporting html. <br />redirect_url <br />String, absolute URL to a webpage <br />redirect_id <br />Page/Post id to redirect to <br />form_type <br />"HUBSPOT" / "TICKET_FORM" <br /> | | HubDB Table | Integer (HubDB table ID) | `123456789` | | | Icon | Object | `{ "name" : "align-center", "unicode" : "f037", "type" : "SOLID"}` | name <br />The icon's name <br />unicode <br />The unicode symbol for the font the icon is from <br />type <br />Symbol style. "SOLID" / "REGULAR" <br />It is recommended you set an icon field and view the values that way, to set the parameters properly. | | Image | Object | `{ "src" : "https://cdn2.hubspot.net/hubfs/image.jpeg", "alt" : "an_image", "width" : 100, "height" : 100}` | src <br />Image URL <br />alt <br />Image alt text, used by screen readers and search engines <br />width <br />The width at which the image is to be displayed <br />height <br />The height at which the image is to be displayed <br /> | | Link | Object | `{ "url" : { "type" : "EXTERNAL", "href" : "www.hubspot.com", "content_id" : null }, "open_in_new_tab" : false, "no_follow" : false }` | url <br />object storing URL data.type <br /><ul><li>"EXTERNAL" for non HubSpot non-email URLs</li><li>"CONTENT" for pages, blog posts, and landing pages</li><li>"FILE" for files uploaded to the file manager</li><li>"EMAIL_ADDRESS" for email addresses</li><li>"BLOG" for blog listing pages</li></ul> <br />href <br />The URL you are linking to. <br /> <br />open_in_new_tab <br />"true"/"false", determines if `target="_blank"` should be added <br />no_follow <br />"true"/"false", determines if `rel="nofollow"` should be used <br /> | | Logo | Object | `{ "override_inherited_src" : true, "src" : "https://cdn2.hubspot.net/hubfs/logo.png", "alt" : "best_logo_ever", "width" : 100, "height" : 100}` | override_inherited_src <br />true/false overide the portal defaults <br />src <br />Image URL <br />alt <br />Alt text, used for screen readers and search engines. <br />width <br />width the image is to be displayed at <br />height <br />height the image is to be displayed at <br /> | | Meeting | String (meeting link) | `"https://app.hubspot.com/meetings/developers-r-kewl"` | | | Menu | Integer (menu ID) | `123456789` | | | Number | Integer | `1` | | | Page | Integer (page ID) | `1234567890` | | | richtext | String (can contain HTML) | `"# Hello, world!"` | | | Salesforce Campaign | String (Salesforce campaign ID) | `"7016A0000005S0tQAE"` | | | Simple Menu | Array of menu item objects | `[ { "isPublished" : true, "pageLinkId" : 123456789, "pageLinkName" : "My page", "isDeleted" : false, "categoryId" : 1, "subCategory" : "site_page", "contentType" : "site_page", "state" : "PUBLISHED_OR_SCHEDULED", "linkLabel" : "This is a page", "linkUrl" : null, "linkParams" : null, "linkTarget" : null, "type" : "PAGE_LINK", "children" : [ ] } ]` | isPublished <br />true/false is the menu item's page published? <br />pageLinkId <br />Page id in the CMS <br />pageLinkName <br />The page's actual name in the CMS <br />isDeleted <br />true/false <br />categoryId <br /><ul><li>1 - Site Page</li><li>3 - Blog post</li></ul> <br />subCategory <br /><ul><li>site_page</li><li>landing_page</li><li>blog</li><li>normal_blog_post</li></ul> <br />contentType <br /><ul><li>site_page</li><li>landing_page</li><li>blog</li></ul> <br />state <br /><ul><li>DRAFT</li><li>DRAFT_AB</li><li>PUBLISHED</li><li>PUBLISHED_OR_SCHEDULED</li><li>PUBLISHED_AB</li><li>SCHEDULED</li></ul> <br />linkLabel <br />text the user reads and clicks <br />linkUrl <br />actual URL the user is sent to upon clicking <br />linkParams <br />\\# links or ? query parameters <br />linkTarget <br />if open in new tab is enabled "\_blank" otherwise "null" <br />type <br /><ul><li>"PAGE_LINK"</li><li>"PAGE_LINK_WITH_PARAMS"</li><li>"NO_LINK"</li></ul> <br />children <br />array of menu item objects, identical to individual menu items. <br /> | | Tag | Integer (tag ID or slug, ID is recommended) | `1234567890` | | | Text | String | `"it's like any other string"` | | | URL | Object | `{ "type" : "CONTENT", "href" : null, "content_id" : 123456789}` | type <br /><ul><li>"EXTERNAL" for non HubSpot non-email URLs</li><li>"CONTENT" for pages, blog posts, and landing pages</li><li>"FILE" for files uploaded to the file manager</li><li>"EMAIL_ADDRESS" for email addresses</li><li>"BLOG" for blog listing pages</li></ul> <br />href <br />String, the URL you are linking to. <br /> | # CMS Reference Overview @ Developer reference docs for the HubSpot Content Management System (CMS). $ reference & cms --- # CMS Reference Overview Use the CMS reference docs to quickly find information you might need when building themes, modules, templates, and more. While the CMS guides are intended to walk you through various features and functionalities, this section is geared towards providing definition lists and boilerplate code that you can use as needed. The information you’ll find in this section assumes that you’re familiar with developing on the CMS. If you’re just getting started, it’s recommended to check out the [CMS quickstart guide](/guides/cms/quickstart/classic-hubl-quickstart). In this section, find reference information for [modules](/reference/cms/modules/configuration), the [HubL](/reference/cms/hubl/overview) templating language, [fields](/reference/cms/fields/module-theme-fields) and [serverless functions](/reference/cms/serverless-functions). # Build health checks @ Learn how HubSpot's CMS React platform uses build health checks in your CMS React project. $ reference & cms/react --- # Build health checks To validate and prevent unexpected production behavior, HuSpot will automatically run a series of health checks at the end of a build for a CMS React project. These health checks are enabled but will not fail a build by default. Build health checks look for the following: - For every React module, HubSpot verifies that: - The built module code can be imported. - There is a `Component` named export and that it is a React component (a function). - There is a `fields` named export and that it is a React element (`<ModuleFields>...</ModuleFields>`) or an array. - There is a `meta` named export and it is a JavaScript object. - For any island import (`?island`), client import (`?client`) or dynamic import (`import(...)`) linked from a React module's code, the code must be able to be imported. Failed health checks are logged in the build output only when strict health checks are enabled. ## ESM and Common JS In addition to verifying React modules, health checks will also uncover problematic [ESM and Common JS issues](https://yuzu.health/blog/cjs-vs-esm) before any code is deployed. HubSpot's CMS React system is built on top of [Vite](https://vitejs.dev/), which can lead to complications when depending on other package that have misconfigured ESM exports. Relatedly, some packages that advertise CJS exports (i.e., `package.json` `export` / `type`) may actually include ESM `import` and `export` syntax (e.g., [`@mui/material@5.15.10`](https://publint.dev/@mui/material@5.15.10)). This can lead to situations where Vite's compiler outputs an import to that package's file in a `node/modules...`, which results in a runtime syntax error when the code is run. However, with build health checks, HubSpot can ensure that the problem is discovered at build-time rather than by visitors at runtime. ## Build health check configuration To explicitly configure your project's build health checks, you can update the `cms-assets.json` file. ```json { "label": "My CMS project", "buildConfig": { "healthchecks": { "enabled": true, "strict": true } } }; ``` # @hubspot/cms-components library @ Learn about the @hubspot/cms-components library $ reference & cms/react --- # @hubspot/cms-components library `@hubspot/cms-components` is a runtime library providing primitives that interact with CMS React features and other HubSpot data from your components. ## Getters ### getHubID `( ) => number` Returns the ID of the HubSpot account for the page that's being rendered. ### getIsDeployed `( ) => boolean` Returns `true` for components rendered live for a deployed project, and `false` when rendered in the dev server. ### getSecret `(secretName: string) => string` Returns a value for a given secret key. The secret must be defined using the `hs secrets` [CLI command](/guides/cms/tools/hubspot-cli/cli-v7#add-a-secret) and the key must be included in a `secretNames` array in your `cms-assets.json` configuration. To prevent accidentally leaking secrets, `getSecret` must be called from a React component function that is rendered on the server. In other words, `getSecret()`: - Cannot be called at the top-level of a module - Cannot be called from inside an island If you want to pass a secret to client-side code, which would make it publicly viewable, you must explicitly pass the secret string via an island prop. Learn more about [using secrets](/guides/cms/react/serverless-functions#secrets). ## Hooks HubSpot provides the following React hooks to help write components that run on both the server and the browser. ### useAfterIslandHydration `( ) => boolean` Will return `true` only after hydration is completed. More specifically it will: - Return `false` during the initial render on the server. - Return `false` during the first render that happens inside the browser. - Return `true` during any subsequent renders that happen after the component has been “mounted” in the browser. This hook is useful because React requires server-rendered HTML to match the initial client render. See the [Server/Client Rendering section](https://github.hubspot.com/cms-react/appendix.html#server-side-client-side-rendering) in the appendix for more information. ### useIsServerRender `( ) => boolean` Returns `true` while the component is being rendered on the server and `false` in the browser. Note that in most cases, it is better to use `useAfterIslandHydration()`, since it makes it easier for your code to “do the right thing” for hydration. ### usePageUrl `( ) => URL` Returns the current page [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL). Works on the server and is reactive to changes to the URL on client. This can be useful when components need to react to URL changes, such as query params, while also supporting server rendering. To programmatically trigger non-navigation URL changes, use `pushHistoryState()` which is identical to `window.history.pushState()` but integrates with `usePageUrl()` to ensure it receives change events. ### useInlineHeadAsset `(renderFunc: () => JSX.Element) => void` Provides an API to pass HTML to render in the `<head>`. This is most useful for collecting CSS from CSS-in-JS libraries and including the emitted CSS in the initial page response. See [Styling](https://github.hubspot.com/cms-react/reference/styling.html) for more details. ### useSharedIslandState ```js const [sharedState, updateSharedState, sharedStateID] = useSharedIslandState(); ``` Returns an object that includes the state shared by multiple islands, and an updater function. It works similarly to `useState`, but updating the state via `updateSharedState(newValue)` will update all of the other islands that also use `useSharedIslandState()`. Works in coordination with the `SharedIslandState` component. ### useSharedIslandReducer ```js const [sharedState, dispatch] = useSharedIslandReducer(); ``` Returns an object that includes the state shared by multiple islands and a dispatch function. It works similarly to `useReducer`, but actions dispatched will "reach across" and update all of the other islands that also use `useSharedIslandReducer()`. Works in coordination with the `SharedIslandReducer` component. ## Components ### Island Learn more about `<Island>` in the [island documentation](/reference/cms/react/islands). ### SharedIslandState Defines the initial value for the shared state accessed in `useSharedIslandState()` by other islands in this JS module or partial. All islands that are "wrapped" by `SharedIslandState` (i.e. are children or descendents of the children) will share a single state reference. `SharedIslandState` must be rendered on the server and cannot be contained inside an island. ```js <SharedIslandState value={...}> … </SharedIslandState> ``` ### SharedIslandReducer Defines the reducer function and initial value for the shared reducer state accessed in `useSharedIslandReducer()` by other islands in this JS module or partial. All islands that are "wrapped" by `SharedIslandReducer` (i.e. are children or descendants of the children) will share a single state reference and dispatch function. `SharedIslandReducer` must be rendered on the server and cannot be contained inside an island. The reducer function passed in must be imported with the `?client` suffix (which will automatically prepare code-splitting that function for the browser to grab it). ```js import reducerFuncReference from '../path/to/reducerFunc.js?client'; <SharedIslandReducer value={...} reducer={reducerFuncReference} > … </SharedIslandReducer> // reducerFunc.js export default function reducerFunc(state, action) { if (action.type === 'increment') { state = { ...state, new: ‘state value’ }; } return state; } ``` ## Field helper components The following components are designed to be used with associated module field definitions. They cannot be used for non-field related use cases. If an associated field is not found at the provided `fieldPath` then the components will render null. ### Icon `@hubspot/cms-components/Icon` The `Icon` component renders an SVG for a referenced `Icon` field. This component also accepts and applies all valid attributes for an SVG element, such as `id` and `style`. ```js import { Icon } from '@hubspot/cms-components'; export function Component() { return <Icon fieldPath="icon_field" height={10} />; } export const meta = { label: `Icon Module`, }; export const fields = [ { type: 'icon', default: { name: 'Alternate Level Up', unicode: 'f3bf', type: 'SOLID', }, icon_set: 'fontawesome-5.14.0', label: 'Icon', name: 'icon_field', }, ]; ``` | Prop | Type | Description | | --- | --- | --- | | `fieldPath` | String | The path of the icon field to render. For example:<ul><li>Top level `fieldPath="icon_field"`</li><li>Nested in a group: `fieldPath="group_field.icon_field"`</li><li>Repeater field: `fieldPath="icon_field[1]"`</li><li>Repeater group field: `fieldPath="group_field[0].icon_field"`</li></ul> | | `iconPurpose` | `'SEMANTIC'` (default) \| `'DECORATIVE'` | `SEMANTIC` will set the `role="img"` attribute on the SVG, as well as `aria-labelledby` pointing to the `title` element. | | `Title` | String | If provided, will render a tag within the SVG for accessibility to be described by `aria-labelledby` via `iconPurpose`. `iconStyle` `'REGULAR'` \| `'SOLID'` \| `'LIGHT'` If provided, overrides the default icon style associated with the field. Not all icons have every style. Will only use the override if the icon style exists. | ### RichText `@hubspot/cms-components/RichText` The `RichText` component handles inserting the RichText HTML into your component. This component passes through all valid attributes, such as `id` and `style`, to the wrapper tag and applies them. ```js import { RichText } from '@hubspot/cms-components'; export function Component() { return <RichText fieldPath="richtext_field" tag="span" />; } export const meta = { label: `RichText Module`, }; export const fields = [ { type: 'richtext', label: 'Rich text', name: 'richtext_field', default: '<p><em><strong>Hello, world!</strong></em></p>', }, ]; ``` | Prop | Type | Description | | --- | --- | --- | | `fieldPath` | String | The field of the Rich Text field to render. | | `tag` | String | The HTML tag used as the wrapping element for the content (e.g., `div`). | # @ Learn more about CMS React module field types. $ reference & cms/react --- # Field types When building CMS React modules, you'll use fields to define the customizable elements of a module, such as rich text fields and image fields. These fields are the same [fields provided for HubL modules](/reference/cms/fields/module-theme-fields), though their [implementation](/guides/cms/react/modules#module-fields) is slightly different. Field types for CMS React modules also include TypeScript definitions, so you'll benefit from autocompletion and validation when defining the fields. View reference documentation for all [available field types on GitHub](https://github.hubspot.com/cms-react/field-types/). ```js import { ModuleFields, TextField, RichTextField, ImageField, } from '@hubspot/cms-components/fields'; import { RichText } from '@hubspot/cms-components'; import logo from '../../../assets/sprocket.svg'; import styles from '../../../styles/getting-started.module.css'; export function Component({ fieldValues, hublParameters }) { const { src, alt, width, height } = fieldValues.logo; const { brandColors } = hublParameters; return ( <div className={styles.wrapper} style={{ backgroundColor: brandColors?.color, opacity: brandColors?.opacity, }} > <img src={src} alt={alt} width={width} height={height} /> <h1>{fieldValues.headline}</h1> <RichText fieldPath="gettingStarted" /> </div> ); } const richTextFieldDefaultValue = ` </div> `; export const fields = ( <ModuleFields> <ImageField name="logo" label="Logo" default={{ src: logo, height: 100, alt: 'HubSpot logo' }} resizable={true} /> <TextField name="headline" label="Headline" default="Getting Started with CMS React" /> <RichTextField name="gettingStarted" label="Getting Started" default={richTextFieldDefaultValue} /> </ModuleFields> ); export const meta = { label: 'Getting Started with CMS React', }; ``` # Islands @ Learn more about using islands in your CMS React projects to add a layer of interactivity to your React modules. $ reference & cms/react --- # Islands As a HubSpot developer, many pages that you develop will be static HTML, such as blog posts, knowledge base articles, and landing pages. But other times, you'll need to build interactive page elements, such as image carousels, forms with client validation, or live chat widgets. With traditional HubL modules, adding interactivity often requires rendering these interactive elements client-side using JavaScript. This can negatively impact page performance and SEO. By using [islands](https://www.patterns.dev/vanilla/islands-architecture/), you can avoid this issue by taking advantage of both client-side and server-side rendering. An island is a subtree of your interactive module. When serving a page to a visitor, the page is fully rendered server-side, initially rendering the page HTML without interactivity. Then, when interaction is needed, the island’s JavaScript is loaded, run, and attached to the page. This process of [hydration](https://react.dev/reference/react-dom/client/hydrateRoot) takes advantage of client-side rendering to optimize the addition of React components to the server-rendered HTML. Using islands comes with several benefits, including: - Reducing the time from [first paint](https://developer.mozilla.org/en-US/docs/Glossary/First_paint) to visitor interaction. - Improving page speed and SEO, including [First Contentful Paint](https://developer.chrome.com/docs/lighthouse/performance/first-contentful-paint), [Time to Interactive](https://developer.chrome.com/docs/lighthouse/performance/interactive), and [Cumulative Layout Shift](https://web.dev/articles/cls) scores. - The ability to customize hydration behavior per island, also known as [progressive hydration](https://www.patterns.dev/posts/progressive-hydration/). Below, learn how to implement islands, along with available island props and hydration behavior. ## Implementation - Import `Island` from `@hubspot/cms-components`. - Import the module you want to wrap, adding `?island` to the end of the import URL. - Render the `Island` component, passing the module name into the `module` prop. - Any serializable props that you want to pass to the module, excluding functions, can be passed as props in the `Island` component. ```js import { Island } from '@hubspot/cms-components'; import WeatherForecast from '../../islands/WeatherForecast.tsx?island'; import { ModuleFields, TextField } from '@hubspot/cms-components/fields'; export function Component({ fieldValues }: any) { const { headline } = fieldValues; return <Island module={WeatherForecast} headline={headline} />; } export const fields = ( <ModuleFields> <TextField label="Weather Headline" name="headline" default="Get the latest weather forecast" /> </ModuleFields> ); export const meta = { label: 'Weather Module', }; ``` ## Available props Below are the available `Island` component props. | Parameter | Type | Description | | --- | --- | --- | | `clientOnly` | Boolean | When set to `true`, the island won't be rendered on the server. This can be useful for components that rely on logic or libraries that can only run in the browser. Default is `false`. | | `hydrateOn` | `"load"` (default) | `"visible"` | `"idle"` | Specifies the type of [type of hydration](#hydration-types) to use. When rendering a page with islands on the server, the output includes a script to initialize islands on the client. Hydrating means downloading and initializing the island component code, so using these different hydration types strategically to defer some of that work can help boost initial page load performance. | | `id` | String | By default, HubSpot generates a unique ID for each island (e.g., `island-1234`). Use this prop to specify your own island ID. | | `module` | `React.Component` | The component to wrap. The component must include `?island` at the end of the import URL. | | `wrapperTag` | String | The HTML tag used as the wrapper. Must be a valid HTML element tag (e.g., `"span"`, `"article"`, or `"section"`). Default is `"div"`. | | `wrapperClassName` | String | A class name to pass to the wrapper. | | `wrapperStyle` | `CSSProperties` | Inline CSS properties to apply to the wrapper. | | `wrapper` | `React.Component` | Wraps the React tree of the `Island` component to provide custom context. This can be useful for integrating with CSS-in-JS libraries, such as styled-components, or other context providers that need to encapsulate the component's subtree for applying styles or context values. When using this prop, the component import URL must include a `?client` suffix to ensure that it's bundled for the client (similar to importing with the `?island` suffix). | ## Hydration types By default, the island initialization script will hydrate all islands as soon as possible (i.e., on load). But for more complex pages, you may want to defer hydration of non-critical page elements until the browser has finished all other work, or until the visitor scrolls down to the island. Using the `hydrateOn` prop, you can specify one of the following hydration behaviors: - `load` (default): hydrates the island on initial page load. - `idle`: defers hydration by using the [requestIdleCallback](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback) method. This can be useful for lower priority components, allowing client resources to be used first on higher priority items. - `visible`: hydration won't occur until the element is visible on screen by using the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). This can be useful for components that aren't immediately visible to the user on page load (i.e., elements farther down the page). For complex islands, this can provide a significant performance benefit: if the user never scrolls to see the island, it will never be loaded. # Local development @ Learn how to run a local development server for building CMS React projects. $ reference & cms/react --- # Local development `@hubspot/cms-dev-server` is a package that facilitates starting an Express + Vite development server, enabling an auto-reloading local development workflow that is nearly identical to your deployed components. The `cms-dev-server` also enables rendering local versions of your components on live CMS pages to aid in development. ## Basic usage You can start a local development server by running `hs-cms-dev-server/path/to/components-directory` in a project that has `@hubspot/cms-dev-server` installed. For example, a CMS page `https://cmssite.com/page` with JS rendered components would be accessible by visiting one of: - `http://cmssite.com.hslocal.net:3000/page` - `http://cmssite.com.localhost:3000/page` Or by visiting `http://hslocal.net:3000/proxy` and pasting in the page you want to proxy. Similar to how previewing a page from the page editor works, you can force the page to render with the context of a known contact by passing an `email` parameter. For example `http://cmssite.com.hslocal.net:3000/page?email=bh@hubspot.com` will cause the contact object to be populated based on the email parameter value. You may also start the dev server with the `--ssl` option, which enables: - `https://cmssite.com.hslocal.net:3000/page` - `https://cmssite.com.localhost:3000/page` ## Routes When the development server starts, it will look at the `components/modules` directory, then will dynamically create routes based on the modules that it finds. There are two routes for previewing your modules: `/module/moduleName` and `/preview/module/moduleName`. ### /module/moduleName `http/module/moduleName` The `/module/moduleName` route is rendered entirely locally without connecting with the HubSpot backend. This enables you to work offline and unauthenticated at this route. Field values that are used are derived entirely from the Field default values and from parameter-level overrides. Parameter-level overrides can be passed via the `fields` parameter, which expects stringified JSON of `fieldValues` that matches the passed `fieldValues` prop (matching the fields definition structure). Using this route includes some caveats for querying data: - GraphQL data in this context is fetched from your local machine using your local access token to authorize the collector service requests. These queries are cached, but you can bust the cache with the `?hsLocalQueryKey` query parameter. - `hublDataTemplate` is not supported at this route. - `Icon`, `CTA`, and other `@hubspot/cms-component` [field helpers](/reference/cms/react/cms-components-library#field-helper-components) are not supported at this route. ### /preview/module/moduleName The `/preview/module/moduleName` route interacts with the HubSpot backend and behaves similarly to viewing a module in the design manager previewer. Field values that are used rely on defaults, as there is no module instance to pull from. There is no `fields` parameter available for overrides. When querying data on this route: - GraphQL data is derived on the backend, and there is no query from the local server to the GraphQL service. - `hublDataTemplate` is supported. - `Icon`, `CTA`, and other `@hubspot/cms-component` field helpers are supported. ## Proxying private pages With the local development server running, you can preview your local changes on live HubSpot pages, including [private pages](/guides/cms/content/memberships/overview) that require passwords or login to access, as well as page previews. ### Proxied membership pages To view your local changes on a proxied membership page, first visit the page and log in with the configured method. Once authenticated, add `hslocal.net:3000` or `localhost:3000` to the root domain, just as you would with proxying a public page. To view the page as a specific contact, you can add an `email` query parameter to the URL, followed by the contact's email address, as shown below: `https://mydomain.hslocal.net:3000/private-page-path?email=hi@hubspot.com` That email will be used for the [`request_contact`](https://developers.hubspot.com/docs/reference/cms/hubl/variables#variables-available-in-all-templates) HubL variable, and can be passed to React using `hublDataTemplate`. ### Proxied preview To view your local changes on a proxied version of a page preview, first navigate to the page editor in HubSpot, then click **Preview** in the upper right, then **Open in a new tab**. With the page preview open in a new tab, add `hslocal.net:3000` or `localhost:3000` to the root domain. To view the page as a specific contact, you can add an `email` query parameter to the URL, followed by the contact's email address, as shown below: `https://mydomain.hslocal.net:3000/private-page-path?hs_preview=[preview_key]&email=hi@hubspot.com` ## Storybook The local development server includes a [Storybook](https://storybook.js.org/) integration which allows you to start a Storybook instance alongside the development server. Include a `--storybook` option when running the command to start the local development server. Once started, you can add `.stories.jsx` files alongside your components to build stories for testing or development. At the root `http://hslocal.net:3000` page, a link will be included to the Storybook UI for your project. To make building stories for HubSpot modules easier, cms-dev-server provides helpers to auto-generate `argTypes` based on module fields. See the [GraphQL and Storybook example project](https://github.com/HubSpot/cms-react/tree/main/examples/graphql-storybook/gql-storybook-project/gql-storybook-app) for usage of `moduleStory()`. Storybook is built with client components in mind, so components that cross island boundaries can have unexpected lifecycle behavior when rendered in a story. Because server-only components never make it to the browser, they cannot be hot reloaded, so a full re-render is necessary to update the server response. To fully emulate hybrid rendering in Storybook at the cost of hot module reloading, you can use `moduleStoryWithIsland()` in your story in place of `moduleStory()`. ## Fields Type Generation If you're using TypeScript in your project, you can use the `--generateFieldTypes` argument when starting the development server. This command will watch for changes to the `fields` object that is exported from module files and create a `.types.ts` file inside of the module directory. You can then import this module directly in your module component and use it in the generate `ModuleProps<T>` type. For example, suppose you have the following `fields.jsx` file: ```js // components/modules/MyModule/fields.tsx export const fields = ( <ModuleFields> <ChoiceField label="Choice Test" name="choice" display="select" choices={[ ['choice1', 'One'], ['choice2', 'Two'], ]} default="choice1" /> <NumberField label="Display on each blog post" name="numberField" /> <FieldGroup name="defaultGroup" label="Default text" locked> <TextField label="Text Field One" name="textFieldOne" default="Text Field" /> <TextField label="Text Field Two" name="textFieldTwo" /> <NumberField label="Number Field" name="numberField" /> </FieldGroup> </ModuleFields> ); ``` Running `hs-cms-dev-server /path/to/project --generateFieldsTypes` will generate a `modules/MyModule/fields.types.ts` file with a default exported type `MyModuleFieldsType`. The above `fields.tsx` will generate the following file: ```js // modules/MyModule/fields.types.ts // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. // Removing the above comment will disable type generation for this module // This file was created by @hubspot/cms-dev-server, for more information see https://github.hubspot.com/cms-react/reference/js-modules.html#module-fields import { type DefaultValues, type ChoiceFieldType, type NumberFieldType, type TextFieldType, type GroupFieldType, type Override } from "@hubspot/cms-components/fields"; type MyModuleFieldsType = DefaultValues<{ choice: Required<ChoiceFieldType>; numberField: NumberFieldType; defaultGroup: Override<GroupFieldType, { children: { textFieldOne: Required<TextFieldType>; textFieldTwo: TextFieldType; numberField: NumberFieldType; }; }>; }>; export default MyModuleFieldsType; ``` Then, you can import and use the type in your component as follows: ```js // components/modules/MyModule/index.tsx import { ModuleProps } from '@hubspot/cms-components'; import MyModule from './fields.types'; export const Component = ({ fieldValues, hublParameters = {}, }: ModuleProps<MyModule>) => { const number = fieldValues.numberField; // ^?const number: number | number[] | null | undefined // Note that can be undefined because no default set const choice = fieldValues.choice; // ^?const choice: string | number | (string | number)[] const text = fieldValues.defaultGroup.textFieldOne // ^?const text: string | null } ``` Note that the generated `fields.types.ts` file will be overwritten every time there is an update made to the fields object. To disable this behavior, remove the `THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY` comment at the top of the file. ## Secrets in local development To make secrets available for local development via `@hubspot/cms-dev-server`, create a `.env` file to define secret values for local use only. Keys in this file need to be prefixed with `HS_` for the development server to recognize it as a secret. ```shell HS_TEST_SECRET=localsecretvalue ``` This secret will be accessible locally without the `HS_` prefix you included in your `.env` file. In the example above, the secret can be accessed with `getSecret('TEST_SECRET')`. # CMS React styling @ Learn how to use various styling libraries in your project, including Tailwind, styled-components, styled-jsx, and CSS Modules. $ reference & cms/react --- # Styling In this guide, learn about the different ways to include styling in your CMS React project. You can also refer to the [styling example project](https://github.com/HubSpot/cms-react/tree/main/styling/styling-project/styling-app) for working examples of supported styling methods, including a number of major styling libraries. Below, learn more about each of the following methods: - [Styling](#styling) - [Tailwind](#tailwind) - [styled-components](#styled-components) - [styled-jsx](#styled-jsx) - [CSS Modules](#css-modules) - [Dynamic styles based on props](#dynamic-styles-based-on-props) - [Using other CSS-in-JS libraries](#using-other-css-in-js-libraries) ## Tailwind [Tailwind CSS](https://tailwindcss.com/) is a framework designed to speed up the building process by providing utility classes for styling your markup without needing to write custom CSS. You can learn more about Tailwind CSS on [HubSpot's blog](https://blog.hubspot.com/website/what-is-tailwind-css). To configure Tailwind for your components: - Add the `tailwindcss` dependency to your `package.json` file in the [JavaScript asset folder](/guides/cms/react/project-structure#project-structure). ```json { "name": "my-react-app", "version": "0.1.0", "type": "module", "dependencies": { ... "tailwindcss": "^3.3.2" } ... } ``` - Add a Tailwind configuration file to your project subcomponent. ```js import { fileURLToPath } from 'url'; const componentsDir = fileURLToPath(new URL('./components', import.meta.url)); export default { content: [`${componentsDir}/**/*.{js,ts,jsx,tsx}`], theme: { extend: {}, }, plugins: [], }; ``` - Add a PostCSS configuration file to your project subcomponent, then import the Tailwind configuration file and include Tailwind as a plugin along with any Tailwind-specific configuration. ```js import tailwind from 'tailwindcss'; import autoprefixer from 'autoprefixer'; import postcssNested from 'postcss-nested'; import tailwindConfig from './tailwind.config.js'; export default { plugins: [tailwind(tailwindConfig), postcssNested, autoprefixer()], }; ``` - Create a `tailwind.css` file in your styles directory. Then, import that file in your top-level component with the base Tailwind layer directives. See [HubSpot's styling example](https://github.com/HubSpot/cms-react/tree/main/examples/styling/styling-project/styling-app) for more details. ## styled-components [styled-components](https://styled-components.com/) is a library that enables you to write CSS in JavaScript and inject styles dynamically. Learn more about [the basics of styled-components](https://styled-components.com/docs/basics). To use styled-components in your project: - Add the `styled-components` dependency to your `package.json` file in the [JavaScript asset folder](/guides/cms/react/project-structure#project-structure). ```json { "name": "my-react-app", "version": "0.1.0", "type": "module", "dependencies": { ... "styled-components": "^6.0.3" } ... } ``` - Create a registry component using the [styled-components server-side rendering API](https://styled-components.com/docs/advanced#server-side-rendering) and [`useInlineHeadAsset()`](/reference/cms/react/cms-components-library#useinlineheadasset), and wrap the components you want to style in it. See the [StyledComponentsRegistry.jsx example file](https://github.com/HubSpot/cms-react/blob/main/examples/styling/styling-project/styling-app/components/StyledComponentsRegistry.jsx) for more details. - For each island you're using, you must wrap each subtree in a registry to capture styles when rendering on the server. To make this easier, you may use the `Wrapper` prop on the `Island` component to wrap the contents without needing to edit the island components themselves. Note that this prop also lets you configure this once by replacing all instances of `<Island />` with a `<StyledIsland />` component that looks similar to the following: ```js import { Island } from '@hubspot/cms-components'; import StyledComponentsRegistry from './StyledComponentsRegistry?client'; export default function StyledIsland(props) { return <Island {...props} Wrapper={StyledComponentsRegistry} />; } ``` As shown above, when using the `Wrapper` prop you must import the component with a `?client` suffix to make sure it can be bundled for the client. - You can now import `styled` from `'styled-components'` and use it to style your components. ```js import { useState, useId } from 'react'; import { styled } from 'styled-components'; const StyledButton = styled.button` padding: ${(props) => 10 + props.$count * 10}px; `; export default function InteractiveStyledComponent() { const id = useId(); const [count, setCount] = useState(0); return ( <StyledButton $count={count} onClick={() => setCount((prevCount) => prevCount + 1)} > Current count is: {count} {id} </StyledButton> ); } ``` ## styled-jsx [styled-jsx](https://github.com/vercel/styled-jsx) is a library that enables you to write encapsulated and scope CSS to style your components. By setting styles at the individual component level, you can add, change, and delete styles for one component without impacting others. To use `styled-jsx` in your project: - Add the `styled-jsx-components` dependency to your `package.json` file in the [JavaScript asset folder](/guides/cms/react/project-structure#project-structure). ```json { "name": "my-react-app", "version": "0.1.0", "type": "module", "dependencies": { ... "styled-jsx": "^5.1.2" } ... } ``` - Create a registry component using the [server-side rendering API](https://github.com/vercel/styled-jsx#server-side-rendering) and [`useInlineHeadAsset()`](/reference/cms/react/cms-components-library#useinlineheadasset). See the [StyledJSXRegistry.jsx example file](https://github.com/HubSpot/cms-react/blob/main/examples/styling/styling-project/styling-app/components/StyledJSXRegistry.jsx) for more details. - The `styled-jsx` registry component must also be wrapped on any `<Island>` usage to prevent hydration mismatches or missing styles on initial load. Note that `styled-jsx'`s implementation depends on `useId()` hooks in a way that can cause mismatches if not properly configured. See the [StyledJSXIsland.jsx example file](https://github.com/HubSpot/cms-react/blob/main/examples/styling/styling-project/styling-app/components/StyledJSXIsland.jsx) for an easier pattern of replacing all direct `<Island />` usage. ```js import { Island } from '@hubspot/cms-components'; import StyledComponentsRegistry from './StyledComponentsRegistry?client'; export default function StyledIsland(props) { return <Island {...props} Wrapper={StyledComponentsRegistry} />; } ``` - You can now use `styled-jsx` to style your components, including ``<style jsx>{`/_ CSS here _/`}</style>`` patterns. See example below, which is copied from the [InteractiveStyledJSXComponent.jsx example file](https://github.com/HubSpot/cms-react/blob/main/examples/styling/styling-project/styling-app/components/InteractiveStyledJSXComponent.jsx). ```js import { useState, useId } from 'react'; export default function InteractiveStyledJSXComponent() { const id = useId(); const [count, setCount] = useState(0); return ( <button onClick={() => setCount((prevCount) => prevCount + 1)}> Current count is: {count} {id} <style jsx> {` button { padding: ${10 + count * 10}px; } `} </style> </button> ); } ``` ## CSS Modules [CSS Modules](https://github.com/css-modules/css-modules) is a CSS file where all class names and animation names are scoped locally by default. You can use CSS Modules within any React component by importing a file ending in `.module.css`, which will return a CSS module object: ```js import classes from './example.module.css'; export default function MyComponent() { ; } ``` ```css .red { color: red; } /* How to have global—non-namespaced—styles in CSS modules */ :global(html) { border: 6px solid SteelBlue; } ``` When you import a CSS modules file from inside a React component: - A `<style>` tag will automatically be inserted into the page when the component is rendered server-side <u>or</u> when it's dynamically rendered in the browser. - Those styles will automatically be namespaced so they don't interfere with anything else on the page. You can also import regular CSS files into your React components, but their selectors will not be automatically namespaced. ### Dynamic styles based on props If you need to dynamically adjust styles based on props, try one of the following options: - If you need conditional styling that is either on or off, you can have a `className` that the React component code conditionally renders in your JSX. - If you need dynamic styling that is not on or off, but rather a specific color or number that you need to apply to your styles: - You can define CSS custom properties in your CSS or CSS Modules code and inject new CSS custom property values via React. - Or use React to set inline styles on the specific part of the module HTML. The code below shows examples of each of the above techniques. ```js import styles from './example2.module.css'; export default function FancierComponent(props) { const { hasPurpleBorder, paddingPx, customSecondTextColor } = props; // Example: toggling styles via a prop const classes = [styles['fancy-module-wrapper']]; if (hasPurpleBorder) { classes.push(styles['purple-border']); } // Example: using inline style attribute (with React's style syntax) const inlineStyles = { padding: paddingPx }; // Example: setting a CSS custom property value that's picked up by other CSS const inlineAndCustomPropertyStyles = { ...inlineStyles, '--second-text-color': customSecondTextColor, }; return ( ); } ``` ```css .fancy-module-wrapper { } .purple-border { border: 2px solid rebeccapurple; } .second-text { color: var(--second-text-color, mediumvioletred); } ``` ## Using other CSS-in-JS libraries CMS React projects support any CSS-in-JS libraries that provide a server-side rendering API and don't depend on a Babel plugin. The same registry patterns described above can be generalized for other libraries to emit CSS to include as part of the server-side render. You'll also need to include the registry as a `Wrapper` on any `<Island>` component if there are styles within the island. # Testing for CMS React @ Learn how to implement unit tests for a CMS React project. $ reference & cms/react --- # Testing React components are easy to unit test with [Vitest](https://vitest.dev/) and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/). To add tests to your own project, start by adding those packages as dev dependencies, as well as `@vitejs/plugin-react` for React support. ```javascript import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], }); ``` For Vitest to work properly with your React components, you'll also need to add a `vitest.config.js` file in your package root. When writing a test file that uses React Testing Library to render components or reference any browser-specific APIs, add the following to the top of the file: ```javascript // @vitest-environment jsdom ``` This enables React Testing Library’s [`render`](https://testing-library.com/docs/react-testing-library/api/#render) function to work. Learn more about [Vitest test environments](https://vitest.dev/guide/environment.html#test-environment) # Serverless functions reference (projects) @ Reference information for serverless functions built with developer projects, including serverless.json, function files, endpoints, CLI commands, and managing packages. $ reference & cms/serverless-functions --- # Serverless functions built with projects This page contains reference documentation for newer, project-based serverless functions. For serverless functions built using the design manager, check out the [serverless functions (design manager) reference](/reference/cms/serverless-functions). If you're looking to implement serverless functions in a UI extension, check out the [CRM customization guide](/guides/crm/private-apps/serverless-functions). On this page, you'll find reference information for serverless functions built with developer projects. Though conceptually the same as serverless functions built with the design manager, project-based serverless functions require a different `serverless.json` configuration, enable the use of third-party dependencies, and have more direct access to private app access tokens for authentication. For a high-level overview of serverless functions, see the [serverless functions overview](/guides/cms/content/data-driven-content/serverless-functions/overview). And to get started building project-based serverless functions, check out the [get started guide](/guides/cms/content/data-driven-content/serverless-functions/getting-started-with-serverless-functions). ## File structure Serverless functions are packaged within a `.functions` directory within the `app` directory of a project. The `.functions` directory can have any name you'd like, as long as it ends in `.functions`. In the `.functions` directory, you'll need to include three files: - `serverless.json`: the configuration file for the serverless function. - `function.js`: A JavaScript file containing the code to execute. This file can have any name, and you'll reference the file name in `serverless.json`. - A `package.json` configuration file to contain the dependencies needed to execute the function. ```shell project-folder/ │── src/ ├──── app │ ├── app.json │ ├── app.functions/ │ ├── function.js │ ├── package.json │ └── serverless.json └─ hsproject.json ``` Below, learn more about each file. ## Serverless.json The `serverless.json` file stores the serverless function configuration. This is a required file, and maps your functions to their endpoints. If you're upgrading your existing serverless functions to be built using developer projects, note that the `runtime`, `environment,` and `version` fields have been removed. ```js { "appFunctions": { "functionName": { "file": "function.js", "secrets": [], "endpoint": { "path": "fetch-quote", "method": ["GET"] } } } } ``` | Field | Type | Description | | --- | --- | --- | | `functionName` | Object | An object containing the serverless function's configuration details. This object can have any name. | | `file` | String | The name of the JavaScript file containing the serverless function code to execute. | | `secrets` | Array | An array containing names of secrets that the function will use for authenticating requests. Learn more about [authentication](#authentication). | | `endpoint` | Object | An object containing details about the endpoint that you can hit to invoke the function. The `path` field defines the last value in the `/hs/serverless/<path>` endpoint path, while the `method` field defines the request method. Learn more about [endpoints](#endpoints). | Do not assign the same name to your secrets and environment variables. Doing so will result in conflicts when returning their values in the function. ### Function file Serverless functions built with developer projects use the [NodeJS](https://nodejs.org/en/about/) runtime (version 18 and higher). Lower versions of Node cannot be specified. In addition, serverless functions should be asynchronous, using `await` and `try`/`catch` instead of promise chaining. Below is an example of JavaScript code that can be executed by a serverless function. This code is taken from the [get started with serverless functions guide](/guides/cms/content/data-driven-content/serverless-functions/getting-started-with-serverless-functions) and fetches a quote via the [Zen Quotes API](https://zenquotes.io/). Learn more about [authenticating requests](#authentication). ```js const axios = require('axios'); exports.main = async (context) => { try { // Make GET request to the ZenQuotes API const response = await axios.get('https://zenquotes.io/api/random'); // Extract the quote data (first item in the array) const quoteData = response.data[0]; // Log the quote and author to console console.log(`"${quoteData.q}" — ${quoteData.a}`); // Return a properly formatted response with status code and body return { statusCode: 200, body: quoteData, headers: { 'Content-Type': 'application/json', }, }; } catch (error) { // Handle any errors that occur during the request console.error('Error fetching quote:', error.message); // Return an error response return { statusCode: 500, body: { error: 'Failed to fetch quote' }, headers: { 'Content-Type': 'application/json', }, }; } }; ``` ### package.json In the `package.json` file, you can specify dependencies to include in the `dependencies` field. When the app is built, dependencies will be bundled with your function code. All dependencies must be published to NPM and be public. For example, if you wanted to add the lodash library in a serverless function, you would first update package.json to include the dependency: ```json { "name": "example-serverless-function", "version": "0.1.0", "author": "HubSpot", "license": "MIT", "dependencies": { "@hubspot/api-client": "^7.0.1", "axios": "^0.27.2", "lodash": "^4.17.21" } } ``` ## Endpoints Serverless functions for the CMS are invoked by calling its public URL, which has the following structure: `https://<domain>/hs/serverless/<path>`. | Parameter | Description | | --- | --- | | `<domain>` | A domain connected to the HubSpot account. This can be any connected domain, including the default `<hubId>.hs-sites.com` domain. | | `/hs/serverless/` | The path reserved for serverless functions. All endpoints exist inside this path. | | `<path>` | The `path` value specified in the `serverless.json` file. | For example, if both website.com and subdomain.brand.com are connected to the account, you could call the function using `https://website.com/hs/serverless/<path>` or `https://subdomain.brand.com/hs/serverless/<path>`. If the serverless function's `path` field was set to `fetch-quote`, the full URL would be: `https://website.com/hs/serverless/fetch-quote` ## Authentication Serverless functions requests built with projects can be authenticated using either the app's private app access token or a secret. Calls authenticated with private app access tokens count against your API call limits. - Private app access tokens can be used for authenticating HubSpot API requests, using the value `PRIVATE_APP_ACCESS_TOKEN`. - Secrets can be referenced by name, and must also be included in the `secrets` array of the `serverless.json` file. To create, manage, and view secrets associated with your account, use the set of `hs secrets` [CLI commands](/guides/cms/tools/hubspot-cli/cli-v7#add-a-secret). - In the function file, authentication for app tokens and secrets are both accessed with `process.env`. Below is an example of the same request but using the private app access token or secret for authentication (found in the `headers` object). ```js const axios = require('axios'); exports.main = async (context = {}) => { try { const response = await axios.get( `https://api.hubspot.com/cms/v3/hubdb/tables/109906251/rows?sort=random()&limit=1`, { headers: { Authorization: `Bearer ${process.env.PRIVATE_APP_ACCESS_TOKEN}`, 'Content-Type': 'application/json', }, } ); return response.data.results; } catch (error) { console.log('Error details:', error); } }; ``` Note that, for secret authentication, if your secret name was `my_secret`, then the `serverless.json` file would need to include `"secrets": ["my_secret"]`. ```js const axios = require('axios'); exports.main = async (context = {}) => { try { const response = await axios.get( `https://api.hubspot.com/cms/v3/hubdb/tables/109906251/rows?sort=random()&limit=1`, { headers: { Authorization: `Bearer ${process.env.my_secret}`, 'Content-Type': 'application/json', }, } ); return response.data.results; } catch (error) { console.log('Error details:', error); } }; ``` ## Context object The context object contains contextual information about the function's execution, stored in the following parameters. | Parameter | Description | | --- | --- | | `accountId` | The HubSpot account ID containing the function. | | `body` | Populated if the request method is `POST` with a content type of `application/json`. | | `contact` | If the request is from a cookied contact, the contact object will be populated with a set of basic contact properties along with the following information:<ul><li>`vid`: The contact’s visitor ID.</li><li>`isLoggedIn`: when using [CMS Memberships](https://knowledge.hubspot.com/website-pages/require-member-registration-to-access-private-content), this will be `true` if the contact is logged in to the domain.</li><li>`listMemberships`: an array of contact list IDs that this contact is a member of.</li></ul> | | `headers` | Contains the [headers](#headers) sent from the client hitting your endpoint. | | `params` | Populated with query string values, along with any HTML Form-POSTed values. These are structured as a map with strings as keys and an array of strings for each value. | ### Headers If you need to know the headers of the client that's hitting your endpoint, you can access them through `context.headers`, similar to how you would access information through `context.body`. Below, review some of the common headers that HubSpot provides. For a full list, see [MDN's HTTP headers documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers). | header | Description | | --- | --- | | `accept` | Communicates which content types the client understands, expressed as [MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types). [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept). | | `accept-encoding` | Communicates the content encoding the client understands. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Encoding). | | `accept-language` | Communicates which human language and locale is preferred. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language). | | `cache-control` | Holds directives for caching. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control). | | `connection` | Communicates whether the network connection stays open. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Connection). | | `cookie` | Contains cookies sent by the client. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cookie). | | `host` | Communicates the domain name and TCP port number of a listening server. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Host). | | `true-client-ip` | IP address of the end-user. [See Cloudflare true-client-ip](https://developers.cloudflare.com/network/true-client-ip-header/). | | `upgrade-insecure-requests` | Communicates the clients preference for an encrypted and authenticated response. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Upgrade-Insecure-Requests). | | `user-agent` | Vendor-defined string identifying the application, operating system, application vendor, and version. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/User-Agent). | | `x-forwarded-for` | Identifies the originating IP address of a client through a proxy or load balancer. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For). | #### Redirect by sending a header You can perform a redirect from your serverless function by sending a response with a _location_ header and `301` statusCode. ```js sendResponse({ statusCode: 301, headers: { Location: 'https://www.example.com', }, }); ``` ## Limits Serverless functions are intended to be fast and have a narrow focus. To enable quick calls and responses, HubSpot serverless functions have the following limits: - 50 secrets per account. - 128MB of memory. - No more than 100 endpoints per HubSpot account. - You must use `contentType` of `application/json` when calling a function. - Serverless function logs are stored for 90 days. - 6MB on an AWS Lambda invocation payload. **Execution limits** - Each function has a maximum of 10 seconds of execution time. - Each account is limited to 600 total execution seconds per minute. This means either of these scenarios can happen: - 60 function executions that take 10 seconds each to complete. - 6,000 function executions that take 100 milliseconds to complete. Functions that exceed those limits will throw an error. Execution count and time limits will return a `429` response. The execution time of each function is included in the [serverless function logs](/guides/cms/tools/hubspot-cli/cli-v7#get-logs). To assist in avoiding these limits, limit data is provided automatically to the [function context](/reference/cms/serverless-functions#function-file) during execution. You can use that to influence your application to stay within those limits. For example, if your application requires polling your endpoint, then you can return with your data a variable to influence the frequency of the polling. That way when traffic is high you can slow the rate of polling avoiding hitting limits, then ramp it back up when traffic is low. # Serverless functions reference (design manager) @ Reference information for serverless functions built with the design manager, including serverless.json, function files, endpoints, CLI commands, and managing packages. $ reference & cms/serverless-functions --- # Serverless functions built with the design manager This page contains reference information for serverless functions built using the design manager. Historically, this is the original way to build serverless functions for your CMS pages, and is still supported. However, moving forward it's recommended to [build and deploy serverless functions with HubSpot developer projects](/guides/cms/content/data-driven-content/serverless-functions/getting-started-with-serverless-functions). Project-based serverless functions can include third-party dependencies, and have more direct access to authentication through private app access tokens. In this article, learn about the files found inside of a [serverless `.functions` folder](/guides/cms/content/data-driven-content/serverless-functions/overview#serverless-function-folders) and the CLI commands you can use with serverless functions. For a high-level overview of serverless functions, see the [serverless functions overview](/guides/cms/content/data-driven-content/serverless-functions/overview). For more information about building serverless functions with projects for [JavaScript rendered modules and partials](https://github.hubspot.com/cms-js-building-block-examples/), see the [developer projects documentation](/guides/crm/private-apps/serverless-functions). ## Serverless.json In the `.functions` folder, the `serverless.json` file stores the serverless function configuration. This is a required file, and maps your functions to their [endpoints](#endpoints). ```json { "runtime": "nodejs20.x", "version": "1.0", "environment": { "globalConfigKey": "some-value" }, "secrets": ["secretName"], "endpoints": { "events": { "file": "function1.js", "method": "GET" }, "events/update": { "method": "POST", "file": "action2.js", "environment": { "CONFIG_KEY": "some-other-value" }, "secrets": ["googleKeyName", "otherkeyname"] } } } ``` | key | Type | Description | | --- | --- | --- | | `runtime` | | `version` | String | HubSpot serverless function schema version. (Current version 1.0) | | `environment` | Object | Configuration variables passed to the executing function as [environment variables](https://nodejs.org/docs/latest-v10.x/api/process.html#process_process_env) at runtime. You might use this to add logic for using a testing version of an API instead of the real thing based on an environment variable. | | `secrets` | Array | An array containing the names of the [secrets](#secrets) your serverless function will use for authentication. Do not store secret values directly in this file, only reference secret names. | | `endpoints` | Object | Endpoints define the paths that are exposed and their mapping to specific JavaScript files, within your functions folder. Learn more about [endpoints](#endpoints). | Do not assign the same name to your secrets and environment variables. Doing so will result in conflicts when returning their values in the function. ### Endpoints Each endpoint can have its own [environment variables](https://nodejs.org/docs/latest-v10.x/api/process.html#process_process_env) and secrets. Variables specified outside of endpoints should be used for configuration settings that apply to all functions and endpoints. ```json "events/update": { "method": "POST", "file": "action2.js", "environment": { "configKey": "some-other-value" }, "secrets": ["googleAPIKeyName","otherKeyName"] } ``` | key | Type | Description | | --- | --- | --- | | `method` | String or array of strings | [HTTP method or methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods) that the endpoint supports. Defaults to [GET](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/GET). | | `file` | String | Path to JavaScript function file with the implementation for the endpoint. | The endpoint can be invoked by calling a URL at one of the domains connected to HubSpot account, including the default `.hs-sites.com` subdomains, with the following URL structure: `https://{domainName}/_hcms/api/{endpoint-name/path}?portalid={hubId}`. | Parameter | Description | | --- | --- | | `domainName` | A domain connected to the HubSpot account. | | `/_hcms/api/` | The path reserved for serverless functions. All endpoints exist inside this path. | | `endpoint-name/path` | The endpoint name or path that you specified in the `serverless.json` file. | | `portalid` | An optional query parameter containing your HubSpot account ID. Providing this in the request will enable you to test your functions within module and template previews. | For example, if both `website.com` and `subdomain.brand.com` are connected to the account, you could call the function using either `https://website.com/_hcms/api/{endpoint-name/path}` or `https://subdomain.brand.com/_hcms/api/{endpoint-name/path}`. ## Function file In addition to the `serverless.json` configuration file, the `.functions` folder will also contain a [Node.js](https://nodejs.org/en/) JavaScript file that defines the function. You can also leverage the [request](https://github.com/request/request#readme) library to make HTTP request to [HubSpot APIs](/reference/api/overview) and other APIs. For example: ```js // Require axios library, to make API requests. const axios = require('axios'); // Environment variables from your serverless.json // process.env.globalConfigKey exports.main = (context, sendResponse) => { // your code called when the function is executed // context.params // context.body // context.accountId // context.limits // secrets created using the CLI are available in the environment variables. // process.env.secretName //sendResponse is what you will send back to services hitting your serverless function. sendResponse({ body: { message: 'my response' }, statusCode: 200 }); }; ``` ### Context object The context object contains contextual information about the function's execution, stored in the following parameters. | Parameter | Description | | --- | --- | | `accountId` | The HubSpot account ID containing the function. | | `body` | Populated if the request is sent as a `POST` with a content type of `application/json`. | | `contact` | If the request is from a cookied contact, the contact object will be populated with a set of basic contact properties along with the following information:<ul><li>`vid`: The contact’s visitor ID.</li><li>`isLoggedIn`: when using CMS Memberships, this will be `true` if the contact is logged in to the domain.</li><li>`listMemberships`: an array of contact list IDs that this contact is a member of.</li></ul> | | `headers` | Contains the [headers](#headers) sent from the client hitting your endpoint. | | `params` | Populated with query string values along with any HTML Form-POSTed values. These are structured as a map with strings as keys and an array of strings for each value.`context.params.yourvalue` | | `limits` | Returns how close you are to hitting the [serverless function rate limits](/guides/cms/content/data-driven-content/serverless-functions/overview#know-your-limits).<ul><li>`executionsRemaining`: how many executions per minute are remaining.</li><li>`timeRemaining`: how much allowed execution time is remaining.</li></ul> | ### Headers If you need to know the headers of the client that's hitting your endpoint, you can access them through `context.headers`, similar to how you would access information through `context.body`. Below, review some of the common headers that HubSpot provides. For a full list, see [MDN's HTTP headers documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers). | header | Description | | --- | --- | | `accept` | Communicates which content types expressed as [MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types), the client understands. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept). | | `accept-encoding` | Communicates the content encoding the client understands. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Encoding). | | `accept-language` | Communicates which human language and locale is preferred. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language). | | `cache-control` | Holds directives for caching. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control). | | `connection` | Communicates whether the network connection stays open. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Connection). | | `cookie` | Contains cookies by sent the client. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cookie). | | `host` | Communicates the domain name and TCP port number of a listening server. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Host). | | `true-client-ip` | IP address of the end-user. [See Cloudflare true-client-ip](https://developers.cloudflare.com/network/true-client-ip-header/). | | `upgrade-insecure-requests` | Communicates the clients preference for an encrypted and authenticated response. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Upgrade-Insecure-Requests). | | `user-agent` | Vendor defined string identifying the application, operating system, application vendor, and version. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/User-Agent). | | `x-forwarded-for` | Identifies the originating IP address of a client through a proxy or load balancer. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For). | #### Redirect by sending a header You can perform a redirect from your serverless function by sending a response with a _location_ header and `301` statusCode. ```js sendResponse({ statusCode: 301, headers: { Location: 'https://www.example.com', }, }); ``` ### Set cookies from your endpoint From your serverless function you can tell the client (web browser) to set a cookie. ```js exports.main = (context, sendResponse) => { sendResponse({ body: { ... }, 'Set-Cookie': 'myCookie1=12345; expires=...; Max-Age=...', statusCode: 200 }); } ``` ### Set multiple values for a single header For headers that support multiple values, you can use `multiValueHeaders`, to pass the values. For example: you can tell the browser to set multiple cookies. ```js exports.main = (context, sendResponse) => { sendResponse({ body: { ... }, multiValueHeaders: { 'Set-Cookie': [ 'myCookie1=12345; expires=...; Max-Age=...', 'myCookie2=56789; expires=...; Max-Age=...' ] }, statusCode: 200 }); } ``` ## Secrets When you need to authenticate a serverless function request, you'll use secrets to store values such as API keys or private app access tokens. Using the CLI, you can add secrets to your HubSpot account to store those values, which you can later access through environment variables (`process.env.secretName`). Secrets are managed through the HubSpot CLI using the following commands: - [`hs secrets list`](/guides/cms/tools/hubspot-cli/cli-v7#list-secrets) - [`hs secrets add`](/guides/cms/tools/hubspot-cli/cli-v7#add-a-secret) - [`hs secrets delete`](/guides/cms/tools/hubspot-cli/cli-v7#remove-a-secret) Once added through the CLI, secrets can be made available to functions by including a `secrets` array containing the name of the secret. This enables you to store your function code in [version control](/guides/cms/setup/github-integration) and use secrets without exposing them. However, you should <u>never</u> return your secret's value through console logging or as a response, as this will expose the secret in logs or in front-end pages that call your serverless function. Due to caching, it can take about one minute to see updated secret values. If you've just updated a secret but are still seeing the old value, check again after about a minute. ## Using serverless functions with the form element When submitting serverless functions use javascript to handle the form submission, and use the `"contentType" : "application/json"` header in your request. Do not use the `<form>` elements `action` attribute. ## CORS [**Cross Origin Resource Sharing (CORS)**](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) is a browser security feature. By default browsers restrict cross-origin requests initiated by javascript. This prevents malicious code running on a different domain, from affecting your site. This is called the same-origin policy. Because sending and retrieving data from other servers is sometimes a necessity, the external server, can supply HTTP headers that communicate which origins are permitted to read the information from a browser. You should not run into CORS issues calling your serverless function within your HubSpot hosted pages. If you do, verify you are using the correct protocol. **Getting this CORS error?** _"Access to fetch at \[your function url\] from origin \[page making request\] has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled."_ **Is your request to a different origin than the site calling it?** - If the domain name is different, yes. - If using a different protocol(http, https), yes. If using a different protocol, simply change the protocol to match and that will fix it. You can't modify HubSpot's `Access-Control-Allow-Origin` header at this time. [See MDN for further detailed CORS error troubleshooting.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS/Errors) ### Get requests Get requests may be able to make [CORS requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) depending on the client. Do not make [GET](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/GET) requests write anything, just return data. ## Preloaded packages HubSpot serverless functions currently come preloaded with the following packages: - [@hubspot/api-client](https://www.npmjs.com/package/@hubspot/api-client): ^1.0.0-beta - [axios](https://www.npmjs.com/package/axios): ^0.19.2 - [request](https://www.npmjs.com/package/request): ^2.88.0 - [requests](https://www.npmjs.com/package/requests): ^0.2.2 To use the latest supported version of a preloaded package, or to use a newly added package: 1. Clone or copy your function file. 2. Change your function's endpoint in the `serverless.json` file to point to your new function file. You can safely delete the old version. If you want to include packages outside of the preloaded package set, you can use [webpack](https://webpack.js.org/) to combine your node modules and have your bundled files be your function files. ## Limits Serverless functions are intended to be fast and have a narrow focus. To enable quick calls and responses, HubSpot serverless functions have the following limits: - 50 secrets per account. - 128MB of memory. - No more than 100 endpoints per HubSpot account. - You must use `contentType` `application/json` when calling a function. - Serverless function logs are stored for 90 days. - 6MB on an AWS Lambda invocation payload. **Execution limits** - Each function has a maximum of 10 seconds of execution time. - Each account is limited to 600 total execution seconds per minute. This means either of these scenarios can happen: - 60 function executions that take 10 seconds each to complete. - 6,000 function executions that take 100 milliseconds to complete. Functions that exceed those limits will throw an error. Execution count and time limits will return a `429` response. The execution time of each function is included in the [serverless function logs](/guides/cms/tools/hubspot-cli/cli-v7#get-logs). To assist in avoiding these limits, limit data is provided automatically to the [function context](/reference/cms/serverless-functions#function-file) during execution. You can use that to influence your application to stay within those limits. For example, if your application requires polling your endpoint, then you can return with your data a variable to influence the frequency of the polling. That way when traffic is high you can slow the rate of polling avoiding hitting limits, then ramp it back up when traffic is low. # CrmActionButton @ Learn about the CrmActionButton component for use in UI extensions. $ reference & ui-components/crm-action-components --- # CrmActionButton | UI components (BETA) The `CrmActionButton` component renders a button that can execute a built-in set of [CRM actions](/reference/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](/reference/ui-components/crm-action-components/overview). ```js import { CrmActionButton } from '@hubspot/ui-extensions/crm'; <CrmActionButton actionType="PREVIEW_OBJECT" actionContext={{ objectTypeId: '0-3', objectId: 123456, }} variant="secondary" > Preview deal </CrmActionButton>; ``` | Prop | Type | Description | | --- | --- | --- | | `actionType` | String | The type of action to perform. See [list of available actions](/reference/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](/reference/ui-components/crm-action-components/overview#available-actions) for required context values. | | `variant` | `'primary'` | `'secondary'` (default) | `'destructive'` | The color variation of the button. | | `type` | `'button'` (default) | `'reset'` | `'submit'` | The button's HTML `role` attribute. | | `size` | `'xs'`, `'extra-small'` | `'sm'`, `'small'` | `'md'`, `'medium'` (default) | The size of the button. | | `disabled` | Boolean | When set to to `true`, button renders in a disabled, greyed-out state and cannot be clicked. | | `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. | ## 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](/reference/ui-components/crm-action-components/crm-action-link) - [CrmCardActions](/reference/ui-components/crm-action-components/crm-card-actions) - [Dropdown](/reference/ui-components/standard-components/dropdown) # CrmActionLink @ Learn about the CrmActionLink component for use in UI extensions. $ reference & ui-components/crm-action-components --- # CrmActionLink | UI components (BETA) The `CrmActionLink` component renders a clickable link that can execute a built-in set of [CRM actions](/reference/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](/reference/ui-components/crm-action-components/overview). ```js import { CrmActionLink } from '@hubspot/ui-extensions/crm'; const dealContext = { objectTypeId: "0-3", objectId: 14795354663, }; hubspot.extend(({ context, runServerlessFunction, actions }) => { return ( <> <CrmActionLink actionType="ADD_NOTE" actionContext={dealContext} > Add a note about this deal to the record </CrmActionLink> </> ); }); ``` | Prop | Type | Description | | --- | --- | --- | | `actionType` | String | The type of action to perform. See [list of available actions](/reference/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](/reference/ui-components/crm-action-components/overview#available-actions) for required context values. | | `variant` | `'primary'` (default) | `'light'` | `'dark'` | `'destructive'` | The color variation of the link. See the [variants section](#variants) for more information. | | `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. | ## 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](/reference/ui-components/crm-action-components/crm-action-button) - [Button](/reference/ui-components/standard-components/button) - [CrmCardActions](/reference/ui-components/crm-action-components/crm-card-actions) # CrmCardActions @ Learn about the CrmCardActions component for use in UI extensions. $ reference & ui-components/crm-action-components --- # CrmCardActions | UI components (BETA) The `CrmCardActions` component renders a smaller standalone or dropdown menu button that can contain multiple [CRM actions](/reference/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](/reference/ui-components/crm-action-components/overview). ```js import { CrmCardActions } from '@hubspot/ui-extensions/crm'; <CrmCardActions actionConfigs={[ { type: 'action-library-button', label: 'Preview', actionType: 'PREVIEW_OBJECT', actionContext: { objectTypeId: '0-3', objectId: 14795354663, }, tooltipText: 'Preview this deal record.', }, { type: 'dropdown', label: 'Activities', options: [ { type: 'action-library-button', label: 'Send email', actionType: 'SEND_EMAIL', actionContext: { objectTypeId: '0-1', objectId: 769851, }, }, { type: 'action-library-button', label: 'Add note', actionType: 'ADD_NOTE', actionContext: { objectTypeId: '0-1', objectId: 769851, }, }, ], }, ]} />; ``` Unlike [CrmActionButton](/reference/ui-components/crm-action-components/crm-action-button) and [CrmActionLink](/reference/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` | | `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](/reference/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](/reference/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](/reference/ui-components/crm-action-components/crm-action-link) - [Button](/reference/ui-components/standard-components/button) - [ButtonRow](/reference/ui-components/standard-components/button-row) # CRM action components @ Learn about CRM action components for use in UI extensions. $ reference & ui-components/crm-action-components --- # CRM action components (BETA) 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`. ```js import { CrmActionButton, CrmActionLink, CrmCardActions, } from '@hubspot/ui-extensions/crm'; ``` ## Available components 1. [CrmActionButton](/reference/ui-components/crm-action-components/crm-action-button)**:** renders a button. 2. [CrmActionLink](/reference/ui-components/crm-action-components/crm-action-link)**:** renders a clickable link. 3. [CrmCardActions](/reference/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 ```js <CrmActionButton actionType="PREVIEW_OBJECT" actionContext={{ objectTypeId: '0-3', objectId: 123456, }} variant="secondary" > Preview deal </CrmActionButton> ``` ### Create a note ```js <CrmActionButton actionType="ADD_NOTE" actionContext={{ objectTypeId: '0-3', objectId: 123456, }} variant="secondary" > Create note </CrmActionButton> ``` ### Create a task ```js <CrmActionButton actionType="ADD_TASK" actionContext={{ objectTypeId: '0-1', objectId: 123456, }} variant="secondary" > Create task </CrmActionButton> ``` ### Send a one-to-one-email ```js <CrmActionButton actionType="SEND_EMAIL" actionContext={{ objectTypeId: '0-3', objectId: 123456, }} variant="secondary" > Send email </CrmActionButton> ``` ### Schedule a meeting ```js <CrmActionButton actionType="SCHEDULE_MEETING" actionContext={{ objectTypeId: '0-1', objectId: 123456, }} > Schedule meeting </CrmActionButton> ``` ### Create an associated record ```js <CrmActionButton actionType="OPEN_RECORD_ASSOCIATION_FORM" actionContext={{ objectTypeId: '0-2', association: { objectTypeId: '0-1', objectId: 123456, }, }} > Create new record </CrmActionButton> ``` ### Navigate to an engagement ```js <CrmActionButton actionType="ENGAGEMENT_APP_LINK" actionContext={{ objectTypeId: '0-2', objectId: 2763710643, engagementId: 39361694368, }} variant="secondary" > Open note </CrmActionButton> ``` ### Navigate to a CRM record ```js <CrmActionButton actionType="RECORD_APP_LINK" actionContext={{ objectTypeId: '0-2', objectId: 2763710643, includeEschref: true, }} variant="secondary" > View company </CrmActionButton> ``` ### Navigate to a HubSpot page ```js <CrmActionButton actionType="PAGE_APP_LINK" actionContext={{ path: '/email/123456/analyze?emailType=followup-2', external: true, }} variant="secondary" > Open email dashboard </CrmActionButton> ``` ### Navigate to an external page ```js <CrmActionButton actionType="EXTERNAL_URL" actionContext={{ href: 'https://www.google.com', }} variant="secondary" > Open Google </CrmActionButton> ``` # CrmAssociationPivot @ Learn about the AssociationPivot component for use in UI extensions. $ reference & ui-components/crm-data-components --- # CrmAssociationPivot | UI components (BETA) 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/crm-setup/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. ```js import { CrmAssociationPivot } from '@hubspot/ui-extensions/crm'; const Extension = () => { return ( <CrmAssociationPivot objectTypeId="0-1" associationLabels={['CEO', 'CEO of subsidiary', 'Co-founder']} maxAssociations={10} preFilters={[ { operator: 'NOT_IN', property: 'dealstage', values: ['closedwon'], }, ]} sort={[ { columnName: 'createdate', direction: -1, }, ]} /> ); }; ``` | 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](/guides/api/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](/reference/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:<ul><li>`columnName`: the column to sort by.</li><li>`direction`: the direction to sort by. Can be either `1` (ascending) or `-1` (descending). By default, order is ascending.</li></ul> | ## Related components - [CrmAssociationPropertyList](/reference/ui-components/crm-data-components/crm-association-property-list) - [CrmAssociationTable](/reference/ui-components/crm-data-components/crm-association-table) - [CrmReport](/reference/ui-components/crm-data-components/crm-report) # CrmAssociationPropertyList @ Learn about the CrmAssociationPropertyList component for use in UI extensions. $ reference & ui-components/crm-data-components --- # CrmAssociationPropertyList | UI components (BETA) 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. ```js import { CrmAssociationPropertyList } from '@hubspot/ui-extensions/crm'; const Extension = () => { return ( <CrmAssociationPropertyList objectTypeId="0-2" properties={['name', 'domain', 'city', 'state']} filters={[ { operator: 'EQ', property: 'domain', value: 'meowmix.com', }, ]} /> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `properties` | Array | The list of properties to display from the associated record, up to 24. | | `objectTypeId` | String | The numeric ID of the type of associated object to display (e.g., `0-1` for contacts). See [complete list](/guides/api/crm/understanding-the-crm#object-type-id) of object IDs. | | `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](/reference/ui-components/crm-data-components/overview#filtering-data) for more information. | | `sort` | Array | The default sorting behavior for the table. In each sort object in the array, you'll specify the following:<ul><li>`columnName`: the column to sort by.</li><li>`direction`: the direction to sort by. Can be either `1` (ascending) or `-1` (descending). By default, order is ascending.</li></ul> | ## Related components - [CrmAssociationTable](/reference/ui-components/crm-data-components/crm-association-table) - [CrmReport](/reference/ui-components/crm-data-components/crm-report) - [CrmAssociationPivot](/reference/ui-components/crm-data-components/crm-association-pivot) # CrmAssociationStageTracker @ Learn about the CrmAssociationStageTracker component for use in UI extensions. $ reference & ui-components/crm-data-components --- # CrmAssociationStageTracker | UI components (BETA) 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. ```js import { CrmAssociationStageTracker } from '@hubspot/ui-extensions/crm'; const Extension = () => { return ( <CrmAssociationStageTracker objectTypeId="0-3" properties={['dealname', 'closed_won_reason', 'hs_acv', 'hs_arr']} showProperties={true} associationLabels={['Decision maker']} /> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `objectTypeId` Required | String | The numeric ID of the type of associated object to display data from (e.g., `0-1` for contacts). See [complete list](/guides/api/crm/understanding-the-crm#object-type-id) 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. | | `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](/reference/ui-components/crm-data-components/overview#filtering-data). | | `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:<ul><li>`columnName`: the column to sort by.</li><li>`direction`: the direction to sort by. Can be either `1` (ascending) or `-1` (descending). By default, order is ascending.</li></ul> | ## Related components - [CrmPropertyList](/reference/ui-components/crm-data-components/crm-property-list) - [CrmDataHighlight](/reference/ui-components/crm-data-components/crm-data-highlight) - [CrmReport](/reference/ui-components/crm-data-components/crm-report) - [CrmStageTracker](/reference/ui-components/crm-data-components/crm-stage-tracker) # CrmAssociationTable @ Learn about the CrmAssociationTable component for use in UI extensions. $ reference & ui-components/crm-data-components --- # CrmAssociationTable | UI components (BETA) 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. ```js import { CrmAssociationTable } from '@hubspot/ui-extensions/crm'; const Extension = () => { return ( <CrmAssociationTable objectTypeId="0-3" propertyColumns={['dealname', 'amount', 'description']} quickFilterProperties={['createdate']} pageSize={10} preFilters={[ { operator: 'EQ', property: 'dealstage', value: 'contractsent', }, ]} sort={[ { direction: 1, columnName: 'amount', }, ]} searchable={true} pagination={true} /> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `propertyColumns` | Array | The properties to display as table columns. | | `objectTypeId` | String | The numeric ID of the type of associated object to display (e.g., `0-1` for contacts). See [complete list](/guides/api/crm/understanding-the-crm#object-type-id) of object IDs. | | `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. | | `associationLabelFilter` | Boolean | When set to `false`, hides the "Association label" quick filter above the table. | | `searchable` | Boolean | When set to `false`, hides the search bar above the table. | | `pagination` | Boolean | When set to `false`, hides the pagination navigation below the table. | | `pageSize` | Number | The number of rows to include per page of results. Include the `pagination` property to enable users to navigate through returned results. | | `preFilters` | Array | Filters the data by specific values of the associated records. Review the [CRM data filter options](/reference/ui-components/crm-data-components/overview#filtering-data) for more information. | | `sort` | Array | The default sorting behavior for the table. In each sort object in the array, you'll specify the following:<ul><li>`columnName`: the column to sort by.</li><li>`direction`: the direction to sort by. Can be either `1` (ascending) or `-1` (descending). By default, order is ascending.</li></ul> | 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](/reference/ui-components/crm-data-components/crm-association-pivot) - [CrmAssociationPropertyList](/reference/ui-components/crm-data-components/crm-association-property-list) - [CrmReport](/reference/ui-components/crm-data-components/crm-report) # CrmDataHighlight @ Learn about the CrmDataHighlight component for use in UI extensions. $ reference & ui-components/crm-data-components --- # CrmDataHighlight | UI components (BETA) 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. ```js import { CrmDataHighlight } from '@hubspot/ui-extensions/crm'; const Extension = () => { return ( <CrmDataHighlight properties={[ 'createdate', 'lifecyclestage', 'hs_num_open_deals', 'hs_num_child_companies', ]} /> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `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. | | `objectTypeId` | String | The numeric ID of the type of associated object to display (e.g., `0-1` for contacts). See [complete list](/guides/api/crm/understanding-the-crm#object-type-id) of object IDs. | | `objectId` | String | The ID of the CRM record to display property data from. | ## Related components - [CrmPropertyList](/reference/ui-components/crm-data-components/crm-property-list) - [CrmStageTracker](/reference/ui-components/crm-data-components/crm-stage-tracker) - [CrmAssociationPivot](/reference/ui-components/crm-data-components/crm-association-pivot) # CrmPropertyList @ Learn about the CrmPropertyList component for use in UI extensions. $ reference & ui-components/crm-data-components --- # CrmPropertyList | UI components (BETA) 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. ```js import { CrmPropertyList } from '@hubspot/ui-extensions/crm'; const Extension = () => { return ( <CrmPropertyList properties={[ 'lastname', 'email', 'createdate', 'address', 'city', 'state', 'zip', ]} direction="row" /> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `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. | | `direction` | `'column'` (default) | `'row'` | The layout direction of the table. | | `objectTypeId` | String | The numeric ID of the type of associated object to display (e.g., `0-1` for contacts). See [complete list](/guides/api/crm/understanding-the-crm#object-type-id) of object IDs. | | `objectId` | String | The ID of the CRM record to display property data from. | ## Related components - [CrmDataHighlight](/reference/ui-components/crm-data-components/crm-data-highlight) - [CrmStageTracker](/reference/ui-components/crm-data-components/crm-stage-tracker) - [CrmAssociationPivot](/reference/ui-components/crm-data-components/crm-association-pivot) # CrmReport @ Learn about the CrmReport component for use in UI extensions. $ reference & ui-components/crm-data-components --- # CrmReport | UI components (BETA) 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. Report data will only display for users with [permissions to view reports](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#reports). ```js import { CrmReport } from '@hubspot/ui-extensions/crm'; const Extension = () => { return ( <CrmReport reportId="6339949" />; ); }; ``` | 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:<ul><li>`associations`: report will only include data from records associated with the currently displaying record.</li><li>`subject`: report will only include data from the currently displaying record. Will not include data from associated records.</li><li>`unfiltered`: report will display all data regardless of the currently displaying record and its associations.</li></ul> | ## Related components - [CrmAssociationPivot](/reference/ui-components/crm-data-components/crm-association-pivot) - [CrmAssociationPropertyList](/reference/ui-components/crm-data-components/crm-association-property-list) - [CrmAssociationTable](/reference/ui-components/crm-data-components/crm-association-table) # CrmStageTracker @ Learn about the CrmStageTracker component for use in UI extensions. $ reference & ui-components/crm-data-components --- # CrmStageTracker | UI components (BETA) 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. ```js import { CrmStageTracker } from '@hubspot/ui-extensions/crm'; const Extension = () => { return ( <CrmStageTracker objectId="13833764681" objectTypeId="0-3" properties={['dealname', 'amount']} /> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `objectTypeId` | String | The numeric ID of the type of associated object to display (e.g., `0-1` for contacts. See [complete list](/guides/api/crm/understanding-the-crm#object-type-id) of object IDs. | | `objectId` | String | The ID of the CRM record to display property data from. | | `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](/reference/ui-components/crm-data-components/crm-property-list) - [CrmDataHighlight](/reference/ui-components/crm-data-components/crm-data-highlight) - [CrmReport](/reference/ui-components/crm-data-components/crm-report) # CrmStatistics @ Learn about the CrmStatistics component for use in UI extensions. $ reference & ui-components/crm-data-components --- # CrmStatistics | UI components (BETA) 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. ```js import { CrmStatistics } from '@hubspot/ui-extensions/crm'; const Extension = () => { return ( <CrmStatistics objectTypeId="0-3" statistics={[ { label: 'Average Deal Amount', statisticType: 'AVG', propertyName: 'amount', }, { label: '50th Percentile Deal Amount', statisticType: 'PERCENTILES', propertyName: 'amount', percentile: 50, }, { label: 'Time Left for Most Important Upcoming Deal', statisticType: 'MIN', propertyName: 'days_to_close', // The filters below narrow the fetched // deals by the following criteria: // - Amount must be >= 10,000 // - Deal must not be closed filterGroups: [ { filters: [ { operator: 'GTE', property: 'amount', value: 10000, }, { operator: 'EQ', property: 'hs_is_closed', value: 'false', }, ], }, ], }, ]} /> ); }; ``` | 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](/guides/api/crm/understanding-the-crm#object-type-id) of object IDs. | | `statistics` [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` | | `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](/guides/api/crm/search#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](/reference/ui-components/crm-data-components/crm-report) - [CrmAssociationTable](/reference/ui-components/crm-data-components/crm-association-table) - [CrmAssociationPropertyList](/reference/ui-components/crm-data-components/crm-association-property-list) # CRM data components (BETA) @ Learn about the CRM data component for use in UI extensions. $ reference & ui-components/crm-data-components --- # CRM data components (BETA) 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`. ```js import { CrmAssociationPivot, CrmReport } from '@hubspot/ui-extensions/crm'; ``` ## Available components - [CrmAssociationPivot](/reference/ui-components/crm-data-components/crm-association-pivot) - [CrmAssociationPropertyList](/reference/ui-components/crm-data-components/crm-association-property-list) - [CrmAssociationTable](/reference/ui-components/crm-data-components/crm-association-table) - [CrmDataHighlight](/reference/ui-components/crm-data-components/crm-data-highlight) - [CrmPropertyList](/reference/ui-components/crm-data-components/crm-property-list) - [CrmReport](/reference/ui-components/crm-data-components/crm-report) - [CrmStageTracker](/reference/ui-components/crm-data-components/crm-stage-tracker) - [CrmStatistics](/reference/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. ```js import { CrmAssociationPivot, CrmReport } from '@hubspot/ui-extensions/crm'; const Extension = () => { return ( <CrmAssociationPivot objectTypeId="0-1" associationLabels={['CEO', 'Co-founder']} maxAssociations={10} preFilters={[ { operator: 'NOT_IN', property: 'dealstage', values: ['closedwon'], }, ]} /> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `operator` | `EQ` |`NEQ` | `LT` | `LTE` | `GT` | `GTE` | `BETWEEN` | `IN` | `NOT_IN` | `HAS_PROPTERTY` | `NOT_HAS_PROPERTY` | The filter's operator (e.g. `IN`). Can be one of:<ul><li>`EQ`: is equal to `value`.</li><li>`NEQ`: is not equal to `value`.</li><li>`LT`: is less than `value`.</li><li>`LTE`: is less than or equal to `value`.</li><li>`GT`: is greater than `value`.</li><li>`GTE`: is greater than or equal to `value`.</li><li>`BETWEEN`: is within the specified range between `value` and `highValue`.</li><li>`IN`: is included in the specified `values` array. This operator is case-sensitive, so inputted values must be in lowercase. </li><li>`NOT_IN`: is not included in the specified `values` array.</li><li>`HAS_PROPERTY`: has a value for the specified property.</li><li>`NOT_HAS_PROPERTY`: does not have a value for the specified property.</li></ul>Learn more about [filtering CRM searches](/guides/api/crm/search#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`. | | `highValue` | String | number | The upper value to filter by when using an operator that requires a range, such as `BETWEEN`. | # Figma Design Kit | UI extensions @ Learn about using the Figma Design Kit for designing UI extensions (BETA). $ reference & ui-components/design --- # Figma Design Kit (BETA) The Figma Design Kit is a visual representation of the [UI components](/reference/ui-components/overview) you can include in React-based app cards in [UI extensions](/guides/crm/ui-extensions/overview). Using this kit, you can create more detailed designs to plan out UI extensions, and can be particularly helpful when needing to share demos with stakeholders or keep teams aligned during the development process. Along with containing visual examples of UI components, the kit also contains high-level UI extension design information, such as a breakdown of app card anatomy and examples of available locations. Note that HubSpot will update this kit over time as new components are added, but the [UI component documentation](/reference/ui-components/overview) will be the most up to date source of truth for available components. Be sure to [check the changelog](#using-the-kit) for any recent or upcoming additions. Learn more about using UI kits in [Figma's documentation](https://help.figma.com/hc/en-us/articles/24037724065943-Start-designing-with-UI-kits). The Figma Design Kit is in beta and is therefore subject to [HubSpot's developer beta terms](https://legal.hubspot.com/hubspot-beta-terms). The contents of the kit may change during this period. ## Access the kit Click the button below to access the Figma Design Kit. After accessing the kit, it's recommended to open the kit in your Figma desktop app, then duplicate it to your drafts: - In the left sidebar, click **Extensibility Components**. - Select **Duplicate to your drafts**. - A copy will be created to your drafts, which you can then use as needed. As updates will be periodically released, you'll need to check the source kit and duplicate to your drafts again to access new components. Learn more about [copying UI kits](https://help.figma.com/hc/en-us/articles/24037724065943-Start-designing-with-UI-kits#h_01J0Y2QYS4GKV112NWZHD6019J). ## Using the kit In the _Pages_ section of the left sidebar, you'll find a _Documentation_ section which contains the following pages: - **Introduction:** a brief introduction to what the kit is and what it should be used for, along with links to documentation and a feedback form. - **Card Anatomy & Location Wires:** high-level breakdowns of app card anatomy, along with wireframes for the different locations you can build UI extensions for. - **Changelog:** recent additions and changes, along with upcoming additions. Each entry will include a link to relevant documentation when available, along with the release date. In the _Components_ section, you'll find the currently available design pages for UI components. Component pages will include a description of the component along with visual examples of different states and variants. # Manage UI extension layout (BETA) @ Learn how to customize UI extension layout using Flex and Box components. $ reference & ui-components/design --- # Manage UI extension layout (BETA) By default, UI extension components will arrange themselves based on their content, order, and any layout-related props included with the component, such as the `width` prop for images. But you can further configure your extension's layout using the `Flex` and `Box` components, which are based on the [CSS flexbox layout](https://css-tricks.com/snippets/css/a-guide-to-flexbox/). `Flex` and `Box` are both used as wrappers around other components. `Flex` can be used on its own, while `Box` can be used as a wrapper around `Flex` child items to fine tune spacing of individual components. Below, learn more about each component along with usage examples. Check out HubSpot's [Managing layout: Flex and Box sample project](/guides/crm/ui-extensions/sample-extensions/overview#manage-layouts-flex-and-box) to view a full example of using `Flex` and `Box`. ## Flex The [`Flex` component](/reference/ui-components/overview#layout-flex) renders an empty `div` container set to `display=flex`. When wrapped around other components, this enables those child components to be arranged using props. Below are the available `Flex` props. To review all `Flex` prop definitions, check out the [components reference guide](/reference/ui-components/overview#layout-flex). | Prop | Values | Description | | --- | --- | --- | | `direction` | <ul><li>`row` (default)</li><li>`column`</li></ul> | Arranges components horizontally or vertically by setting the main axis. | | `justify` | <ul><li>`center`</li><li>`start` (default)</li><li>`end`</li><li>`around`</li><li>`between`</li></ul> | Distributes components along the main axis using the available free space. | | `align` | <ul><li>`start`</li><li>`center`</li><li>`baseline`</li><li>`end`</li><li>`stretch` (default)</li></ul> | Distributes components along the cross-axis using the available free space. | | `alignSelf` | <ul><li>`start`</li><li>`center`</li><li>`baseline`</li><li>`end`</li><li>`stretch`</li></ul> | Distributes a child component along the cross-axis using the available free space. Use this prop for nested child `Flex` and `Box` components to align them differently from other child components in the `Flex` group. | | `wrap` | <ul><li>`wrap`</li><li>`nowrap`</li></ul> | Whether components will wrap instead of trying to fit on one line. | | `gap` | <ul><li>`flush` (default)</li><li>`extra-small`</li><li>`small`</li><li>`medium`</li><li>`large`</li><li>`extra-large`</li></ul> | Sets the spacing between components. | Review the examples below to see how the [`Flex` component](/reference/ui-components/overview#flex) can be used to arrange components in various ways. ### 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. To arrange components horizontally and evenly spaced: ```js <Flex direction={'row'} justify={'between'}> <Tile>Tile 1</Tile> <Tile>Tile 2</Tile> <Tile>Tile 3</Tile> </Flex> ``` To arrange components horizontally, evenly spaced, and with equal spacing on the left and right margins: ```js <Flex direction={'row'} justify={'around'}> <Tile>Tile 1</Tile> <Tile>Tile 2</Tile> <Tile>Tile 3</Tile> </Flex> ``` To arrange components horizontally at the end of the extension container: ```js <Flex direction={'row'} justify={'end'}> <Tile>Tile 1</Tile> <Tile>Tile 2</Tile> <Tile>Tile 3</Tile> </Flex> ``` To arrange components horizontally at the center of the extension container: ```js <Flex direction={'row'} justify={'center'}> <Tile>Tile 1</Tile> <Tile>Tile 2</Tile> <Tile>Tile 3</Tile> </Flex> ``` To arrange components horizontally at the start of the extension container: ```js <Flex direction={'row'} justify={'start'}> <Tile>Tile 1</Tile> <Tile>Tile 2</Tile> <Tile>Tile 3</Tile> </Flex> ``` ### 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. ```js <Flex direction={'row'} justify={'between'} wrap={'wrap'} gap={'medium'}> <Tile>One</Tile> <Tile>Two</Tile> <Tile>Three</Tile> <Tile>Four</Tile> <Tile>Five</Tile> <Tile>Six</Tile> <Tile>Seven</Tile> <Tile>Eight</Tile> </Flex> ``` ### 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. To arrange components vertically at the start of the extension container: ```js <Flex direction={'column'} align={'start'}> <Tile>Tile 1</Tile> <Tile>Tile 2</Tile> <Tile>Tile 3</Tile> </Flex> ``` To arrange components vertically at the center of the extension container: ```js <Flex direction={'column'} align={'center'}> <Tile>Tile 1</Tile> <Tile>Tile 2</Tile> <Tile>Tile 3</Tile> </Flex> ``` To arrange components vertically at the end of the extension container: ```js <Flex direction={'column'} align={'end'}> <Tile>Tile 1</Tile> <Tile>Tile 2</Tile> <Tile>Tile 3</Tile> </Flex> ``` ### 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. ```js <Flex direction={'row'} justify={'start'} gap={'flush' | 'extra-small' | 'small' | 'medium' | 'large' | 'extra-large'} > <Tile>Tile 1</Tile> <Tile>Tile 2</Tile> <Tile>Tile 3</Tile> </Flex> ``` ### 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. ```js <Flex direction={'row'} justify={'end'} wrap={'wrap'} gap={'small'}> <Tile>Left</Tile> <Tile>Right</Tile> <Flex direction={'column'}> <Tile>Bottom</Tile> </Flex> </Flex> ``` ## Box When wrapping components with `Flex`, you can further configure individual component layout by wrapping a child of `Flex` in a [`Box` component](/reference/ui-components/overview#layout-box). This component supports the following props. To review all `Box` prop definitions, check out the [components reference guide](/reference/ui-components/overview#layout-box). | Prop | Values | Description | | --- | --- | --- | | `alignSelf` | <ul><li>`start`</li><li>`center`</li><li>`baseline`</li><li>`end`</li><li>`stretch`</li></ul> | Distributes a child component along the cross-axis using the available free space. Use this prop for nested child `Flex` and `Box` components to align them differently from other child components in the `Flex` group. | | `flex` | <ul><li>number value</li><li>`initial` (default)</li><li>`auto`</li><li>`none`</li></ul> | Distributes components based on the available empty space around them. | 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. For example, the four tiles below take up an increasing amount of space based on their `flex` values. ```js <Flex direction={'row'} justify={'start'} gap={'small'}> <Box flex={1}> <Tile>flex = 1</Tile> </Box> <Box flex={2}> <Tile>flex = 2</Tile> </Box> <Box flex={3}> <Tile>flex = 3</Tile> </Box> <Box flex={4}> <Tile>flex = 4</Tile> </Box> </Flex> ``` When using `Box`, you only need to wrap 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. ```js <Flex direction={'row'} justify={'start'} gap={'small'}> <Box flex={1}> <Tile>Tile 1</Tile> </Box> <Tile>Tile 2</Tile> <Tile>Tile 3</Tile> </Flex> ``` When setting a `flex` value for only one `Box`, you can use any number. This is because any number on its own will result in all available space being assigned to that one component. You can also use the `alignSelf` prop to override alignment rules for individual `Box` components. ```js <Flex direction={'column'} gap={'small'} align={'start'}> <Box alignSelf={'end'}> <Tile>Top right</Tile> </Box> <Box alignSelf={'center'}> <Tile>Middle</Tile> </Box> <Tile>Bottom left</Tile> </Flex> ``` # Button design patterns @ Learn about the design patterns HubSpot recommends for using buttons in app cards. $ reference & ui-components/design/patterns --- # Buttons Include `Button` components to enable users to perform actions in app cards, such as submitting a form, sending data to an external system, or deleting data. Follow the design patterns below to keep your app card consistent with HubSpot's designs. To learn more about implementation, check out the [Button component reference documentation](/reference/ui-components/standard-components/button). ## Patterns <CardsRow> <Card> <h3>Variants</h3> <ul> <li> Only include one `primary` button per surface (e.g., one per panel, one per modal). The button should be reserved for the main action that a user can perform. </li> <li> To avoid confusion, don’t pair `destructive` variant buttons with `primary` buttons. Limit button pairings to either `primary` and `secondary` or `destructive` and `secondary`. </li> <li> In general, use the default button size. You should only be sizing down if it’s necessary for the space that the button is being placed in (e.g., in a table with narrow rows). </li> </ul> </Card> <Card> <h3>Alignment</h3> <ul> <li> Always position buttons at the bottom of [Form](/reference/ui-components/standard-components/form), [Modal](/reference/ui-components/standard-components/modal) and [Panel](/reference/ui-components/standard-components/panel) components. </li> <li> Buttons should always be aligned to the left. When pairing `primary` and `secondary` variant buttons, the `primary` button should appear as the leftmost button. The only time you should align a button to the right of a container is for buttons that navigate through multi-step forms, as this is where the user will intuitively click. </li> </ul> </Card> </CardsRow> ## Examples The examples below are taken from HubSpot's [design patterns example app](https://github.com/HubSpot/ui-extensions-examples/tree/main/design-patterns) and [Figma design kit](/reference/ui-components/design/figma-design-kit). ### Buttons in modals <IFrame width="645" height="450" src="https://embed.figma.com/design/jJ1Wq7Q3O0iLnbSZTyYjS6/HubSpot-Extensibility-Design-Kit?node-id=1422-2054&embed-host=share&footer=false&page-selector=false" allowfullscreen ></IFrame> [View code on GitHub](https://github.com/HubSpot/ui-extensions-examples/blob/main/design-patterns/src/app/extensions/components/ModalExample.tsx) ### Buttons in panels <IFrame width="645" height="450" src="https://embed.figma.com/design/jJ1Wq7Q3O0iLnbSZTyYjS6/HubSpot-Extensibility-Design-Kit?node-id=635-445&embed-host=share&footer=false&page-selector=false" allowfullscreen ></IFrame> [View code on GitHub](https://github.com/HubSpot/ui-extensions-examples/blob/main/design-patterns/src/app/extensions/components/PanelExample.tsx) # Form design patterns @ Learn about the design patterns HubSpot recommends for using forms in app cards. $ reference & ui-components/design/patterns --- # Forms Use forms to enable users to submit data to HubSpot or an external system. For example, include a form in an app card to submit customer information to an external database, or to place a product order on behalf of a customer. Follow the design patterns below to keep your app card consistent with HubSpot's designs. To learn more about implementation, check out the [Form component reference documentation](/reference/ui-components/standard-components/form). ## Patterns <CardsRow> <Card> <h3>Field organization</h3> <ul> <li> Form fields should be stacked vertically, as most people are used to a vertical scroll and may not have wide enough screens to fully display horizontally stacked inputs. A good principle to follow is to include only one input per line. </li> <li> If stacking form fields vertically would make the app card too long, break the form up into multiple steps so that the content isn't overwhelming. </li> </ul> </Card> <Card> <h3>Form buttons</h3> <ul> <li> Always include a submit button with each form. </li> <li> For multi-step forms, include a "Next" button at the bottom of each step to guide users to the next step, with the last step ending with a submit button. See the [multi-step form in panels example](#multi-step-forms) below. </li> </ul> </Card> </CardsRow> ### Examples The examples below are taken from HubSpot's [design patterns example app](https://github.com/HubSpot/ui-extensions-examples/tree/main/design-patterns) and [Figma design kit](/reference/ui-components/design/figma-design-kit). ### Forms in modals <IFrame width="645" height="450" src="https://embed.figma.com/design/jJ1Wq7Q3O0iLnbSZTyYjS6/HubSpot-Extensibility-Design-Kit?node-id=723-718&embed-host=share&footer=false&page-selector=false" allowfullscreen ></IFrame> [View code on GitHub](https://github.com/HubSpot/ui-extensions-examples/blob/main/design-patterns/src/app/extensions/FormModal.tsx) ### Multi-step forms <IFrame width="645" height="450" src="https://embed.figma.com/design/jJ1Wq7Q3O0iLnbSZTyYjS6/HubSpot-Extensibility-Design-Kit?node-id=723-1100&embed-host=share&footer=false&page-selector=false" allowfullscreen ></IFrame> [View code on GitHub](https://github.com/HubSpot/ui-extensions-examples/blob/main/design-patterns/src/app/extensions/FormMultistep.tsx) # Table design patterns @ Learn about the design patterns HubSpot recommends for using tables in app cards. $ reference & ui-components/design/patterns --- # Tables Use `Table` components to render tables in app cards for displaying sets of data. Tables can be useful for when your app card needs to display a set of raw data that is too detailed or complicated to be described with text only, or when a user needs to compare sets of data. Follow the design patterns below to keep your app card consistent with HubSpot's designs. To learn more about implementation, check out the [Table component reference documentation](/reference/ui-components/standard-components/table). ## Patterns <CardsRow> <Card> <h3>Buttons</h3> <p> A row should only contain a button if there's an action directly associated with the row. If needed, use the `xs` button size and `secondary` variant. Keep table row buttons right-aligned to the information they are relevant to, enabling the user to read the information from left to right, then decide to take action. </p> </Card> <Card> <h3>Content</h3> <p> Don't overwhelm users with too much information. Long form content should instead be contained in a `Panel` that can be opened from the row to prevent information from crowding the table itself. </p> </Card> </CardsRow> ## Example The example below is taken from HubSpot's [design patterns example app](https://github.com/HubSpot/ui-extensions-examples/tree/main/design-patterns) and [Figma design kit](/reference/ui-components/design/figma-design-kit). <IFrame width="645" height="450" src="https://embed.figma.com/design/jJ1Wq7Q3O0iLnbSZTyYjS6/HubSpot-Extensibility-Design-Kit?node-id=723-1101&embed-host=share&footer=false&page-selector=false" allowfullscreen ></IFrame> [View code on GitHub](https://github.com/HubSpot/ui-extensions-examples/blob/main/design-patterns/src/app/extensions/Table.jsx) # UI extension components @ Learn about the components that you can use to build UI extensions. $ reference & ui-components --- # UI components overview (BETA) When building a [UI extension](/guides/crm/ui-extensions/create), you can include any number of HubSpot-provided reusable components. These components range from simple text fields to out-of-the-box CRM object reports, and each component offers customization options through properties. To access the latest components, ensure that you've installed the latest npm package by running `npm i @hubspot/ui-extensions` in the `extensions` directory. Components are imported at the top of your `tsx` or `jsx` extension file. Depending on the type of component, you'll need to import them from one of two SDK directories. - [Standard components](#standard-components) are imported from `'@hubspot/ui-extensions'` - [CRM data](#crm-data-components) and [CRM action components](#crm-action-components) are imported from `'@hubspot/ui-extensions/crm'` ```js import { Alert, Text } from '@hubspot/ui-extensions'; import { CrmAssociationPivot, CrmActionLink } from '@hubspot/ui-extensions/crm'; ``` For a visual library of these UI components, check out HubSpot's [Figma Design Kit](/reference/ui-components/figma-design-kit). ## Standard components Standard components are components that can be used for both internal and external data. These components do not fetch data on their own, but are more flexible in their implementation. These components are imported from `'@hubspot/ui-extensions'`. | <span style={{ display: 'inline-block', width: 200 }}>Component</span> | Description | | --- | --- | | [Accordion](/reference/ui-components/standard-components/accordion) | A collapsable accordion section that can contain other components. | | [Alert](/reference/ui-components/standard-components/alert) | Alerts for indicating statuses and action outcomes, such as success and error messages. | | [BarChart](/reference/ui-components/standard-components/charts/bar-chart) | A bar chart used for visualizing categorical data. | | [Box](/reference/ui-components/standard-components/box) | Component used for layout management. Can be used with [Flex](/reference/ui-components/standard-components/flex). Learn more about [managing extension layout](/reference/ui-components/manage-ui-extension-layout). | | [Button](/reference/ui-components/standard-components/button) | Buttons that enable users to perform actions, such as sending or retrieving data. | | [Button row](/reference/ui-components/standard-components/button-row) | For rendering multiple buttons. | | [Checkbox](/reference/ui-components/standard-components/checkbox) | A single checkbox input. To display multiple checkboxes, use [ToggleGroup](/reference/ui-components/standard-components/toggle-group) instead. | | [DateInput](/reference/ui-components/standard-components/date-input) | A field that enables users to select a date. | | [DescriptionList](/reference/ui-components/standard-components/description-list) | Displays pairs of custom labels and values, similar to how HubSpot properties appear in the left sidebar of CRM records. | | [Divider](/reference/ui-components/standard-components/divider) | A horizontal line for separating components within an extension. | | [Dropdown](/reference/ui-components/standard-components/dropdown) | A dropdown menu for selecting values, styled as either buttons or hyperlinks. | | [EmptyState](/reference/ui-components/standard-components/empty-state) | A labeled illustration to indicate a component without content. | | [ErrorState](/reference/ui-components/standard-components/error-state) | Labeled illustrations to indicate errors. | | [Flex](/reference/ui-components/standard-components/flex) | Wraps other components in an empty `div` set to `display=flex`. Use this component and [Box](/reference/ui-components/standard-components/box) to [manage extension layout](/reference/ui-components/manage-ui-extension-layout). | | [Form](/reference/ui-components/standard-components/form) | A form for submitting data, which can contain other related components such as [Input](/reference/ui-components/standard-components/input), [Select](/reference/ui-components/standard-components/select), and [Button](/reference/ui-components/standard-components/button). | | [Heading](/reference/ui-components/standard-components/heading) | Renders large text for titles. | | [Icon](/reference/ui-components/standard-components/icon) | Renders a visual icon within other components. | | [Illustration](/reference/ui-components/standard-components/illustration) | Renders an image from HubSpot's illustration library. | | [Image](/reference/ui-components/standard-components/image) | An image, primarily used for adding logos or other visual brand identity assets, or to accentuate other extension content. | | [Input](/reference/ui-components/standard-components/input) | A text input field where users can enter custom text values. Primarily used within [Form](/reference/ui-components/standard-components/form) components. | | [LineChart](/reference/ui-components/standard-components/charts/line-chart) | A line chart used for visualizing time series plots or trend data. | | [Link](/reference/ui-components/standard-components/link) | A clickable hyperlink for navigating to external and HubSpot app pages, or for triggering functions. | | [List](/reference/ui-components/standard-components/list) | An ordered or unordered list of items. | | [LoadingButton](/reference/ui-components/standard-components/loading-button) | Similar to a [Button](/reference/ui-components/standard-components/button) component with additional loading state options. | | [LoadingSpinner](/reference/ui-components/standard-components/loading-spinner) | A visual indicator that the card is loading or processing. | | [Modal](/reference/ui-components/standard-components/modal) | A pop-up overlay containing other components. Useful for short messages and action confirmation boxes. Can be opened with [Button](/reference/ui-components/standard-components/button), [LoadingButton](/reference/ui-components/standard-components/loading-button), [Link](/reference/ui-components/standard-components/link), [Tag](/reference/ui-components/standard-components/tag), and [Image](/reference/ui-components/standard-components/image) components. | | [MultiSelect](/reference/ui-components/standard-components/multi-select) | A dropdown select field where users can select multiple values. To allow only one value to be selected, use [Select](/reference/ui-components/standard-components/select) instead. Primarily used within [Form](/reference/ui-components/standard-components/form) components. | | [NumberInput](/reference/ui-components/standard-components/number-input) | A number input field. Primarily used within [Form](/reference/ui-components/standard-components/form) components. | | [Panel](/reference/ui-components/standard-components/panel) | A panel that opens on the right side of the page, containing other components. Can be opened with [Button](/reference/ui-components/standard-components/button), [LoadingButton](/reference/ui-components/standard-components/loading-button), [Link](/reference/ui-components/standard-components/link), [Tag](/reference/ui-components/standard-components/tag), and [Image](/reference/ui-components/standard-components/image) components. | | [ProgressBar](/reference/ui-components/standard-components/progress-bar) | A visual representation of data in motion toward a positive or negative target. Can display both numbers and percentages. | | [RadioButton](/reference/ui-components/standard-components/radio-button) | A radio select button. If you want to include more than two radio buttons, or are building a [Form](/reference/ui-components/standard-components/form), it's recommended to use [ToggleGroup](/reference/ui-components/standard-components/toggle-group) instead. | | [Select](/reference/ui-components/standard-components/select) | A dropdown select field where a user can select a single value. To allow selecting multiple values, use [MultiSelect](/reference/ui-components/standard-components/multi-select) instead. | | [Statistics](/reference/ui-components/standard-components/statistics) | A visual spotlight of one or more data points. Includes numeric values and trend indicators (increasing/decreasing percentage). | | [StatusTag](/reference/ui-components/standard-components/status-tag) | A visual indicator to display the current status of an item. | | [StepIndicator](/reference/ui-components/standard-components/step-indicator) | A visual indicator to describe the progress within a multi-step process. | | [StepperInput](/reference/ui-components/standard-components/stepper-input) | Similar to the [NumberInput](/reference/ui-components/standard-components/number-input) component, but this field enables users to increase or decrease the value by a set amount. | | [Table](/reference/ui-components/standard-components/table) | Displays data in columns and rows. Tables can be paginated and sortable. | | [Tag](/reference/ui-components/standard-components/tag) | Colored labels for categorizing information or other components. Can be static or clickable for triggering functions. | | [Text](/reference/ui-components/standard-components/text) | Renders text with formatting options. | | [Tile](/reference/ui-components/standard-components/tile) | A rectangular, bordered container for creating groups of related components. | | [TextArea](/reference/ui-components/standard-components/text-area) | Similar to [Text](/reference/ui-components/standard-components/text), but for longer sets of text. Includes props for configuring field size, maximum characters, and resizeability. | | [Toggle](/reference/ui-components/standard-components/toggle) | A boolean toggle switch that can be configured with sizing and label options. | | [ToggleGroup](/reference/ui-components/standard-components/toggle-group) | A list of selectable checkboxes or radio buttons. | ## CRM data components 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. Learn more about [CRM data components](/reference/ui-components/crm-data-components/overview). These components are imported from `@hubspot/ui-extensions/crm`. | <span style={{ display: 'inline-block', width: 200 }}>Component</span> | Description | | --- | --- | | [CrmAssociationPivot](/reference/ui-components/crm-data-components/crm-association-pivot) | A list of records associated with the currently displaying record, organized by their association label. | | [CrmAssociationPropertyList](/reference/ui-components/crm-data-components/crm-association-property-list) | An editable list of CRM properties belonging to a record associated with the currently displaying record. | | [CrmAssociationStageTracker](/reference/ui-components/crm-data-components/crm-association-stage-tracker) | A lifecycle or pipeline stage progress bar that displays data from associated records. Can include a list of properties which can be edited inline. | | [CrmAssociationTable](/reference/ui-components/crm-data-components/crm-association-table) | A table of records associated with the currently displaying record. | | [CrmDataHighlight](/reference/ui-components/crm-data-components/crm-data-highlight) | A list of CRM properties belonging to the currently displaying record or another specified record. | | [CrmPropertyList](/reference/ui-components/crm-data-components/crm-property-list) | An editable list of CRM properties belonging to the currently displaying record or another specified record. | | [CrmReport](/reference/ui-components/crm-data-components/crm-report) | Display an existing single object report. Includes filtering options, or can display all report data unfiltered. | | [CrmStageTracker](/reference/ui-components/crm-data-components/crm-stage-tracker) | A lifecycle or pipeline stage progress bar with a list of properties. | | [CrmStatistics](/reference/ui-components/crm-data-components/crm-statistics) | Display summaries of data calculated from the currently displaying record's associations. For example, the average revenue across a contact's associated companies. | ## CRM action components 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. Learn more about [CRM action components](/reference/ui-components/crm-action-components/overview). CRM action components are imported from `@hubspot/ui-extensions/crm`. | <span style={{ display: 'inline-block', width: 200 }}>Component</span> | Description | | --- | --- | | [CrmActionButton](/reference/ui-components/crm-action-components/crm-action-button) | A button that can execute a built-in set of CRM actions. | | [CrmActionLink](/reference/ui-components/crm-action-components/crm-action-link) | A clickable link that can execute a built-in set of CRM actions. | | [CrmCardActions](/reference/ui-components/crm-action-components/crm-card-actions) | Smaller standalone or dropdown menu buttons that can contain multiple CRM actions. | # Accordion @ Learn about the accordion component for use in UI extensions. $ reference & ui-components/standard-components --- # Accordion | UI components (BETA) The `Accordion` component renders an expandable and collapsable section that can contain other components. This component can be helpful for saving space and breaking up extension content. ```js import { Accordion, Text } from '@hubspot/ui-extensions'; const Extension = () => { return ( <> <Accordion title="Item One" defaultOpen={true}> <Text>Call me Ishmael. Some years ago—never mind how long precisely—having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world.</Text> </Accordion> <Accordion title="Item Two"> <Text>Second inner text</Text> </Accordion> </> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `title` | String | The accordion's title text. | | `defaultOpen` | Boolean | When set to `true`, the accordion will be open on initial page load. The `open` prop takes precedence over this prop. | | `disabled` | Boolean | When set to `true`, the accordion's state cannot be changed. Set to `false` by default. | | `open` | Boolean | Controls the accordion's open state programmatically. When set to `true`, the accordion will open. Takes precedence over `defaultOpen`. | | `onClick` | `() => void` | A function that will be invoked with the accordion title is clicked. This function receives no arguments and its returned value is ignored. | | `size` | `'xs'`, `'extra-small'` | `'sm'`, `'small'` | `'med'`, `'medium'` (default) | The size of the accordion title. | ## Related components - [Box](/reference/ui-components/standard-components/box) - [Divider](/reference/ui-components/standard-components/divider) - [Heading](/reference/ui-components/standard-components/heading) # Alert @ Learn about the Alert component for use in UI extensions. $ reference & ui-components/standard-components --- # Alert | UI components (BETA) Use the `Alert` component to render an alert within a card. Use this component to give usage guidance, notify users of action results, or warn them about potential issues or failures. Alerts can be placed in components statically or triggered dynamically as the result of an action. If you want to render an alert banner at the top of the page, Learn more about the [AddAlert method](/guides/crm/ui-extensions/sdk#display-alert-banners). 1. **Title:** the alert's bolded title text which should summarize its intent or an action outcome. 2. **Body:** the descriptive text that follows the title, which should provide the user with the necessary information to proceed. 3. **Variant:** the color of the alert. Learn more about when to use each variant below. ```js import { Alert } from '@hubspot/ui-extensions'; const Extension = () => { return ( <> <Alert title="Important Info" variant="info"> This is an informative message. </Alert> <Alert title="Tip" variant="tip"> Double check fields before submitting. </Alert> <Alert title="Success"variant="success"> Operation completed successfully. </Alert> <Alert title="Warning" variant="warning" > Proceed with caution. </Alert> <Alert title="Danger" variant="danger" > This action cannot be undone. Be careful. </Alert> </> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `title` | String | The bolded text of the alert. | | `variant` | `'info'` (default) | `'tip'` | `'success'` | `'warning'` | `'danger'` | The color of the alert. See below for more information about variants. | ## Variants There are five alert types, which can be set using the `variant` prop. - `info`**:** a blue alert for general messages to inform the user. - `tip`: a white alert to provide guidance and tips. - `success`**:** a green alert to indicate the successful completion of a task or to convey a positive CRM record status. - `warning`**:** a yellow alert for general warnings related to the performance of the system or the status of the CRM record. - `danger`**:** a red alert indicating an error, the degradation of a system, or a negative CRM record status. You should not use this alert type for anything other than errors and negative statuses. ## Usage examples - Use an `info` alert to add instructions to a custom card, such as "Fill out this form using the contact's company information." - Use a `success` alert when a user successfully submits a form in the UI extension, or to convey that a contact qualifies for enrollment in a promotional offering. - Use a `warning` alert to call attention to a contact's upcoming renewal date. - Use a `danger` alert to notify the user of a form submission error or to signify that a high-urgency ticket has lapsed. ## Guidelines - **DO:** use alert text that clearly communicates to the user what they need to know. - **DO:** make alert text actionable, especially for error alerts which should provide a clear path to resolution. - **DO:** use proper punctuation, such as periods and question marks at the end of the descriptive text. - **DO:** set the alert as the first component in the extension when possible for visibility. Extensions with alerts should also be placed at the top of the CRM record by a HubSpot admin. Learn more about [customizing CRM record tabs](https://knowledge.hubspot.com/crm-setup/view-and-customize-record-overviews). - **DON'T:** use exclamation points in warning or error alerts. Exclamation points should only be used for short, positive messages (e.g. "Great!" or "Well done!"). ## Related components - [Display alert banners outside of a card](/guides/crm/ui-extensions/sdk#display-alert-banners) - [EmptyState](/reference/ui-components/standard-components/empty-state) - [ErrorState](/reference/ui-components/standard-components/error-state) - [LoadingSpinner](/reference/ui-components/standard-components/loading-spinner) # Box @ Learn about the Box component for configuring UI extension layout. $ reference & ui-components/standard-components --- # Box | UI components (BETA) The `Box` component renders an empty `div` container for fine tuning the spacing of components. Commonly used with the [Flex](/reference/ui-components/standard-components/flex) component. To see an example of how `Flex` and `Box` can be used for layout, check out HubSpot's [Manage layouts: Flex and Box sample project](/guides/crm/ui-extensions/sample-extensions/overview#managing-layouts-flex-and-box). ```js import { Flex, Box, Tile } from '@hubspot/ui-extensions'; const Extension = () => { return ( <Flex direction={'row'} justify={'start'} gap={'small'}> <Box flex={1}> <Tile>flex = 1</Tile> </Box> <Box flex={2}> <Tile>flex = 2</Tile> </Box> <Box flex={3}> <Tile>flex = 3</Tile> </Box> <Box flex={4}> <Tile>flex = 4</Tile> </Box> </Flex> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `alignSelf` | `'start'` | `'center'` | `'end'` | `'baseline'` | `'stretch'` | Distributes a child component along the cross-axis using the available free space. Use this prop for nested `Box` components to align them differently from other child components in the `Flex` group. | | `flex` | `'initial'` | `'auto'` (default) | `'none'` | Number | Distributes components based on the available empty space around them. Options are as follows:<ul><li>`initial`: The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. </li><li>`auto`: The item is sized according to its width and height properties, but grows to absorb any extra free space in the flex container, and shrinks to its minimum size to fit the container.</li><li>`none`: The item is sized according to its width and height properties. It is fully inflexible: it neither shrinks nor grows in relation to the flex container.</li><li>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.</li></ul> | 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. ```js <Flex direction={'row'} justify={'start'} gap={'small'}> <Box flex={1}> <Tile>Tile 1</Tile> </Box> <Tile>Tile 2</Tile> <Tile>Tile 3</Tile> </Flex> ``` ## 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. ```js <Flex direction={'row'} justify={'start'} gap={'small'}> <Box flex={1}> <Tile>flex = 1</Tile> </Box> <Box flex={2}> <Tile>flex = 2</Tile> </Box> <Box flex={3}> <Tile>flex = 3</Tile> </Box> <Box flex={4}> <Tile>flex = 4</Tile> </Box> </Flex> ``` ### Using alignSelf Use the `alignSelf` prop to override alignment rules for individual `Box` components. ```js <Flex direction={'column'} gap={'small'} align={'start'}> <Box alignSelf={'end'}> <Tile>Top right</Tile> </Box> <Box alignSelf={'center'}> <Tile>Middle</Tile> </Box> <Tile>Bottom left</Tile> </Flex> ``` ## Related components - [Flex](/reference/ui-components/standard-components/flex) - [Divider](/reference/ui-components/standard-components/divider) - [Tile](/reference/ui-components/standard-components/tile) # ButtonRow @ Learn about the ButtonRow component for use in UI extensions. $ reference & ui-components/standard-components --- # ButtonRow | UI components (BETA) The `ButtonRow` component renders a row of specified [Button](/reference/ui-components/standard-components/button)[](/reference/ui-components/overview) 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](/reference/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. ```js import { Button, ButtonRow } from '@hubspot/ui-extensions'; const Extension = () => { return ( <ButtonRow disableDropdown={false}> <Button onClick={() => { console.log('Regular button clicked'); }} > Regular Button </Button> <Button onClick={() => { console.log('Reset button clicked'); }} variant="destructive" type="reset" > Reset </Button> <Button onClick={() => { console.log('Submit button clicked'); }} variant="primary" type="submit" > Submit </Button> </ButtonRow> ); }; ``` | **Prop** | **Type** | **Description** | | --- | --- | --- | | `disableDropdown` | Boolean | 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. Default is `false`. | | `dropDownButtonOptions` | Object | 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:<ul><li>`size`: the size of the button. Can be `'xs'`, `'sm'`, or `'md'` (default).</li><li>`text`: the button's text. By default, is set to `More`. If set to an empty value, will display a gear icon.</li><li>`variant`: the button variation. Can be `'primary'`, `'secondary'` (default), or `'transparent'`.</li></ul>Learn more about [dropdown button variants](#dropdown-variants). | ## Dropdown variants Buttons that exceed available space will be presented in one [Dropdown](/reference/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. ```js import { Button, ButtonRow } from '@hubspot/ui-extensions'; const Extension = () => { return ( <ButtonRow dropDownButtonOptions={{ text: 'Extra', size: 'sm', variant: 'transparent', }} > <Button variant="primary">Primary</Button> <Button variant="destructive" type="reset"> Destructive </Button> <Button type="submit">Submit</Button> <Button type="button">Other</Button> </ButtonRow> ); }; ``` ## 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](/reference/ui-components/standard-components/button) - [CRM action components](/reference/ui-components/crm-action-components/overview) - [CrmActionButton](/reference/ui-components/crm-action-components/crm-action-button) # Button @ Learn about the Button component for use in UI extensions. $ reference & ui-components/standard-components --- # Button | UI components (BETA) 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](/reference/ui-components/design/patterns/buttons). 1. **Button text:** the visible button text that describes the button's action. 2. **Variant:** the color of the button. Learn more about [available button variants below](#variants). ```js import { Button } from '@hubspot/ui-extensions'; const Extension = () => { return ( <Button onClick={() => { console.log('Someone clicked the button!'); }} href={{ url: 'https://wikipedia.org', external: true, }} variant="primary" size="md" type="button" > Click me! </Button> ); }; ``` | **Prop** | **Type** | **Description** | | --- | --- | --- | | `href` | Object | Include this prop to open a URL on click. Contains the following fields:<ul><li>`url` (string): the URL that will open on click.</li><li>`external` (boolean): set to `true` to open the URL in a new tab and display an external link icon. By default:<ul><li>Links to HubSpot app pages will open in the same tab and will not include an icon.</li><li>Links to non-HubSpot app pages will open in a new tab and include the icon.</li></ul></li></ul>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. | | `disabled` | Boolean | When set to `true`, the button will render in a greyed-out state and cannot be clicked. | | `variant` | `'primary'` | `'secondary'` (default) | `'destructive'` | `'transparent'` | Sets the color of the button. See [variants section](#variants) for more information. | | `type` | `'button'` (default) | `'reset'` | `'submit'` | Sets the `role` HTML attribute of the button. | | `size` | `'xs'`, `'extra-small'` | `'sm'`, `'small'` | `'med'`, `'medium'` (default) | The size of the button. | | `overlay` | Object | Include a [Modal](/reference/ui-components/standard-components/modal) or [Panel](/reference/ui-components/standard-components/panel) component in this object to open it as an overlay on click. Learn more about [using overlays](/guides/crm/ui-extensions/sdk#open-an-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:** 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. 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](/reference/ui-components/standard-components/button-row) - [](/reference/ui-components/standard-components/button-row)[CRM action components](/reference/ui-components/crm-action-components/overview) - [](/reference/ui-components/crm-action-components/overview)[CrmActionButton](/reference/ui-components/crm-action-components/crm-action-button) - [Panel](/reference/ui-components/standard-components/panel) # BarChart | UI components (BETA) @ Learn about the BarChart component for use in UI extensions. $ reference & ui-components/standard-components/charts --- # BarChart | UI components (BETA) 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](/reference/ui-components/standard-components/charts/line-chart) component for time series plots or visualizing trend data. Learn more about [charts](/reference/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 projects](/guides/crm/ui-extensions/sample-extensions/overview#create-line-and-bar-charts). 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. ```js import { BarChart } from '@hubspot/ui-extensions'; const dailyInventorySample = [ { Product: 'Hats', Amount: 187, }, { Product: 'Socks', Amount: 65, }, { Product: 'Ascots', Amount: 120, } ]; return ( <BarChart data={dailyInventorySample} axes={{ x: { field: 'Product', fieldType: 'category' }, y: { field: 'Amount', fieldType: 'linear' }, options: {groupFieldByColor: 'Product'} }} options={{ title: "My chart", showLegend: true, showDataLabels: true, showTooltips: true }} /> ); }; ``` | 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](/reference/ui-components/standard-components/charts/overview#formatting-data). | | `axes`Learn more about [chart axes](/reference/ui-components/standard-components/charts/overview#configuring-axes). | | `options` | Object | Additional chart configuration options. Options include:<ul><li>`title` (string): a title for the chart.</li><li>`showLegend` (boolean): set to `true` to display a legend above the chart. </li><li>`showDataLabels` (boolean): set to `true` to display labels above data points.</li><li>`showTooltips` (boolean): set to `true` to display tooltips for data points on hover.</li><li>`colorList` (array): specify a custom order for colors to used in the report.</li></ul>Learn more about [chart options](/reference/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. ```js const dailyInventorySample = [ { Product: 'Standalone product A', Sales: 159, }, { Product: 'Bundle A', Sales: 53, }, { Product: 'Bundle B', Sales: 99, }, ]; return ( <BarChart data={dailyInventorySample} axes={{ x: { field: 'Product', fieldType: 'category' }, y: { field: 'Sales', fieldType: 'linear' }, options: { groupFieldByColor: 'Product' }, }} /> ); ``` For comparison, below is the same chart without `groupFieldByColor` configured for the axes: Colors will be automatically assigned in a [preset order](/reference/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. ```js const dailyInventorySample = [ { Product: 'Standalone product A', Sales: 159, }, { Product: 'Bundle A', Sales: 53, }, { Product: 'Bundle B', Sales: 99, }, ]; return ( <BarChart data={dailyInventorySample} axes={{ x: { field: 'Product', fieldType: 'category' }, y: { field: 'Sales', fieldType: 'linear' }, options: { groupFieldByColor: 'Product' }, }} options={{ colorList: ['purple', 'green', 'darkBlue'], }} /> ); ``` 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](/reference/ui-components/standard-components/charts/overview#colors). ```js const dailyInventorySample = [ { Product: 'Standalone product A', Sales: 159, }, { Product: 'Bundle A', Sales: 53, }, { Product: 'Bundle B', Sales: 99, }, ]; return ( <BarChart data={dailyInventorySample} axes={{ x: { field: 'Product', fieldType: 'category' }, y: { field: 'Sales', fieldType: 'linear' }, options: { groupFieldByColor: 'Product', colors: { 'Standalone product A': 'blue', 'Bundle A': 'orange', 'Bundle B': 'yellow', }, }, }} /> ); ``` ## 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. ```js 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 ( <BarChart data={{ data: dealCountSample, options: { propertyLabels: { dealstage: { appointmentScheduled: 'Appointments scheduled (Sales)', closedWon: 'Closed won (Sales)', }, user_id: { 194784: 'Sales user A', 295834: 'Sales user B', 938453: 'Sales user C', }, }, }, }} axes={{ x: { field: 'dealstage', fieldType: 'category', label: 'Deal Stage', }, y: { field: 'count', fieldType: 'linear', label: 'Count of Deals', }, options: { groupFieldByColor: 'user_id', stacking: true }, }} options={{ showLegend: true, showDataLabels: true, showTooltips: true, }} /> ); }; ``` 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](/reference/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](/reference/ui-components/standard-components/charts/bar-chart) - [Statistics](/reference/ui-components/standard-components/statistics) - [Table](/reference/ui-components/standard-components/table) # LineChart | UI components (BETA) @ Learn about the LineChart component for use in UI extensions. $ reference & ui-components/standard-components/charts --- # LineChart | UI components (BETA) 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](/reference/ui-components/standard-components/charts/bar-chart) component for comparing categorical data. Learn more about [charts](/reference/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 projects](/guides/crm/ui-extensions/sample-extensions/overview#create-line-and-bar-charts). 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. ```js 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 ( <LineChart data={salesOverTimeSample} axes={{ x: { field: 'Date', fieldType: 'datetime' }, y: { field: 'Sales', fieldType: 'linear' }, }} options={{ title: 'My chart', showLegend: true, showDataLabels: true, showTooltips: true, }} /> ); ``` | 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](/reference/ui-components/standard-components/charts/overview#formatting-data). | | `axes`Learn more about [chart axes](/reference/ui-components/standard-components/charts/overview#configuring-axes). | | `options` | Object | Additional chart configuration options. Options include:<ul><li>`title` (string): a title for the chart.</li><li>`showLegend` (boolean): set to `true` to display a legend above the chart. </li><li>`showDataLabels` (boolean): set to `true` to display labels above data points.</li><li>`showTooltips` (boolean): set to `true` to display tooltips for data points on hover.</li><li>`colorList` (array): specify a custom order for colors to used in the report.</li></ul>Learn more about [chart options](/reference/ui-components/standard-components/charts/overview#chart-options). | ## Colors To apply [colors](/reference/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. ```js 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 ( <LineChart data={VisitsPerSourceOverTime} axes={{ x: { field: 'Session Date', fieldType: 'category' }, y: { field: 'Visits', fieldType: 'linear' }, options: { groupFieldByColor: 'Breakdown' }, }} options={{ showLegend: true }} /> ); ``` Colors will be automatically assigned in a [preset order](/reference/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. ```js 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 ( <LineChart data={VisitsPerSourceOverTime} axes={{ x: { field: 'Session Date', fieldType: 'category' }, y: { field: 'Visits', fieldType: 'linear' }, options: { groupFieldByColor: 'Breakdown' }, }} options={{ showLegend: true, colorList: ['purple', 'green', 'darkBlue'], }} /> ); ``` 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](/reference/ui-components/standard-components/charts/overview#colors). ```js return ( <LineChart data={VisitsPerSourceOverTime} axes={{ x: { field: 'Session Date', fieldType: 'category' }, y: { field: 'Visits', fieldType: 'linear' }, options: { groupFieldByColor: 'Breakdown', colors: { Direct: 'blue', Email: 'orange', Referrals: 'yellow', }, }, }} options={{ showLegend: true }} /> ); ``` ## 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`. ```js 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 ( <LineChart data={{ data: visitsPerSourceOverTime, options: { propertyLabels: { breakdown: { direct: 'Direct', email: 'Email', referrals: 'Referrals', }, }, }, }} axes={{ x: { field: 'sessionDate', fieldType: 'category', label: 'Session Date' }, y: { field: 'visits', fieldType: 'linear', label: 'Visits' }, options: { groupFieldByColor: 'breakdown', stacking: true }, }} options={{ showLegend: true }} /> ); ``` 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](/reference/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](/reference/ui-components/standard-components/charts/line-chart) - [Statistics](/reference/ui-components/standard-components/statistics) - [Table](/reference/ui-components/standard-components/table) # Charts overview | UI components (BETA) @ Learn about the available chart components for visualizing data in UI extensions. $ reference & ui-components/standard-components/charts --- # Charts overview | UI components (BETA) Use charts to display data visualizations in UI extensions. HubSpot provides two chart components: [BarChart](/reference/ui-components/standard-components/charts/bar-chart) and [LineChart](/reference/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](/guides/crm/ui-extensions/sample-extensions/overview#create-line-and-bar-charts). 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 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. ```js // Example data array [ { 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. ```js // Example data array [ { 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. ```js <BarChart data={{ data: dealCountSample, options: { propertyLabels: { dealstage: { appointmentScheduled: 'Appointments scheduled (Sales)', closedWon: 'Closed won (Sales)', }, user_id: { '194784': 'Sales user A', '295834': 'Sales user B', '938453': 'Sales user C', }, }, }, }} axes={{ x: { field: 'dealstage', fieldType: 'category', label: 'Deal Stage', }, y: { field: 'count', fieldType: 'linear', label: 'Count of Deals', }, options: { groupFieldByColor: 'user_id', stacking: true }, }} options={{ showLegend: true, showDataLabels: true, showTooltips: true, }} /> ); ``` ## 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. One axis must have a `fieldType` of `linear`. ```js <BarChart axes={{ x: { field: 'Product', fieldType: 'category' }, y: { field: 'Amount', fieldType: 'linear', label: '# of sales' }, }} /> ``` | Parameter | Type | Description | | --- | --- | --- | | `field` | String | The name of the field from the dataset. | | `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:<ul><li>`groupFieldByColor` (string): specify a `field` to group by color. When not specified, only one color will be used for data visualization.</li><li>`stacking` (boolean): when set to `true`, grouped data will be stacked. Default is `false`.</li></ul> | 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`. ```js const dailyInventorySample = [ { Product: 'Hats', Amount: 187, }, { Product: 'Socks', Amount: 65, }, { Product: 'Ascots', Amount: 120, }, ]; return ( <BarChart data={dailyInventorySample} axes={{ x: { field: 'Product', fieldType: 'category' }, y: { field: 'Amount', fieldType: 'linear' }, options: { groupFieldByColor: 'Product', }, }} /> ); ``` ### 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`. ```js 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 ( <BarChart data={{ data: dealCountSample, options: { propertyLabels: { dealstage: { appointmentScheduled: 'Appointments scheduled (Sales)', closedWon: 'Closed won (Sales)', }, user_id: { 194784: 'Sales user A', 295834: 'Sales user B', 938453: 'Sales user C', }, }, }, }} axes={{ x: { field: 'dealstage', fieldType: 'category', label: 'Deal Stage', }, y: { field: 'count', fieldType: 'linear', label: 'Count of Deals', }, options: { groupFieldByColor: 'user_id', stacking: true }, }} options={{ showLegend: true, showDataLabels: true, showTooltips: true, }} /> ); }; ``` 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. ```js 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 ( <LineChart data={visitsPerSourceOverTime} axes={{ x: { field: 'Session Date', fieldType: 'category' }, y: { field: 'Visits', fieldType: 'linear' }, options: { groupFieldByColor: 'Breakdown', stacking: true }, }} options={{ showLegend: true }} /> ); ``` 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. ```js <BarChart data={dailyInventorySample} axes={{ x: { field: 'Product', fieldType: 'category' }, y: { field: 'Amount', fieldType: 'linear' }, options: { groupFieldByColor: 'Product' }, }} options={{ title: 'My Chart', showTooltips: true, showLegend: true, showDataLabels: true, colorList: ['darkGreen', 'darkPurple', 'blue'], }} /> ``` | 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. ```js const dailyInventorySample = [ { Product: 'Standalone product A', Sales: 159, }, { Product: 'Bundle A', Sales: 53, }, { Product: 'Bundle B', Sales: 99, }, ]; return ( <BarChart data={dailyInventorySample} axes={{ x: { field: 'Product', fieldType: 'category' }, y: { field: 'Amount', fieldType: 'linear' }, options: { groupFieldByColor: 'Product' }, }} options={{ colorList: ['darkGreen', 'blue', 'darkPurple'], }} /> ); ``` 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. ```js const dailyInventorySample = [ { Product: 'Standalone product A', Sales: 159, }, { Product: 'Bundle A', Sales: 53, }, { Product: 'Bundle B', Sales: 99, }, ]; return ( <BarChart data={dailyInventorySample} axes={{ x: { field: 'Product', fieldType: 'category' }, y: { field: 'Sales', fieldType: 'linear' }, options: { groupFieldByColor: 'Product', colors: { 'Standalone product A': 'blue', 'Bundle A': 'orange', 'Bundle B': 'yellow', }, }, }} /> ); ``` ### 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. # Checkbox @ Learn about the Checkbox component for use in UI extensions. $ reference & ui-components/standard-components --- # Checkbox | UI components (BETA) The `Checkbox` component renders single checkbox input. If you want to display multiple checkboxes, you should use [ToggleGroup](/reference/ui-components/standard-components/toggle-group) instead, as it comes with extra logic for handling multiple checkboxes and radio buttons. ```js import { Checkbox } from '@hubspot/ui-extensions'; const Extension = () => { return ( <Checkbox checked={isSuperAdmin} name="adminCheck" description="Select to grant superpowers" > Super Admin </Checkbox> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `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. | | `name` | String | The checkbox's unique identifier. | | `checked` | Boolean | When set to `true`, the checkbox is selected. Default is `false`. | | `initialIsChecked` | Boolean | When set to `true`, the checkbox is selected by default. Default is `false`. | | `readOnly` | Boolean | When set to `true`, the checkbox cannot be selected. | | `description` | String | Text that describes the field's purpose. | | `aria-label` | String | The checkbox's accessibility label. | | `variant` | `'sm'`, `'small'` | `'default'` | The size of the checkbox | | `inline` | Boolean | When set to `true`, arranges checkboxes side by side. Default is `false`. | | `onChange` | (checked: boolean, value: string) => void; | A callback function that is called when the checkbox is selected or cleared. Passes the new value. | ## Related components - [RadioButton](/reference/ui-components/standard-components/radio-button) - [ToggleGroup](/reference/ui-components/standard-components/toggle-group) - [TextArea](/reference/ui-components/standard-components/text-area) # DateInput | UI components @ Learn about the date input component for use in UI extensions. $ reference & ui-components/standard-components --- # DateInput | UI components (BETA) The `DateInput` component renders an input field where a user can select a date. ```js import { Flex, DateInput } from '@hubspot/ui-extensions'; function RemoteApp() { const [dateValue, setDateValue] = useState(null); return ( <Flex direction="column"> <DateInput label="Appointment Date Partially Controlled" name="date" onChange={(value) => { setDateValue(value); }} value={dateValue} format="ll" /> <DateInput label="Appointment Date Uncontrolled" name="appointment-date" format="LL" /> </Flex> ); } ``` | Prop | Type | Description | | --- | --- | --- | | `name` | String | The input's unique identifier. | | `label` | String | The text that displays above the input. | | `value` | Object | The value of the input. Must include the year, month, and day:`{ year: number;``month: number;``date: number }`<ul><li>`year`: the four-digit year (e.g., `2023`).</li><li>`month`: starting at `0`, the number of the month (e.g., `0` = January, `11` = December).</li><li>`date`: the number of the day (e.g., `1` = the first day of the month).</li></ul> | | `defaultValue` | Object | The default date value. Uses the same format as the `value` field. | | `required` | Boolean | When set to `true`, displays a required field indicator. | | `tooltip` | String | The text that displays in a tooltip next to the label. | | `readOnly` | Boolean | When set to `true`, the checkbox cannot be selected. | | `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. | | `validationMessage` | String | The text to display if the input has an error. | | `min` | Object | Sets the earliest valid date available using the following format:`{ year: number;``month: number;``date: number }` | | `max` | Object | A Sets the latest 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. | | `maxValidationMessage` | String | Sets the message that users will see when the hover over dates after the max date. | | `format` | `'short'` (default) | `'long'` | `'medium'` | `'standard'` | `'YYYY-MM-DD'` | `L` | `LL` | `ll` | The date format.<ul><li>`short`: 09/04/1986</li><li>`long`: September 4, 1986</li><li>`medium`: Sep 4, 1986</li><li>`standard`: 1986-09-04</li><li>`YYYY-MM-DD`: 1986-09-04</li><li>`L`: 09/04/1986</li><li>`LL`: September 4, 1986</li><li>`ll`: Sep 4, 1986</li></ul> | | `clearButtonLabel` | String | Sets the label of the button that clears the date. | | `todayButtonLabel` | String | Sets the label of the button that inserts today's date. | | `timezone` | `'userTz'` (default) | `'portalTz'` | Sets the timezone that the component will user to calculate valid dates.<ul><li>`userTz` (default): the user's time zone.</li><li>`portalTz`: the portal's default time zone.</li></ul> | | `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. | | `onBlur` | (value: DateInputEventsPayload) => void | A function that is called and passes the value when the field loses focus. | | `onFocus` | (value: DateInputEventsPayload) => void | A function that is called and passed the value when the field gets focused. | ## Related components - [Form](/reference/ui-components/standard-components/form) - [MultiSelect](/reference/ui-components/standard-components/multi-select) - [NumberInput](/reference/ui-components/standard-components/number-input) # DescriptionList | UI components @ Learn about the DescriptionList component for use in UI extensions. $ reference & ui-components/standard-components --- # DescriptionList | UI components (BETA) 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. ```js import { DescriptionList, DescriptionListItem, Text, } from '@hubspot/ui-extensions'; const Extension = () => { return ( <DescriptionList direction="row"> <DescriptionListItem label={'First Name'}> <Text>Alan</Text> </DescriptionListItem> <DescriptionListItem label={'Last Name'}> <Text>Turing</Text> </DescriptionListItem> </DescriptionList> ); }; ``` ### DescriptionList props | **Prop** | **Type** | **Description** | | --- | --- | --- | | `direction` | `row` | `column` (default) | The direction that the label and value pairs are displayed. | ### DescriptionListItem 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 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](/reference/ui-components/standard-components/list) - [Table](/reference/ui-components/standard-components/table) - [Statistics](/reference/ui-components/standard-components/statistics) - [CrmPropertyList](/reference/ui-components/crm-data-components/crm-property-list) # Divider @ Learn about the Divider component for use in UI extensions. $ reference & ui-components/standard-components --- # Divider | UI components (BETA) 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. ```js import { Divider, Text } from '@hubspot/ui-extensions'; const Extension = () => { return ( <> <Text>Text above the divider.</Text> <Divider /> <Text>Text below the divider.</Text> </> ); }; ``` | **Prop** | **Type** | **Description** | | --- | --- | --- | | `distance` | `'flush'` | `'extra-small'` | `'small'` (default) | `'medium'` | `'large'` | `'extra-large'` | The space between the divider and the content above and below it. | ## Variants Using the `distance` 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](/reference/ui-components/standard-components/box) - [Accordion](/reference/ui-components/standard-components/accordion) - [Tile](/reference/ui-components/standard-components/tile) # Dropdown | UI components @ Learn about the Dropdown component for use in UI extensions. $ reference & ui-components/standard-components --- # Dropdown | UI components (BETA) The `Dropdown` component renders a dropdown menu that can appear as a button or hyperlink. Use this component to enable users to select from multiple options in a compact list. This component includes sizing options. ```js import { Dropdown } from '@hubspot/ui-extensions'; const Extension = () => { const ddOptions = [ { label: 'Clone', onClick: () => console.log({ message: 'Clone group' }), }, { label: 'Delete', onClick: () => console.log({ message: 'Delete group' }), }, ]; return ( <Dropdown options={ddOptions} variant="primary" buttonSize="md" buttonText="More" /> ); }; ``` | **Prop** | **Type** | **Description** | | --- | --- | --- | | `options` | | `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. | | `buttonText` | String | The button text. | | `buttonSize` | `'xs'`, `'extra-small'` | `'sm'`, `'small'` | `'md'`, `'medium'` (default) | The size of the button. | | `disabled` | Boolean | When set to `true`, the dropdown button cannot be focused or clicked. Set to `false` by default. | ## 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](/reference/ui-components/crm-action-components/crm-action-link) - [CrmCardActions](/reference/ui-components/crm-action-components/crm-card-actions) - [Button](/reference/ui-components/standard-components/button) # EmptyState | UI components @ Learn about the EmptyState component for use in UI extensions. $ reference & ui-components/standard-components --- # EmptyState | UI components (BETA) 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](https://developers.hubspot.com/docs/platform/ui-extension-components/text) to provide further guidance. This does not come with the component by default. 4. **Additional button:** an additional [`Button` component](/reference/ui-components/standard-components/button) to can help users take action. This does not come with the component by default. ```js import { EmptyState, Text } from '@hubspot/ui-extensions'; const Extension = ({ data }) => { if (!data || !data.length) { return ( <EmptyState title="Nothing here yet" layout="vertical" reverseOrder={true}> <Text>Go out there and get some leads!</Text> </EmptyState> ) } return ( {data.map(...)} ); } ``` | **Prop** | **Type** | **Description** | | --- | --- | --- | | `flush` | Boolean | When set to `true`, removes the default vertical margins for the component. By default, set to `false`. | | `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. | ## 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](/reference/ui-components/standard-components/alert) - [ErrorState](/reference/ui-components/standard-components/error-state) - [LoadingSpinner](/reference/ui-components/standard-components/loading-spinner) # Error state | UI components @ Learn about the ErrorState component for use in UI extensions. $ reference & ui-components/standard-components --- # ErrorState | UI components (BETA) 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](/reference/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](/reference/ui-components/standard-components/button) to can help users take action. This does not come with the component by default. ```js import { ErrorState, Text, Button } from '@hubspot/ui-extensions'; const Extension = ({ data, error, fetchData }) => { if (error) { return ( <ErrorState title="Trouble fetching properties."> <Text> Please try again in a few moments. </Text> <Button onClick={fetchData}> Try again </Button> </ErrorState> ) } return ( {data.map(...)} ); } ``` | **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. ```js const Extension = ({ data, error, fetchData }) => { if (error) { return ( <ErrorState title="Trouble fetching properties." type="error" > </ErrorState> ) } return ( {data.map(...)} ); } ``` ```js const Extension = ({ data, error, fetchData }) => { if (error) { return ( <ErrorState title="Something went wrong." type="support" > </ErrorState> ) } return ( {data.map(...)} ); } ``` ```js const Extension = ({ data, error, fetchData }) => { if (error) { return ( <ErrorState title="You must log in to view this data." type="lock" > </ErrorState> ) } 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](/reference/ui-components/standard-components/alert) - [EmptyState](/reference/ui-components/standard-components/empty-state) - [LoadingSpinner](/reference/ui-components/standard-components/loading-spinner) # Flex @ Learn about the Flex component for configuring UI extension layout. $ reference & ui-components/standard-components --- # Flex | UI components (BETA) 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. To see an example of how `Flex` and `Box` can be used for layout, check out HubSpot's [Manage layouts: Flex and Box sample project](/guides/crm/ui-extensions/sample-extensions/overview#managing-layouts-flex-and-box). ```js import { Flex, Tile } from '@hubspot/ui-extensions'; const Extension = () => { return ( <Flex direction={'row'} justify={'end'} wrap={'wrap'} gap={'small'}> <Tile>Left</Tile> <Tile>Right</Tile> <Flex direction={'column'}> <Tile>Bottom</Tile> </Flex> </Flex> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `direction` | `'row'` (default) | `'column'` | Arranges the components horizontally or vertically by setting the main axis. | | `justify` | `'start'` (default) | `'center'` | `'end'` | `'around'` | `'between'` | Distributes components along the main axis using the available free space. | | `align` | `'start'` | `'center'` | `'end'` | `'baseline'` | `'stretch'` (default) | Distributes components along the cross-axis using the available free space. | | `alignSelf` | `'start'` | `'center'` | `'end'` | `'baseline'` | `'stretch'` | 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. | | `wrap` | `'wrap'` | `'nowrap'` (default) | Whether components will wrap rather than trying to fit on one line. | | `gap` | `'flush'` (default) | `'extra-small'` | `'small'` | `'medium'` | `'large'` | `'extra-large'` | Sets the spacing between components. | ## 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. <table> <tbody> <tr> <td> <img alt="flex-tiles-justify-between" src="https://developers.hubspot.com/hs-fs/hubfs/Knowledge_Base_2023/flex-tiles-justify-between.png" /> </td> </tr> <tr> <td> <code>justify={'between'}</code> </td> </tr> <tr> <td> <p> <img alt="flex-tiles-justify-around" src="https://developers.hubspot.com/hs-fs/hubfs/Knowledge_Base_2023/flex-tiles-justify-around.png" /> </p> </td> </tr> <tr> <td> <code>justify={'around'}</code> </td> </tr> <tr> <td> <p> <img alt="ui-extrensions-layout-tile-justify-start" src="https://developers.hubspot.com/hs-fs/hubfs/Knowledge_Base_2023/ui-extrensions-layout-tile-justify-start.png" /> </p> </td> </tr> <tr> <td> <code>justify={'start'}</code> </td> </tr> <tr> <td> <p> <img alt="ui-extrensions-layout-tile-justify-center" src="https://developers.hubspot.com/hs-fs/hubfs/Knowledge_Base_2023/ui-extrensions-layout-tile-justify-center.png" /> </p> </td> </tr> <tr> <td> <code>justify={'center'}</code> </td> </tr> <tr> <td> <p> <img alt="ui-extrensions-layout-tile-justify-end" src="https://developers.hubspot.com/hs-fs/hubfs/Knowledge_Base_2023/ui-extrensions-layout-tile-justify-end.png" /> </p> </td> </tr> <tr> <td> <code>justify={'end'}</code> </td> </tr> </tbody> </table> ### 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. ```js <Flex direction={'row'} justify={'between'} wrap={'wrap'} gap={'medium'}> <Tile>One</Tile> <Tile>Two</Tile> <Tile>Three</Tile> <Tile>Four</Tile> <Tile>Five</Tile> <Tile>Six</Tile> <Tile>Seven</Tile> <Tile>Eight</Tile> </Flex> ``` ### 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. <table> <tbody> <tr> <td> <img alt="ui-extrensions-layout-tile-column-align-start" src="https://developers.hubspot.com/hs-fs/hubfs/Knowledge_Base_2023/ui-extrensions-layout-tile-column-align-start.png?width=500&height=268&name=ui-extrensions-layout-tile-column-align-start.png" /> </td> </tr> <tr> <td> <code>align={'start'}</code> </td> </tr> <tr> <td> <p> <img alt="ui-extrensions-layout-tile-column-align-center" src="https://developers.hubspot.com/hs-fs/hubfs/Knowledge_Base_2023/ui-extrensions-layout-tile-column-align-center.png?width=500&height=267&name=ui-extrensions-layout-tile-column-align-center.png" /> </p> </td> </tr> <tr> <td> <code>align={'center'}</code> </td> </tr> <tr> <td> <p> <img alt="ui-extrensions-layout-tile-column-align-end" src="https://developers.hubspot.com/hs-fs/hubfs/Knowledge_Base_2023/ui-extrensions-layout-tile-column-align-end.png?width=500&height=262&name=ui-extrensions-layout-tile-column-align-end.png" /> </p> </td> </tr> <tr> <td> <code>align={'end'}</code> </td> </tr> </tbody> </table> ### 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. ```js <Flex direction={'row'} justify={'start'} gap={'flush' | 'extra-small' | 'small' | 'medium' | 'large' | 'extra-large'} > <Tile>Tile 1</Tile> <Tile>Tile 2</Tile> <Tile>Tile 3</Tile> </Flex> ``` ### 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. ```js <Flex direction={'row'} justify={'end'} wrap={'wrap'} gap={'small'}> <Tile>Left</Tile> <Tile>Right</Tile> <Flex direction={'column'}> <Tile>Bottom</Tile> </Flex> </Flex> ``` ## Related components - [Tile](/reference/ui-components/standard-components/tile) - [Box](/reference/ui-components/standard-components/box) - [Divider](/reference/ui-components/standard-components/divider) # Form @ Learn about the Form component for use in UI extensions. $ reference & ui-components/standard-components --- # Form | UI components (BETA) The `Form` component renders a form that can contain other subcomponents, such as [Input](/reference/ui-components/standard-components/input), [Select](/reference/ui-components/standard-components/select), and [Button](/reference/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](/reference/ui-components/design/patterns/forms). 1. **Label:** the input label. 2. **Text input:** an [Input](/reference/ui-components/standard-components/input) component with a placeholder value of _First name_. 3. **Label:** the select input label. 4. **Select input:** a [Select](/reference/ui-components/standard-components/select) component with value of _Customer_. 5. **Button:** a [Button](/reference/ui-components/standard-components/button) component to submit the form's information. ```js import { Form, Input, Button } from '@hubspot/ui-extensions'; const Extension = () => { return ( <Form onSubmit={() => { console.log('Form submitted!'); }} > <Input label="First Name" name="first-name" tooltip="Please enter your first name" description="Please enter your first name" placeholder="First name" /> <Input label="Last Name" name="last-name" tooltip="Please enter your last name" description="Please enter your last name" placeholder="Last name" /> <Button onClick={() => { console.log('Submit button clicked'); }} variant="primary" type="submit" > Submit </Button> </Form> ); }; ``` | **Prop** | **Type** | **Description** | | --- | --- | --- | | `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. | | `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/Attributes/autocomplete). | ## 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](/reference/ui-components/standard-components/button) - [DateInput](/reference/ui-components/standard-components/date-input) - [Input](/reference/ui-components/standard-components/input) - [MultiSelect](/reference/ui-components/standard-components/multi-select) - [NumberInput](/reference/ui-components/standard-components/number-input) - [Select](/reference/ui-components/standard-components/select) # Heading @ Learn about the Heading component for use in UI extensions. $ reference & ui-components/standard-components --- # Heading | UI components (BETA) The `Heading` component renders large heading text. Use this component to introduce or differentiate sections of your component. ```js import { Heading } from '@hubspot/ui-extensions'; const Extension = () => { return <Heading>Heading text</Heading>; }; ``` | **Prop** | **Type** | **Description** | | -------- | -------- | --------------------------------------------- | | `inline` | Boolean | When set to `true`, text will not line break. | ## 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](/reference/ui-components/standard-components/text) - [Link](/reference/ui-components/standard-components/link) - [Accordion](/reference/ui-components/standard-components/accordion) # Icon @ Learn about the Icon component for use in UI extensions. $ reference & ui-components/standard-components --- # Icon | UI components (BETA) 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). ```js import { Alert, Button, Flex, Icon, Text } from '@hubspot/ui-extensions'; const Extension = () => { return ( <Flex direction="column" gap="small"> <Alert title="AlertTitle"> <Icon name="download" /> Icon inside an Alert </Alert> <Button> <Icon name="download" /> Icon inside a button </Button> <Text> <Icon name="download" /> Icon with text </Text> </Flex> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `name` Required | String | Sets the icon to display. See [all available icons](#available-icons). | | `size` | `'small'` \| `'medium'` \| `'large'` | 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. | | `screenReaderText` | String | Sets the text that screen readers will read for the icon. | ## Spacing By default, spacing is not added around the icon. However, there may be times when you want more space between the icon and neighboring text. You can do this by manually adding a space to the text string, or by using ` ` or `{" "}` as shown below. ```js import { Alert, Flex, Icon, Tile, Text } from '@hubspot/ui-extensions'; const Extension = () => { return ( <Flex direction="column" gap="small"> <Alert title="AlertTitle"> <Icon name="warning" /> Icon inside an alert </Alert> <Tile> <Icon name="robot" /> Icon inside a tile </Tile> <Text> <Icon name="faceHappy" /> Icon with text </Text> </Flex> ); }; ``` ## Guidelines 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. ## Available icons Below are the currently available icons and their `name` values. <CardsRow columnSize={140}> <Card style={{ padding: '12' }}> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1 }} > <img width="32px" alt="icon-add" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/add.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px' }} > <code>add</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-appointment" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/appointment.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>appointment</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-approvals" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/approvals.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>approvals</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-attach" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/attach.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>attach</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-bank" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/bank.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>bank</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-block" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/block.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>block</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-book" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/book.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>book</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-bulb" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/bulb.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>bulb</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-calling" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/calling.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>calling</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-callingHangup" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/callingHangup.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>callingHangup</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-callingMade" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/callingMade.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>callingMade</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-callingMissed" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/callingMissed.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>callingMissed</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-callingVoicemail" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/callingVoicemail.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>callingVoicemail</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-campaigns" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/campaigns.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>campaigns</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-cap" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/cap.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>cap</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-clock" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/clock.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>clock</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-comment" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/comment.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>comment</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-contact" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/contact.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>contact</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-copy" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/copy.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>copy</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-dataSync" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/dataSync.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>dataSync</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-date" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/date.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>date</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-delete" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/delete.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>delete</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-downCarat" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/downCarat.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>downCarat</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-download" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/download.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>download</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-edit" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/edit.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>edit</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-email" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/email.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>email</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-enroll" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/enroll.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>enroll</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <img width="32px" alt="icon-exclamation" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/exclamation.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>exclamation</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-facebook" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/facebook.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>facebook</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-faceHappy" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/faceHappy.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>faceHappy</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-faceHappyFilled" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/faceHappyFilled.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>faceHappyFilled</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-faceNeutral" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/faceNeutral.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>faceNeutral</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-faceNeutralFilled" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/faceNeutralFilled.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>faceNeutralFilled</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-faceSad" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/faceSad.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>faceSad</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-faceSadFilled" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/faceSadFilled.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>faceSadFilled</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-file" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/file.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>file</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-filter" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/filter.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>filter</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-gauge" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/gauge.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>gauge</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-generateChart" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/generateChart.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>generateChart</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-gift" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/gift.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>gift</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-globe" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/globe.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>globe</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-goal" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/goal.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>goal</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-googlePlus" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/googlePlus.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>googlePlus</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-flame" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/flame.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>flame</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-hash" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/hash.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>hash</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-home" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/home-1.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>home</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-image" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/image.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>image</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-imageGallery" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/imageGallery.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>imageGallery</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-inbox" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/inbox.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>inbox</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-insertVideo" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/insertVideo.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>insertVideo</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-instagram" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/instagram.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>instagram</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-invoice" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/invoice-1.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>invoice</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-key" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/key.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>key</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-left" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/left.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>left</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-lesson" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/lesson.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>lesson</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-link" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/link.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>link</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-linkedin" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/linkedin.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>linkedin</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-listView" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/listView.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>listView</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-location" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/location.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>location</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-locked" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/locked.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>locked</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-mention" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/mention.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>mention</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-messages" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/messages.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>messages</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-mobile" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/mobile.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>mobile</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-notification" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/notification.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>notification</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-notificationOff" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/notificationOff.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>notificationOff</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-objectAssociations" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/objectAssociations.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>objectAssociations</code> </div> </div> </Card> </CardsRow> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>objectAssociationsManyToMany</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-objectAssociationsManyToOne" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/objectAssociationsManyToOne.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>objectAssociationsManyToOne</code> </div> </div> </Card> </CardsRow> </div> <CardsRow columnSize={145}> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-office365" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/office365.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>office365</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-order" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/order.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>order</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-paymentSubscriptions" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/paymentSubscriptions.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>paymentSubscriptions</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-pinterest" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/pinterest.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>pinterest</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-powerPointFile" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/powerPointFile.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>powerPointFile</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-presentation" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/presentation.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>presentation</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-product" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/product.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>product</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-publish" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/publish.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>publish</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-question" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/question.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>question</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-questionAnswer" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/questionAnswer.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>questionAnswer</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-quickbooks" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/quickbooks.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>quickbooks</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-quote" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/quote.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>quote</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-readMore" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/readMore.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>readMore</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-readOnlyView" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/readOnlyView.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>readOnlyView</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-realEstateListing" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/realEstateListing.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>realEstateListing</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-recentlySelected" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/recentlySelected.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>recentlySelected</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-record" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/record.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>record</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-redo" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/redo.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>redo</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-refresh" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/refresh.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>refresh</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-registration" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/registration.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>registration</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-remove" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/remove.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>remove</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-replace" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/replace.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>replace</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-reports" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/reports.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>reports</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-right" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/right.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>right</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-robot" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/robot.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>robot</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-rotate" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/rotate.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>rotate</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-rss" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/rss.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>rss</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-salesQuote" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/salesQuote.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>salesQuote</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-salesQuote" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/salesQuote.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>salesTemplates</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <img width="32px" alt="icon-save" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/save.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>save</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-search" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/search.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>search</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-sequences" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/sequences.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>sequences</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-settings" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/settings.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>settings</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-shoppingCart" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/shoppingCart.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>shoppingCart</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-sortAlpAsc" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/sortAlpAsc.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>sortAlpAsc</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-sortAlpDesc" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/sortAlpDesc.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>sortAlpDesc</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-sortAmtAsc" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/sortAmtAsc.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>sortAmtAsc</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-sortAmtDesc" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/sortAmtDesc.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>sortAmtDesc</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-sortNumAsc" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/sortNumAsc.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>sortNumAsc</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-sortNumDesc" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/sortNumDesc.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>sortNumDesc</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-sortTableAsc" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/sortTableAsc.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>sortTableAsc</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-sortTableDesc" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/sortTableDesc.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>sortTableDesc</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-spellCheck" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/spellCheck.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>spellCheck</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-star" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/star.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>star</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-strike" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/strike.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>strike</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-success" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/success.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>success</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-tablet" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/tablet.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>tablet</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-tag" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/tag.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>tag</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <img width="32px" alt="icon-tasks" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/tasks.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>tasks</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-test" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/test.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>test</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-text" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/text.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>text</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-textColor" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/textColor.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>textColor</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-textDataType" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/textDataType.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>textDataType</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-textSnippet" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/textSnippet-1.png?" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>textSnippet</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-thumbsDown" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/thumbsDown.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>thumbsDown</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-thumbsUp" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/thumbsUp.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>thumbsUp</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-ticket" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/ticket.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>ticket</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-translate" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/translate.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>translate</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-trophy" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/trophy.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>trophy</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-twitter" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/twitter.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>twitter</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-undo" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/undo.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>undo</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-upCarat" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/upCarat.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>upCarat</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-upload" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/upload.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>upload</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-insertVideo" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/video.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>video</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-videoFile" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/videoFile.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>videoFile</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-videoPlayerSubtitles" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/videoPlayerSubtitles.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>videoPlayerSubtitles</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-view" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/view.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>view</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-viewDetails" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/viewDetails.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>viewDetails</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-warning" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/warning.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>warning</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-website" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/website.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>website</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-workflows" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/workflows.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>workflows</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-x" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/x.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>x</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-xing" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/xing.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>xing</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-youtube" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/youtube.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>youtube</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <img width="32px" alt="icon-youtubePlay" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/youtubePlay.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>youtubePlay</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-zoomIn" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/zoomIn.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>zoomIn</code> </div> </div> </Card> <Card> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center', }} > <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60px', flexGrow: 1, }} > <img width="32px" alt="icon-zoomOut" src="https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/uie-icons/zoomOut.png" /> </div> <div style={{ marginTop: '8px', padding: '4px 8px', borderRadius: '4px', }} > <code>zoomOut</code> </div> </div> </Card> </CardsRow> ## Related components - [Alert](/reference/ui-components/standard-components/alert) - [Tag](/reference/ui-components/standard-components/tag) - [Text](/reference/ui-components/standard-components/text) # Illustration @ Learn about the Illustration component for use in UI extensions. $ reference & ui-components/standard-components --- <style> {` .image-text-container { display: flex; flex-direction: column; height: 100%; align-items: center; } .image-container { display: flex; alignItems: center; justifyContent: center; height: auto; flexGrow: 1, } .text-container { margin-top: 8px; padding: 4px 8px; border-radius: 4px; } `} </style> # Illustration | UI components (BETA) The `Illustration` component renders an illustration from HubSpot's illustration library. Use this component to add a visual indicator to your app card. ```jsx import { Illustration } from '@hubspot/ui-extensions'; const Extension = () => { return ( <Illustration name="lock" alt="Lock icon indicating content is currently restricted or inaccessible" width={300} height={300} /> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `name` | String | The name of the illustration. See [available illustrations](#available-illustrations). | | `alt` | String | The illustration's alt text for accessibility. Default value is `<name> illustration`. | | `width` | Number | The width of the illustration in pixels. | | `height` | Number | The height of the illustration in pixels. | ## Available illustrations <CardsRow columnSize={200}> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> <Card style={{ padding: '12' }}> </div> </Card> </CardsRow> ## Related components - [Icon](/reference/ui-components/standard-components/icon) - [EmptyState](/reference/ui-components/standard-components/empty-state) - [ErrorState](/reference/ui-components/standard-components/error-state) # Image @ Learn about the Image component for use in UI extensions. $ reference & ui-components/standard-components --- # Image | UI components (BETA) 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. ```js import { Image } from '@hubspot/ui-extensions'; const Extension = () => { return ( <Image alt="A picture of a welcome sign" src="https://picsum.photos/id/237/200/300" href={{ url: 'https://www.wikipedia.org', external: true, }} onClick={() => { console.log('Someone clicked the image!'); }} width={200} /> ); }; ``` | **Prop** | **Type** | **Description** | | --- | --- | --- | | `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). | | `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/Element/img#attributes). | | `href` | Object | When provided, sets the URL that will open when the image is clicked. Contains the following fields:<ul><li>`url` (string): the URL that will open on click.</li><li>`external` (boolean): set to `true` to open the URL in a new tab. By default:<ul><li>Links to HubSpot app pages will open in the same tab.</li><li>Links to non-HubSpot app pages will open in a new tab.</li></ul></li></ul>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. | | `width` | Number | The pixel width of the image. | | `height` | Number | The pixel height of the image. | | `overlay` | Object | Include a [Modal](/reference/ui-components/standard-components/modal) or [Panel](/reference/ui-components/standard-components/panel) component in this object to open it as an overlay on click. Learn more about [using overlays](/guides/crm/ui-extensions/sdk#open-an-overlay). | ## 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. ```js import { Image } from "@hubspot/ui-extensions"; import myImage from './images/myImage.png' import myImage2 from './images/myImage2.png' const Extension = () => { return ( <> <Image src={myImage} width={300}></Image> <Image src={myImage2} width={300}></Image> </> ); }; ``` ## Related components - [Link](/reference/ui-components/standard-components/link) - [Text](/reference/ui-components/standard-components/text) - [Heading](/reference/ui-components/standard-components/heading) # Input @ Learn about the Input component for use in UI extensions. $ reference & ui-components/standard-components --- # Input | UI components (BETA) 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](/reference/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. ```js 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 ( <Form> <Input label="First Name" name="first-name" tooltip="Please enter your first name" description="Please enter your first name" placeholder="First name" required={true} error={!isValid} validationMessage={validationMessage} onChange={(value) => { setName(value); }} onInput={(value) => { if (value !== 'Bill') { setValidationMessage('This form only works for people named Bill'); setIsValid(false); } else if (value === '') { setValidationMessage('First name is required'); setIsValid(false); } else { setValidationMessage('Valid first name!'); setIsValid(true); } }} /> <Input label="Password" name="password" description="Enter your password" placeholder="Password" onInput={() => {}} type="password" /> </Form> ); }; ``` | **Prop** | **Type** | **Description** | | --- | --- | --- | | `name` | String | The input's unique identifier, similar to the [HTML input element name attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#name). | | `label` | String | The text that displays above the input. Required if `inputType` is not set to `hidden`. | | `required` | Boolean | When set to `true`, displays a required field indicator. | | `value` | Function | The value of the input. | | `type` | `'text'` (default) | `'password'` | The type of input. An input with the `'password'` type will hide the characters that the user types. | | `readOnly` | Boolean | When set to `true`, users will not be able to enter a value into the field. | | `description` | String | Displayed text that describes the field's purpose. | | `tooltip` | String | The text that displays in a tooltip next to the input label. | | `placeholder` | String | The text that appears in the input before a value is set. | | `error` | Boolean | 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. | | `validationMessage` | String | The text to show if the input has an error. | | `onChange` | (value: string) => void | 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. | | `onInput` | (value: string) => 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). | | `onBlur` | (value: string) => void | A function that is called and passes the value when the field loses focus. | | `onFocus` | (value: string) => void | A function that is called and passed the value when the field gets focused. | ## 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](/reference/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](/reference/ui-components/standard-components/select) - [TextArea](/reference/ui-components/standard-components/text-area) - [Form](/reference/ui-components/standard-components/form) # Link @ Learn about the link component for use in UI extensions. $ reference & ui-components/standard-components --- # Link | UI components (BETA) 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. ```js import { Link } from '@hubspot/ui-extensions'; const Extension = () => { return ( <Link href={{ url: 'https://www.wikipedia.org', external: true, }} > Wikipedia </Link> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `href` Required | Object | Sets the link's URL and open behavior. Contains the following fields:<ul><li>`url` (string): the URL that will open on click.</li><li>`external` (boolean): set to `true` to open the URL in a new tab and display an external link icon. By default:<ul><li>Links to HubSpot app pages will open in the same tab and will not include an icon.</li><li>Links to non-HubSpot app pages will open in a new tab and include the icon.</li></ul></li></ul> | | `variant` | `'primary'` (default) | `'light'` | `'dark'` | `'destructive'` | The color of the link. See the [variants section](#variants) for more information. | | `onClick` | `() => void` | A function that is invoked when the link is clicked. The function receives no arguments and its return value is ignored. | | `preventDefault` | Boolean | When set to `true`, `event.preventDefault()` will be invoked before the `onClick` function is called, preventing automatic navigation to the href URL. | | `overlay` | Object | Include a [Modal](/reference/ui-components/standard-components/modal) or [Panel](/reference/ui-components/standard-components/panel) component in this object to open it as an overlay on click. Learn more about [using overlays](/guides/crm/ui-extensions/sdk#open-an-overlay). | ## 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](/reference/ui-components/standard-components/heading) - [Text](/reference/ui-components/standard-components/text) - [Image](/reference/ui-components/standard-components/image) # List @ Learn about the List component for use in UI extensions. $ reference & ui-components/standard-components --- # List | UI components (BETA) The `List` component renders a list of items. Each item in `List` will be wrapped in `<li>` tags. A list can be styled as inline, ordered, or unordered with the `variant` prop. ```js import { List } from '@hubspot/ui-extensions'; const Extension() { return ( <List variant="unordered-styled"> <Link href="www.hubspot.com">List item 1</Link> <Link href="www.developers.hubspot.com">List item 2</Link> <Link href="www.knowledge.hubspot.com">List item 3</Link> </List> ); } ``` | Prop | Type | Description | | --- | --- | --- | | `variant` | `'unordered'` (default) | `'unordered-styled'` | `'ordered'` | `'ordered-styled'` | `'inline'` | `'inline-divided'` | The type of list to render. See [variants section](#variants) below for examples. | ## 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: ```js <List variant="unordered-styled"> <Link href="www.hubspot.com">List item 1</Link> <Link href="www.developers.hubspot.com">List item 2</Link> <Link href="www.knowledge.hubspot.com">List item 3</Link> </List> ``` To create a numbered list without styling: ```js <List variant="ordered"> <Link href="www.hubspot.com">List item 1</Link> <Link href="www.developers.hubspot.com">List item 2</Link> <Link href="www.knowledge.hubspot.com">List item 3</Link> </List> ``` To create a numbered list with styling: ```js <List variant="ordered-styled"> <Link href="www.hubspot.com">List item 1</Link> <Link href="www.developers.hubspot.com">List item 2</Link> <Link href="www.knowledge.hubspot.com">List item 3</Link> </List> ``` To stack list items horizontally: ```js <List variant="inline"> <Link href="www.hubspot.com">List item 1</Link> <Link href="www.developers.hubspot.com">List item 2</Link> <Link href="www.knowledge.hubspot.com">List item 3</Link> </List> ``` To stack list items horizontally with a divider between each item: ```js <List variant="inline-divided"> <Link href="www.hubspot.com">List item 1</Link> <Link href="www.developers.hubspot.com">List item 2</Link> <Link href="www.knowledge.hubspot.com">List item 3</Link> </List> ``` ## Related components - [Text](/reference/ui-components/standard-components/text) - [Accordion](/reference/ui-components/standard-components/accordion) - [DescriptionList](/reference/ui-components/standard-components/description-list) # LoadingButton | UI components @ Learn about the LoadingButton component for use in UI extensions. $ reference & ui-components/standard-components --- # LoadingButton | UI components (BETA) The `LoadingButton` component renders a button with loading state options. It includes the same props as the [Button component](/reference/ui-components/standard-components/button) with a few additional props for managing loading state. ```js import React from 'react'; import { useState } from 'react'; import { Flex, Heading, LoadingButton, hubspot } from '@hubspot/ui-extensions'; hubspot.extend(({ actions }) => <Extension actions={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 ( <Flex direction="column" gap="md" align="start"> <LoadingButton loading={isFetching} onClick={handleClick}> Fetch Name </LoadingButton> {!isFetching && contactName !== '' && <Heading>{contactName}</Heading>} </Flex> ); } ``` <thead> <tr> <th>Prop</th> <th>Type</th> <th>Description</th> </tr> </thead> <tbody> <tr> <td>`href`</td> <td>String</td> <td> Include this prop to open a URL on click. Contains the following fields: <ul> <li>`url` (string): the URL that will open on click.</li> <li> `external` (boolean): set to `true` to open the URL in a new tab and display an external link icon. By default: <ul> <li> Links to HubSpot app pages will open in the same tab and will not include an icon. </li> <li> Links to non-HubSpot app pages will open in a new tab and include the icon. </li> </ul> </li> </ul> When a button includes both `href` and an `onClick` action, both will be executed on button click. </td> </tr> <tr> <td>`onClick`</td> <td>() => void</td> <td> A function that will be invoked when the button is clicked. It receives no arguments and it's return value is ignored. </td> </tr> <tr> <td>`disabled`</td> <td>Boolean</td> <td> When set to `true`, the button will render in a greyed-out state and cannot be clicked. </td> </tr> <tr> <td>`variant`</td> <td>`'primary'` | `'secondary'` (default) | `'destructive'`</td> <td> Sets the color of the button. See [variants section](#variants) for more information. </td> </tr> <tr> <td>`type`</td> <td>`'button'` (default) | `'reset'` | `'submit'`</td> <td>Sets the `role` HTML attribute of the button.</td> </tr> <tr> <td>`size`</td> <td> `'xs'`, `'extra-small'` | `'sm'`, `'small'` | `'med'`, `'medium'` (default) </td> <td>The size of the button.</td> </tr> <tr> <td>`loading`</td> <td>Boolean</td> <td> Set to `true` to display the loading indicator and disable the button. Default is `false`. </td> </tr> <tr> <td>`resultIconName`</td> <td>String</td> <td> Set to an [icon name](/reference/ui-components/standard-components/icon) to display an icon after loading. By default, will display a check mark. </td> </tr> <tr> <td>`overlay`</td> <td>Object</td> <td> Include a [Modal](/reference/ui-components/standard-components/modal) or [Panel](/reference/ui-components/standard-components/panel) component in this object to open it as an overlay on click. Learn more about [using overlays](/guides/crm/ui-extensions/sdk#open-an-overlay). </td> </tr> </tbody> </Table> ## Opening overlays Like the [Button](/reference/ui-components/standard-components/button), [Link](/reference/ui-components/standard-components/link), [Tag](/reference/ui-components/standard-components/tag), and [Image](/reference/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. ```js import React from 'react'; import { useState } from 'react'; import { Flex, Heading, LoadingButton, Panel, PanelBody, PanelSection, hubspot, } from '@hubspot/ui-extensions'; hubspot.extend(({ actions }) => <Extension actions={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 ( <Flex direction="column" gap="md" align="start"> <LoadingButton loading={isFetching} onClick={handleClick} overlayOptions={{ openBehavior: 'onLoadingFinish' }} overlay={ <Panel title={'Contact name '} id="my-panel"> <PanelBody> <PanelSection> {contactName !== '' && <Heading>{contactName}</Heading>} </PanelSection> </PanelBody> </Panel> } > Fetch name (overlay) </LoadingButton> </Flex> ); } ``` ## 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. 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](/reference/ui-components/standard-components/button-row) - [Button](/reference/ui-components/standard-components/button) - [](/reference/ui-components/crm-action-components/overview)[CrmActionButton](/reference/ui-components/crm-action-components/crm-action-button) - [Panel](/reference/ui-components/standard-components/panel) # LoadingSpinner @ Learn about the LoadingSpinner component for use in UI extensions. $ reference & ui-components/standard-components --- # LoadingSpinner | UI components (BETA) 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`. ```js import { LoadingSpinner } from '@hubspot/ui-extensions'; const Extension = () => { return <LoadingSpinner label="Loading..." />; }; ``` | Prop | Type | Description | | --- | --- | --- | | `label` | String | The text that displays next to 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. | | `layout` | `'inline'` (default) | `'centered'` | The position 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](/reference/ui-components/standard-components/alert) - [EmptyState](/reference/ui-components/standard-components/empty-state) - [ErrorState](/reference/ui-components/standard-components/error-state) # Modal @ Learn about the Modal component for use in UI extensions. $ reference & ui-components/standard-components --- # Modal | UI components (BETA) 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](/reference/ui-components/standard-components/button), [LoadingButton](/reference/ui-components/standard-components/loading-button), [Link](/reference/ui-components/standard-components/link), [Tag](/reference/ui-components/standard-components/tag), or [Image](/reference/ui-components/standard-components/image) component. To see an example of using overlays, check out [HubSpot's Overlay example project](/guides/crm/ui-extensions/sample-extensions/overview#overlay-example). 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. ```js import React from 'react'; import { Button, Modal, ModalBody, Text, hubspot, } from '@hubspot/ui-extensions'; hubspot.extend(() => <Extension />); const Extension = () => { 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.</Text> </ModalBody> </Modal> } > Open modal </Button> </> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `id` Required | String | The unique identifier for the modal. | | `width` | `'small'`, `'sm'` (default) | `'medium'`, `'md'` | `'large'`, `'lg'` | The width of the modal. | | `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. | | `aria-label` | String | The modal's accessibility label. | | `onOpen` | `() => void` | A function that will be invoked when the modal has finished opening. | | `onClose` | `() => void` | A function that will be invoked when the modal has finished closing. | ## Opening and closing modals By default, HubSpot handles opening the modal when the user clicks the parent [Button](/reference/ui-components/standard-components/button), [LoadingButton](/reference/ui-components/standard-components/loading-button), [Link](/reference/ui-components/standard-components/link), [Tag](/reference/ui-components/standard-components/tag), or [Image](/reference/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](/guides/crm/ui-extensions/sdk#registering-the-extension) in `hubspot.extend()` as seen in the example code below. Learn more about [opening and closing overlays](/guides/crm/ui-extensions/sdk#open-an-overlay). ```js 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> </> ); }; ``` - 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](/reference/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](/guides/crm/ui-extensions/sdk#open-an-overlay), including when to use a `Modal` or a `Panel`. ## Related components - [Panel](/reference/ui-components/standard-components/panel) - [Button](/reference/ui-components/standard-components/button) - [LoadingButton](/reference/ui-components/standard-components/loading-button) - [Display iframe modals using the UI extensions SDK](/guides/crm/ui-extensions/sdk) # MultiSelect @ Learn about the MultiSelect component for use in UI extensions. $ reference & ui-components/standard-components --- # MultiSelect | UI components (BETA) The `MultiSelect` component renders a dropdown menu select field where a user can select multiple values. Commonly used within the [Form](/reference/ui-components/standard-components/form) component. ```js import { Form, MultiSelect, Button } from '@hubspot/ui-extensions'; function MultiSelectControlledExample() { const [formValue, setFormValue] = useState([]); return ( <Form preventDefault={true} onSubmit={() => console.log(formValue, 'hola')}> <MultiSelect value={formValue} placeholder="Pick your Products" label="Select Mutiple Products" name="selectProduct" required={true} onChange={(value) => setFormValue(value)} options={[ { label: 'Amazing Product 1', value: 'p1' }, { label: 'Amazing Product 2', value: 'p2' }, { label: 'Amazing Product 3', value: 'p3' }, { label: 'Amazing Product 4', value: 'p4' }, { label: 'Amazing Product 5', value: 'p5' }, { label: 'Amazing Product 6', value: 'p6' }, ]} /> <Button type="submit">Submit</Button> </Form> ); } ``` | Prop | Type | Description | | --- | --- | --- | | `name` | String | The input's unique identifier. | | `label` | String | The text that displays above the dropdown menu. | | `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. | | `value` | String | number | The value of the input. | | `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. | | `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. | | `validationMessage` | String | The text to display if the input has an error. | | `onChange` | (value: (string | number)\[\]) => void | A callback function that is invoked when the value is committed. | ## Related components - [Form](/reference/ui-components/standard-components/form) - [DateInput](/reference/ui-components/standard-components/date-input) - [NumberInput](/reference/ui-components/standard-components/number-input) - [Select](/reference/ui-components/standard-components/select) # NumberInput @ Learn about the NumberInput component for use in UI extensions. $ reference & ui-components/standard-components --- # NumberInput | UI components (BETA) The `NumberInput` component renders a number input field. Commonly used within the [Form](/reference/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. ```js import { NumberInput } from '@hubspot/ui-extensions'; const Extension = () => { const [portalCount, setPortalCount] = useState(0); return ( <NumberInput label={'HubSpot Portal Count'} name="portalsNumber" description={'Number of active portals'} placeholder={'number of portals'} value={portalCount} onChange={(value) => setPortalCount(value)} /> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `name` | String | The input's unique identifier. | | `label` | String | The text that displays above the dropdown menu. | | `value` | String | number | The value of the input. | | `defaultValue` | Number | The value of the input on initial render. | | `description` | String | Text that describes the field's purpose. | | `required` | Boolean | When set to `true`, displays a required field indicator. | | `readOnly` | Boolean | When set to `true`, users will not be able to enter a value into the field. Set to `false` by default. | | `placeholder` | String | The text that appears in the input before a value is set. | | `tooltip` | String | The text that displays in a tooltip next to the label. | | `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. | | `validationMessage` | String | The text to display if the input has an error. | | `min` | Number | Sets the lower bound of the input. | | `max` | Number | Sets the upper bound of the input. | | `precision` | Number | Sets the number of digits to the right of the decimal point. | | `formatStyle` | `'decimal'` | `'percentage'` | Formats the input as a decimal or percentage. | | `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. | ## 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](/reference/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](/reference/ui-components/standard-components/form) - [DateInput](/reference/ui-components/standard-components/date-input) - [MultiSelect](/reference/ui-components/standard-components/multi-select) # Panel @ Learn about the Panel component for use in UI extensions. $ reference & ui-components/standard-components --- # Panel | UI components (BETA) The `Panel` component renders a panel overlay on the right side of the page and contains other components. Like the [Modal](/reference/ui-components/standard-components/modal) component, you'll include the `Panel` component in an `overlay` prop within a [Button](/reference/ui-components/standard-components/button), [LoadingButton](/reference/ui-components/standard-components/loading-button), [Link](/reference/ui-components/standard-components/link), [Tag](/reference/ui-components/standard-components/tag), or [Image](/reference/ui-components/standard-components/image) component. To see an example of using a panel in an extension, check out HubSpot's [Build a multi-step flow](/guides/crm/ui-extensions/sample-extensions/overview#build-a-multi-step-flow) or [Overlay example project](/guides/crm/ui-extensions/sample-extensions/overview#overlay-example). As of June 24, 2024, the method for including `Panel` components has changed. Moving forward: - Panels are nested within the components that open them. - Opening the panel is automatically handled by the parent component, rather than requiring reactions. Extensions using the old method will continue to function, but it's recommended to review the new functionality below and update your extensions as needed. The `Panel` component uses three subcomponents to control its design and content, which follows the general structure below: - `<Panel>`: the outermost container. It must be a top-level component. You cannot put a `Panel` inside another component, such as `Flex`. - `<PanelBody>`: the container that wraps the panel's content and makes it scrollable. Include only one `PanelBody` per `Panel`. - `<PanelSection>`: a container that adds padding and bottom margin to provide spacing between content. You can use [Flex](/reference/ui-components/standard-components/flex) and [Box](/reference/ui-components/standard-components/box) to further customize content layout. - `<PanelFooter>`: a sticky footer component at the bottom of the panel. Include only one `PanelFooter` per `Panel`. ```js import { Button, Panel, PanelSection, PanelBody, PanelFooter, Text, hubspot } from '@hubspot/ui-extensions'; hubspot.extend(() => <OverlayExampleCard />); const OverlayExampleCard = () => { 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.</Text> </PanelSection> </PanelBody> <PanelFooter> </PanelFooter> </Panel> } > Open panel </Button> </> ); }; ``` Below are the props available for `Panel` and `PanelSection`. ## Panel props | Prop | Type | Description | | --- | --- | --- | | `id` | String | A unique ID for the panel. | | `onOpen` | onOpen() => void | A function that will be invoked when the panel has finished opening. | | `onClose` | onClose() => void | A function that will be invoked when the panel has finished closing. | | `width` | `'sm'`, `'small'` (default) | `'md'`, `'medium'` | `'lg'`, `'large'` | The width of the panel. | | `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. | | `aria-label` | String | The panel's accessibility label. | ## PanelSection 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](/reference/ui-components/standard-components/button), [LoadingButton](/reference/ui-components/standard-components/loading-button), [Link](/reference/ui-components/standard-components/link), [Tag](/reference/ui-components/standard-components/tag), or [Image](/reference/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](/guides/crm/ui-extensions/sdk#registering-the-extension) in `hubspot.extend()` as seen in the example code below. Learn more about [opening and closing overlays](/guides/crm/ui-extensions/sdk#open-an-overlay). ```js 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> </> ); }; ``` - 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](/reference/ui-components/standard-components/box) - [Button](/reference/ui-components/standard-components/button) - [LoadingButton](/reference/ui-components/standard-components/loading-button) - [Divider](/reference/ui-components/standard-components/divider) - [Link](/reference/ui-components/standard-components/link) - [Image](/reference/ui-components/standard-components/image) - [Modal](/reference/ui-components/standard-components/modal) - [Tag](/reference/ui-components/standard-components/tag) - [Tile](/reference/ui-components/standard-components/tile)[](/reference/ui-components/standard-components/divider) # ProgressBar @ Learn about the ProgressBar component for use in UI extensions. $ reference & ui-components/standard-components --- # ProgressBar | UI components (BETA) 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. ```js import { ProgressBar } from '@hubspot/ui-extensions'; const Extension = () => { return ( <ProgressBar variant="warning" value={50} maxValue={200} showPercentage={true} /> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `title` | String | The text that displays above the progress bar. | | `showPercentage` | Boolean | When set to `true`, the progress bar will display the completion percentage. Default is `false`. | | `value` | Number | The number representing the progress so far. Default is `0`. | | `maxValue` | Number | The maximum value of the progress bar. Default is `100`. | | `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. | | `aria-label` | String | The accessibility label. | ## Variants Using the variant prop, you can set the following progress bar colors: - `success`: a green colored bar to indicate movement towards a positive goal or outcome. - `warning`: a yellow colored bar to indicate movement towards a negative outcome or limitation. - `danger`: a red colored 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](/reference/ui-components/standard-components/step-indicator) - [LoadingSpinner](/reference/ui-components/standard-components/loading-spinner) - [Statistics](/reference/ui-components/standard-components/statistics) # RadioButton @ Learn about the RadioButton component for use in UI extensions. $ reference & ui-components/standard-components --- # RadioButton | UI components (BETA) 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](/reference/ui-components/standard-components/toggle-group) component instead. ```js import { RadioButton } from '@hubspot/ui-extensions'; function Extension() { const [roleType, setRoleType] = useState( 'support' ); return ( <> <RadioButton checked={roleType === 'superAdmin'} name="roleType" description="Select to grant superpowers." onChange={() => { setRoleType('superAdmin'); }} > Super Admin </RadioButton> <RadioButton checked={roleType === 'support'} name="roleType" description="Select to assign a Support role." onChange={() => { setRoleType('support'); }} > Customer Support </RadioButton> </> ); } ``` | Prop | Type | Description | | --- | --- | --- | | `name` | String | The input's unique identifier. | | `value` | String | number | The radio button value. This value is not displayed, but is passed on the server side when submitted, along with `name`. | | `checked` | Boolean | Whether the radio button is currently selected. Default is `false`. | | `initialIsChecked` | Boolean | When set to `true`, the option will be selected by default. Default is `false`. | | `description` | String | Text that describes the field's purpose. | | `readonly` | Boolean | When set to `true`, users will not be able to enter a value into the field. Set to `false` by default. | | `variant` | `'sm'`, `'small'` | `'default'` (default) | The size of the checkbox. | | `inline` | Boolean | When set to `true`, arranges radio buttons side by side. Default is `false`. | | `onChange` | (checked: boolean, value: string) => void | A callback function that is invoked when the radio button is selected. Passes the new value. | ## Related components - [Checkbox](/reference/ui-components/standard-components/checkbox) - [ToggleGroup](/reference/ui-components/standard-components/toggle-group) - [TextArea](/reference/ui-components/standard-components/text-area) # Select @ Learn about the Select component for use in UI extensions. $ reference & ui-components/standard-components --- # Select | UI components (BETA) 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. ```js 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 ( <Form> <Select label="Best Bill & Ted Character?" name="best-char" tooltip="Please choose" description="Please choose" placeholder="Bill or Ted?" required={true} error={!isValid} validationMessage={validationMessage} onChange={(value) => { setName(value); if (!value) { setValidationMessage('This is required'); setIsValid(false); } else { setValidationMessage('Excellent!'); setIsValid(true); } }} options={options} /> </Form> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `name` | String | The input's unique identifier. | | `label` | String | The text that displays above the input. | | `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. | | `value` | String | number | boolean | The value of the input. | | `variant` | `input` (default) | `transparent` | The visual style of the button | | `required` | Boolean | When set to `true`, displays a required field indicator. Default is `false`. | | `readOnly` | Boolean | When set to `true`, users will not be able to fill the input field. Default is `false`. | | `tooltip` | String | The text that displays in a tooltip next to the label. | | `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. | | `validationMessage` | String | The text to display if the input has an error. | | `onChange` | (value: string) => void | A callback function that is invoked when the value is committed. | ## 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](/reference/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](/reference/ui-components/standard-components/form) - [Text](/reference/ui-components/standard-components/text) - [TextArea](/reference/ui-components/standard-components/text-area) # Statistics @ Learn about the Statistics component for use in UI extensions. $ reference & ui-components/standard-components --- # Statistics | UI components (BETA) 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). ```js import { Statistics, StatisticsItem, StatisticsTrend, } from '@hubspot/ui-extensions'; const Extension = () => { return ( <Statistics> <StatisticsItem label="Item A Sales" number="10000"> <StatisticsTrend direction="decrease" value="200%" /> </StatisticsItem> <StatisticsItem label="Item B Sales" number="100000"> <StatisticsTrend direction="increase" value="100%" /> </StatisticsItem> </Statistics> ); }; ``` ## StatisticsItem 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. | ## StatisticsTrend props | Prop | Type | Description | | --- | --- | --- | | `direction` | `'increase'` (default) \| `'decrease'` | The direction of the trend arrow. | | `value` | String | The text to be displayed as the trend value. | | `color` | `'red'` \| `'green'` | The color of the trend arrow. | ## 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](/reference/ui-components/standard-components/table) - [DescriptionList](/reference/ui-components/standard-components/description-list) - [CrmPropertyList](/reference/ui-components/crm-data-components/crm-property-list) # StatusTag | UI components @ Learn about the StatusTag component for use in UI extensions. $ reference & ui-components/standard-components --- # StatusTag | UI components (BETA) 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. ```js import { Flex, Heading, StatusTag, Text, hubspot, } from '@hubspot/ui-extensions'; const Extension = () => { return ( <Flex direction="column" gap="sm"> <Heading>Account status</Heading> <Flex direction="column" gap="sm"> <Text format={{ fontWeight: 'bold' }}> Billing: <StatusTag variant="success">Good standing</StatusTag> </Text> <Text format={{ fontWeight: 'bold' }}> Outreach:{' '} <StatusTag variant="warning">> 2 weeks since last check-in</StatusTag> </Text> <Text format={{ fontWeight: 'bold' }}> Support:{' '} <StatusTag variant="danger">1 escalated support ticket</StatusTag> </Text> <Text format={{ fontWeight: 'bold' }}> Upgrades: <StatusTag>No upgrades</StatusTag> </Text> <Text format={{ fontWeight: 'bold' }}> Referrals: <StatusTag variant="info">1 recent referral</StatusTag> </Text> </Flex> </Flex> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `variant` | `'default'` (default) | `'info'` | `'danger'` | `'warning'` | `'success'` | The color of the dot indicator. See the [variants section](#variants) for more information. | | `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. | | `showRemoveIcon` | Boolean | When set to `true`, the status tag will include a small, clickable x icon to remove it. Default is `false`. | | `onRemoveClick` | () => void; | A function that will be invoked when the remove icon is clicked. | ## 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](/reference/ui-components/standard-components/tag) - [Alert](/reference/ui-components/standard-components/alert) - [Icon](/reference/ui-components/standard-components/icon) - [ProgressBar](/reference/ui-components/standard-components/progress-bar) # StepIndicator @ Learn about the StepIndicator component for use in UI extensions. $ reference & ui-components/standard-components --- # StepIndicator | UI components (BETA) The `StepIndicator` component renders an indicator to show the current step of a multi-step process. ```js import { Flex, Box, StepIndicator, Button } from '@hubspot/ui-extensions'; hubspot.extend(() => <Extension />); function Extension() { const [currentStep, setCurrentStep] = useState(0); return ( <Flex direction="column" gap="md"> <StepIndicator currentStep={currentStep} stepNames={['First', 'Second', 'Third']} /> <Box> <Button onClick={() => setCurrentStep(currentStep - 1)}> Previous </Button> <Button onClick={() => setCurrentStep(currentStep + 1)}>Next</Button> </Box> </Flex> ); } ``` | 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. | | `stepNames` | Array | An array containing the name of each step. | | `variant` | `'flush'` | `'default'` (default) | `'compact'` | Sets component spacing.<ul><li>`compact`: only shows the title of the currently active step.</li><li>`flush`: only shows the title of the currently active step and removes left and right margin.</li></ul> | | `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. | ## 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](/reference/ui-components/standard-components/progress-bar) - [LoadingSpinner](/reference/ui-components/standard-components/loading-spinner) - [Toggle](/reference/ui-components/standard-components/toggle) # StepperInput @ Learn about the StepperInput component for use in UI extensions. $ reference & ui-components/standard-components --- # StepperInput | UI components (BETA) 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. ```js import { StepperInput } from '@hubspot/ui-extensions'; return ( <StepperInput min={5} max={20} minValueReachedTooltip="You need to eat at least 5 cookies." maxValueReachedTooltip="More than 20 cookies is a bit much." label="Number of cookies to eat" name="cookiesField" description={'I want cookies'} value={cookies} stepSize={5} onChange={(value) => { setCookieCount(value); }} /> ); ``` | Prop | Type | Description | | --- | --- | --- | | `name` | String | The input's unique identifier. | | `label` | String | The text that displays above the input. | | `stepSize` | Number | The amount that the current value will increase or decrease by. Default is `1`. | | `min` | Number | The lowest number allowed in the input. | | `max` | Number | The highest number allowed in the input. | | `minValueReachedTooltip` | String | Text that will appear in a tooltip when the user has reached the minimum value. | | `maxValueReachedTooltip` | String | Text that will appear in a tooltip when the user has reached the maximum value. | | `precision` | Number | The number of digits to the right of the decimal point. | | `formatStyle` | `'decimal'` (default) | `'percentage'` | Formats the number as a decimal or percentage. | | `value` | String | The value of the input. | | `defaultValue` | String | The default input value. | | `placeholder` | String | Text that appears in the input when no value is set. | | `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. | | `readOnly` | Boolean | When set to `true`, the checkbox cannot be selected. Default is `false`. | | `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. | | `validationMessage` | String | The text to display if the input has an error. | | `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. | | `onBlur` | (value: number) => void | A function that is called and passes the value when the field loses focus. | ## Related components - [Form](/reference/ui-components/standard-components/form) - [DateInput](/reference/ui-components/standard-components/date-input) - [NumberInput](/reference/ui-components/standard-components/number-input) # Table | UI components @ Learn about the Table component for use in UI extensions. $ reference & ui-components/standard-components --- # Table | UI components (BETA) 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](/reference/ui-components/design/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. ```js import { Table, TableHead, TableRow, TableHeader, TableBody, TableCell, } from '@hubspot/ui-extensions'; const Extension = () => { return ( </TableRow> </TableHead> </TableRow> </TableRow> </TableRow> </TableRow> </TableBody> </Table> ); }; ``` ## Table props | Prop | Type | Description | | --- | --- | --- | | `bordered` | Boolean | When set to `false`, the table will not include borders. Default is `true`. | | `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. | ## TableHeader 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.<ul><li>`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.</li><li>`max`: the content will expand to occupy the maximum available width without overflowing.</li><li>`auto`: the content will adjust its width based on the available space without overflowing.</li></ul> | ## TableCell props | Prop | Type | Description | | --- | --- | --- | | `align` | `'center'` | `'left'` | `'right'` | Sets the alignment of a table cell. | | `width` | Number | `'min'` | `'max'` | `'auto'` | Sets the width of a table cell.<ul><li>`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.</li><li>`max`: the content will expand to occupy the maximum available width without overflowing.</li><li>`auto`: the content will adjust its width based on the available space without overflowing.</li></ul> | ## 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. | | `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`. | | `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. | ## 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. ```js 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(() => <Extension />); 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, }, ]; const DEFAULT_STATE = { name: 'none', yearsOnAir: 'none', emmys: 'none', }; function Extension() { const [data, setData] = useState(ORIGINAL_DATA); const [sortState, setSortState] = useState({ ...DEFAULT_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_STATE, [fieldName]: sortDirection }); setData(dataClone); } return ( <TableHeader sortDirection={sortState.name} onSortChange={(sortDirection) => handleOnSort('name', sortDirection) } > Series </TableHeader> <TableHeader sortDirection={sortState.yearsOnAir} onSortChange={(sortDirection) => handleOnSort('yearsOnAir', sortDirection) } > Years on air </TableHeader> <TableHeader sortDirection={sortState.emmys} onSortChange={(sortDirection) => handleOnSort('emmys', sortDirection) } > Emmys </TableHeader> </TableRow> </TableHead> {data.map(({ name, yearsOnAir, emmys }) => { return ( </TableRow> ); })} </TableBody> </TableRow> </TableFooter> </Table> ); } ``` | Prop | Type | Description | | --- | --- | --- | | `sortDirection` | `'none'` (default) | `'ascending'` | `'descending'` | Visually indicates with an arrow which way the rows are sorted. | | `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). | | `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`. | ## 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](/reference/ui-components/standard-components/empty-state) when there are no results to display. ## Related components - [DescriptionList](/reference/ui-components/standard-components/description-list) - [Statistics](/reference/ui-components/standard-components/statistics) - [CrmPropertyList](/reference/ui-components/crm-data-components/crm-property-list) # Tag @ Learn about the Tag component for use in UI extensions. $ reference & ui-components/standard-components --- # Tag | UI components (BETA) 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. ```js import { Tag } from '@hubspot/ui-extensions'; const Extension = () => { return ( <Tag variant="success" onClick={() => { console.log('Tag clicked!'); }} inline={true} > Success ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `variant` | `'default'` (default) | `'warning'` | `'success'` | `'error'` | `'info'` | The color of the alert. See the [variants section](#variants) for more information. | | `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](/reference/ui-components/standard-components/modal) or [Panel](/reference/ui-components/standard-components/panel) component in this object to open it as an overlay on click. Learn more about [using overlays](/guides/crm/ui-extensions/sdk#open-an-overlay). | ## 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. ```js <Text> Some text over here </Text> ``` ```js <Text> 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. </Text> ``` ```js <Text> 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. </Text> ``` ## 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](/reference/ui-components/standard-components/toggle) - [Alert](/reference/ui-components/standard-components/alert) - [ProgressBar](/reference/ui-components/standard-components/progress-bar) # TextArea @ Learn about the TextArea component for use in UI extensions. $ reference & ui-components/standard-components --- # TextArea | UI components (BETA) 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. ```js 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 ( <Form> <TextArea label="Description" name="description" tooltip="Provide as much detail as possible" description="Please include a link" placeholder="My description" required={true} error={!isValid} validationMessage={validationMessage} onChange={(value) => { setDescription(value); }} onInput={(value) => { if (!value.includes('http')) { setValidationMessage('A link must be included.'); setIsValid(false); } else { setValidationMessage('Valid description!'); setIsValid(true); } }} /> </Form> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `name` | String | The input's unique identifier. | | `label` | String | The text that displays above the input. | | `value` | String | The value of the input. | | `description` | String | Text that describes the field's purpose. | | `tooltip` | String | The text that displays in a tooltip next to the label. | | `placeholder` | String | Text that appears in the input when no value is set. | | `required` | Boolean | When set to `true`, displays a required field indicator. Default is `false`. | | `cols` | Number | The visible width of the text field in average character widths. | | `rows` | Number | The number of visible text lines for the text field. | | `maxLength` | Number | The maximum number of characters (UTF-16 code units) that the user can enter. If not specified, maximum length is unlimited. | | `resize` | `'vertical'` | `'horizontal'` | `'both'` (default) | `'none'` | Sets whether the text field is resizable, and if so, in which directions. | | `readOnly` | Boolean | When set to `true`, the checkbox cannot be selected. Default is `false`. | | `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. | | `validationMessage` | String | The text to display if the input has an error. | | `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. | | `onBlur` | (value: number) => void | A function that is called and passes the value when the field loses focus. | | `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). | ## 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](/reference/ui-components/standard-components/text) - [Select](/reference/ui-components/standard-components/select) - [Form](/reference/ui-components/standard-components/form) # Text @ Learn about the Text component for use in UI extensions. $ reference & ui-components/standard-components --- # Text | UI components (BETA) The `Text` component renders text with formatting options. ```js import { Text } from '@hubspot/ui-extensions'; const Extension = () => { return ( <> <Text truncate={{ tooltipText:'string', maxWidth: 68 }}>Truncated text</Text> <Text>Plain text</Text> <Text format={{ fontWeight: 'bold' }}>Bold</Text> <Text format={{ italic: true }}>Italics</Text> <Text format={{ fontWeight: 'bold', italic: true }}> Bold and Italic text </Text> <Text format={{ lineDecoration: 'strikethrough' }}> Strikethrough Text </Text> <Text variant="microcopy"> Microcopy text <Text inline={true} format={{ fontWeight: 'bold' }}> with inner bold </Text> </Text> </> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `format` | Object | Text formatting options, which include:<ul><li>`{ fontWeight: 'bold' }`</li><li>`{ fontWeight: 'demibold' }`</li><li>`{ italic: true }`</li><li>`{ lineDecoration: 'strikethrough' }`</li><li>`{ lineDecoration: 'underline' }`</li></ul>See the [variants section](#variants) for more information. | | `variant` | `'bodytext'` (default) | `'microcopy'` | The style of text to display. 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.<ul><li>`false` (default): text is not truncated.</li><li>`true`: truncates text to a single line. Full text will display in a tooltip on hover.</li></ul>Alternatively, set this prop to one of the following objects to specify truncate options:<ul><li>`{tooltipText: 'string'}`: truncates the string and sets the contents of the tooltip.</li><li>`{maxWidth: number}`: sets the width of the line in pixels.</li></ul> | ## Variants 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](/reference/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](/reference/ui-components/standard-components/alert) would fit better. - **DON'T:** use text components in place of headers, alerts, and errors. ## Related components - [Heading](/reference/ui-components/standard-components/heading) - [Link](/reference/ui-components/standard-components/link) - [List](/reference/ui-components/standard-components/list) # Tile @ Learn about the Tile component for use in UI extensions. $ reference & ui-components/standard-components --- # Tile | UI components (BETA) The `Tile` component renders a square tile that can contain other components. Use this component to create groups of related components. ```js import { Tile, Text } from '@hubspot/ui-extensions'; const Extension = () => { return ( <> <Tile> <Text>This is the default tile. It has a small amount of left padding</Text> </Tile> <Tile compact={true}> <Text>This is a compact tile. It reduces the amount of padding within.</Text> </Tile> <Tile flush={true}> <Text>This is a flush tile. It has no left padding</Text> </Tile> </> ); }; ``` | 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](/reference/ui-components/standard-components/box) - [Divider](/reference/ui-components/standard-components/divider) - [Accordion](/reference/ui-components/standard-components/accordion) # ToggleGroup @ Learn about the ToggleGroup component for use in UI extensions. $ reference & ui-components/standard-components --- # ToggleGroup | UI extension components (BETA) 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. ```js 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 ( <ToggleGroup name="toggle-checkboxes" label="Toggle these things" error={false} options={options} tooltip="Here's a secret tip." validationMessage="Make sure you do the thing correctly." required={false} inline={false} toggleType="checkboxList" variant="default" /> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `name` | String | The input's unique identifier. | | `label` | String | The text that displays above the toggles. | | `options` | | `toggleType` | `'radioButtonList'` | `'checkboxList'` (default) | The type of toggle, whether checkboxes or radio buttons. Radio buttons only allow one option to be selected. | | `variant` | `'default'` (default) | `'small'` | The size of the toggle. | | `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. | | `value` | String | The value of the toggle group.<ul><li>Accepts a string when `toggleType` is `radioButtonList`.</li><li>Accepts an array when `toggleType` is `checkboxList`.</li></ul> | | `required` | Boolean | When set to `true`, displays a required indicator next to the toggle group. Default is `false`. | | `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. | | `inline` | Boolean | When set to `true`, stacks the options horizontally. Default is `false`. | | `readonly` | Boolean | When set to `true`, users will not be able to select the toggle. Default is `false`. | | `onChange` | (checked: boolean) => void | A function that is invoked when the toggle is clicked. | ## 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. <table> <tbody> <tr> <td> <p> <img alt="toggle-group-checklist-variant" src="https://developers.hubspot.com/hs-fs/hubfs/Knowledge_Base_2023/design-guidelines-togglegroup-styles_3.png" /> </p> </td> </tr> <tr> <td> <code>toggleType='checkboxList'</code> (default) </td> </tr> <tr> <td> <p> <img alt="toggle-group-radio-buttons" src="https://developers.hubspot.com/hs-fs/hubfs/Knowledge_Base_2023/design-guidelines-togglegroup-styles_4.png" /> </p> </td> </tr> <tr> <td> <code>toggleType='radioButtonList'</code> </td> </tr> <tr> <td> <p> <img alt="toggle-group-inline" src="https://developers.hubspot.com/hs-fs/hubfs/Knowledge_Base_2023/design-guidelines-togglegroup-styles_1.png" /> </p> </td> </tr> <tr> <td> <code>inline={true}</code> </td> </tr> </tbody> </table> ## 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](/reference/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](/reference/ui-components/standard-components/checkbox) - [RadioButton](/reference/ui-components/standard-components/radio-button) - [Toggle](/reference/ui-components/standard-components/toggle) # Toggle @ Learn about the Toggle component for use in UI extensions. $ reference & ui-components/standard-components --- # Toggle | UI components (BETA) The `Toggle` component renders a boolean toggle switch that can be configured with sizing, label position, read-only, and more. ```js import { Toggle } from '@hubspot/ui-extensions'; const Extension = () => { return ( <Toggle size="md" label="My toggle" labelDisplay="top" initialIsChecked={true} /> ); }; ``` | Prop | Type | Description | | --- | --- | --- | | `name` | String | The input's unique identifier. | | `label` | String | The text that displays above the input. | | `size` | `'xs'`, `'extra-small'` | `'sm'`, `'small'` | `'md'`, `'medium'` (default) | The size of the toggle. Only `'md'` / `'medium'` sized toggles can display text on the toggle (ON, OFF). All other sizes will hide checked/unchecked text. | | `labelDisplay` | `'inline'` (default) | `'top'` | `'hidden'` | The display option for the toggle label. | | `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. | | `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. | | `readonly` | Boolean | When set to `true`, users will not be able to select the toggle. Default is `false`. | | `onChange` | (checked: boolean) => void | A function that is invoked when the toggle is clicked. | ## 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 `'medium'`. Shrink the toggle size by setting this prop to `'xs'`/`'extra-small'` or `'sm'`/`'small'`. Note that only medium toggles will display ON/OFF status text. ## Related components - [Tag](/reference/ui-components/standard-components/tag) - [ToggleGroup](/reference/ui-components/standard-components/toggle-group) - [ProgressBar](/reference/ui-components/standard-components/progress-bar) # Send custom event completions @ Custom events allow you to track advanced activity via a JavaScript or HTTP API. The Events API can be used to get details about your events. $ guides & api/analytics-and-events/custom-events --- # Send custom event completions Custom events are account-defined events that store event details in event properties. In addition to [customizing your tracking code](/reference/api/analytics-and-events/tracking-code) to send event data to HubSpot, you can also send event completion data via this API. Below, learn how to use the API to create custom events and send/retrieve custom event data. ## Define the event To send event completion data to HubSpot, you first need to define the event itself, including its metadata, CRM object associations, and properties. You can define events using the [custom event definition API](/guides/api/analytics-and-events/custom-events/custom-event-definitions), or if you have a _**Marketing Hub** Enterprise_ subscription you can [create the event in HubSpot](https://knowledge.hubspot.com/analytics-tools/create-codeless-custom-behavioral-events). When creating the event, HubSpot will provide the option to include a set of [default event properties](/guides/api/analytics-and-events/custom-events/custom-event-definitions#hubspot-s-default-event-properties) that you can use to store event data. You can also [create additional properties](/guides/api/analytics-and-events/custom-events/custom-event-definitions#define-new-properties) for the event. These properties can be created or edited at any time. Once you’ve set up your event, you can send data to it through the API or by [customizing your HubSpot tracking code](/reference/api/analytics-and-events/tracking-code). ## Send event data To send event data to HubSpot, make a `POST` request to `https://api.hubspot.com/events/v3/send` with your event data in the request body. Before sending event data, review the limits below, as exceeding these limits will result in an error. ```json { "eventName": "pe1234567_login_event", "objectId": "608051", "occurredAt": "2024-06-28T12:09:31Z", "properties": { "hs_city": "Cambridge", "hs_country": "United States", "hs_page_id": "53005768010", "hs_page_content_type": "LANDING_PAGE", "hs_device_type": "PDA;Smartphone", "hs_touchpoint_source": "DIRECT_TRAFFIC" } } ``` | Parameter | Type | Description | | --- | --- | --- | | `eventName` | String | The internal name of the event. You can find this by [querying your existing event definitions](/guides/api/analytics-and-events/custom-events/custom-event-definitions#get-existing-event-definitions) or [within the HubSpot app](https://knowledge.hubspot.com/reports/create-custom-behavioral-events-with-the-code-wizard#find-internal-name). | | `objectId` | String | The ID of the CRM record that the event will be associated with. For contacts, you can alternatively use the `email` or `utk` field to identify the contact by email address or [HubSpot usertoken](/reference/api/analytics-and-events/tracking-code). All other object types require `objectId`, unless a custom matching ID is defined for the event. If a `customMatchingId` is defined for the event, HubSpot will automatically set or override the `objectId` based on the configured mapping. Learn more in the [custom event definitions guide](/guides/api/analytics-and-events/custom-events/custom-event-definitions#create-an-event-definition). | | `occurredAt` | String | By default, HubSpot will set the event completion timestamp to the time that the request is sent. To specify the time of event completion, include a timestamp in an `occurredAt` field in the `POST` request body ([ISO 8601 format](https://en.wikipedia.org/wiki/ISO_8601)). This can be especially helpful for backdating event data to more accurately reflect real-life event completion. | | `properties` | Object | The event properties to send data to. This can include [HubSpot's default event properties](/guides/api/analytics-and-events/custom-events/custom-event-definitions#hubspot-s-default-event-properties) or any [custom properties](/guides/api/analytics-and-events/custom-events/custom-event-definitions#define-new-properties) you've defined for the event. Most default event properties are string properties, but you can review all event properties by either [querying the event definition](/guides/api/analytics-and-events/custom-events/custom-event-definitions#get-existing-event-definitions) or navigating to the event in HubSpot. If a custom matching ID is [set up for the event](/guides/api/analytics-and-events/custom-events/custom-event-definitions#create-an-event-definition), you may omit the `objectId`. HubSpot will attempt to link the event with a CRM object by setting the `objectId` on the event based on the configured mapping. Learn more about [event properties](#event-properties) below. | Exceeding any of the following limits will result in a failed request: - The property label and internal name are limited to 50 characters. - URL and referrer properties can receive up to 1,024 characters, while all other properties can receive up to 256 characters. - Each event completion can contain data for up to 50 properties. - Property internal names must start with a letter and contain only lowercase letters a-z, numbers 0-9, and underscores. - Properties with the same internal name after lowercasing are considered duplicates, and only one of the properties will be used on completion. HubSpot will sort in ascending lexicographical order and keep the last property seen among the first 50 properties. - There is a limit of 500 unique event definitions per account. - There is a limit of 30 million event completions per month. ## Retrieve event data To [retrieve event data](/reference/api/analytics-and-events/event-analytics#get-%2Fevents%2Fv3%2Fevents%2F), make a `GET` request to `/events/v3/events`. - To return all event completions for a specific event, include the `eventType` parameter along with the internal event name (e.g., `pe123456_custom_event`). You can retrieve all event types using the [event analytics API](/reference/api/analytics-and-events/event-analytics#get-%2Fevents%2Fv3%2Fevents%2Fevent-types). - To return event completions for a specific object, include the `objectType` parameter along with either the `objectId` or `objectProperty.<property>` parameters. The `objectType` should specify the type of CRM object (e.g., `contact`), while the other parameters specify the unique identifier value for the object (either record ID or a unique identifier property value). For contacts, you can use `email` as a unique identifier property. For example, to retrieve all events completed by a specific contact, your request URL could be: `/events/v3/events?objectType=contact&objectId=111111`. Alternatively, you can use the contact's email address: `/events/v3/events?objectType=contacts&objectProperty.email=bilbo@shire.com` To filter the results by event completions with a specific event property value, you can include the `property.<propertyName>` parameter. For example, to retrieve page visit events for your homepage, your request URL might be: `/events/v3/events?eventType=e_visited_page&property.hs_page_title=home` For property values with spaces, replace the spaces with either `%20` or `+`. For example: `property.hs_page_title=home+page`. Learn more about available parameters in the [reference documentation](/reference/api/analytics-and-events/event-analytics#get-%2Fevents%2Fv3%2Fevents%2F). ## Event properties Event completion data is stored in event properties, either in the set of [default event properties](/guides/api/analytics-and-events/custom-events/custom-event-definitions#hubspot-s-default-event-properties) or in [custom-defined properties](/guides/api/analytics-and-events/custom-events/custom-event-definitions#define-new-properties). When sending event data, include a `properties` object with key-value pairs for the properties you want to update along with the property values to store. ```json "properties": { "property1": "string", "property2": "string", "property3": "string" } ``` The values you send will depend on the type of event property. Most of the default event properties are single-line text (string). However, you can create custom properties of any type for each event. Review the table below when formatting property values. | Property type | Description | | --- | --- | | `enumeration` | A string representing a set of options. When sending multiple values, separate them with a semicolon. In HubSpot, this type corresponds to dropdown select, radio select, and multiple checkbox properties. | | `date` | A timestamp in the form of epoch milliseconds or ISO8601. In HubSpot, this type corresponds to date picker properties. | | `string` | A plain text string limited to 65,536 characters. In HubSpot, this type corresponds to single-line and multi-line text properties. | | `number` | A number value containing numeric digits and at most one decimal. In HubSpot, this type corresponds to number and calculation properties. | To view an event's available properties: - In your HubSpot account, navigate to **Data Management** > **Custom Events**. - In the table, click the **name** of the event. - At the top, click the **Properties** tab. - In the properties table, view the property type under the name of the property. ## Attribution reporting JavaScript events such as [clicked element](https://knowledge.hubspot.com/analytics-tools/create-codeless-custom-behavioral-events) and [visited URL](https://knowledge.hubspot.com/analytics-tools/create-codeless-custom-behavioral-events) events are automatically populated with asset type and interaction data for attribution reporting. To include the same data for manually tracked events, you'll need to manually include the data in the request body using event properties. Learn more about [analyzing custom events](https://knowledge.hubspot.com/analytics-tools/analyze-custom-behavioral-events). Below, learn about the available values for asset types and interaction sources, along with example requests. ### Asset type To attribute a specific asset type to a custom behavioral event request, include the `hs_page_content_type` property in the request body. For example: ```json { "eventName": "pe1234567_manually_tracked_event", "properties": { "hs_page_id": "53005768010", "hs_page_content_type": "LANDING_PAGE" }, "objectId": "6091051" } ``` You can also use the `hs_asset_type` property. If both `hs_page_content_type` and `hs_asset_type` are included in one request, `hs_page_content_type` will override the `hs_asset_type` value. HubSpot's standard content types, such as landing pages and blog posts, can be represented with the following values: | Value | Description | | ------------------- | --------------------------------------------- | | `STANDARD_PAGE` | An interaction with a website page. | | `LANDING_PAGE` | An interaction with a landing page. | | `BLOG_POST` | An interaction with a blog post. | | `KNOWLEDGE_ARTICLE` | An interaction with a knowledge base article. | For all other types of assets, use the following values: | Value | Description | | --- | --- | | `AD` | An interaction with an ad, such as a Facebook or Google ad. | | `CALL` | An interaction with a call. | | `CONTACT_IMPORT` | An interaction via a contact import. | | `CONVERSATION` | An interaction related to a HubSpot conversation. | | `CUSTOM_BEHAVIORAL_EVENT_NAME` | The internal name of a custom event, such as `pe123456_manually_tracked_event`. | | `EMAIL` | An interaction with an email. | | `EXTERNAL_PAGE` | An interaction with an external page. | | `INTEGRATIONS` | An interaction via an integration. | | `MARKETING_EVENT` | An interaction with a [marketing event](/guides/api/marketing/marketing-events). | | `MEDIA_BRIDGE` | An interaction via the [media bridge](/guides/api/cms/media-bridge). | | `MEETING` | An interaction with a meeting. | | `SALES_EMAIL` | An interaction with a 1:1 sales email. | | `SEQUENCE` | An interaction with a sequence. | | `SOCIAL_POST` | An interaction with a social media post. | | `OTHER` | An interaction with an asset not in one of the above categories. | ### Asset title To attribute a custom event to an asset, include the `hs_page_title` or `hs_asset_title` property in your request with the name of the asset formatted as a string. For example: `hs_page_title:` ```json { "eventName": "pe1234567_manually_tracked_event", "properties": { "hs_page_title": "Sweepstakes Sign Up", "hs_page_content_type": "LANDING_PAGE" }, "objectId": "6091051" } ``` ### Interaction sources To attribute a custom behavioral event to a specific source, include the `hs_touchpoint_source` property in your request with one of the following values: | Value | Description | | --- | --- | | `CONVERSATION` | The interaction source is a conversation. | | `DIRECT_TRAFFIC` | The interaction source is direct traffic. | | `EMAIL_MARKETING` | The interaction source is a marketing email. | | `HUBSPOT_CRM` | The interaction source is the HubSpot CRM. | | `INTEGRATION` | The interaction source is an integration. | | `MARKETING_EVENT` | The interaction source is a [marketing event](/guides/api/marketing/marketing-events). | | `OFFLINE` | The interaction source is offline. | | `ORGANIC_SEARCH` | The interaction source is organic search. | | `OTHER_CAMPAIGNS` | The interaction source is from an uncategorized campaign. | | `PAID_SEARCH` | The interaction source is a paid search ad. | | `PAID_SOCIAL` | The interaction source is a paid social ad. | | `REFERRALS` | The interaction source is a referral. | | `SALES` | The interaction source is sales. | | `SOCIAL_MEDIA` | The interaction source is social media (not a paid social ad). | # Define custom events @ Learn more about creating custom events using HubSpot's API. $ guides & api/analytics-and-events/custom-events --- # Define custom events Custom events enable you to define and track events that are unique to your business, such as events on your site or in an app. You can configure events to store information within properties, which you can then use across HubSpot's tools. To send [custom event](/guides/api/analytics-and-events/custom-events/custom-event-completions) data to HubSpot, you first need to define the event. This is similar to custom CRM objects, where you first need to define the custom object before you can create individual records for that object. An event definition includes details such as its metadata, CRM object associations, and properties. Below, learn more about creating and managing event definitions using the API. To learn how to create event definitions without using the API, check out [HubSpot's Knowledge Base](https://knowledge.hubspot.com/reports/create-custom-behavioral-events-with-the-code-wizard). ## Create an event definition To create the custom event schema, make a `POST` request to `events/v3/event-definitions`. In the request body, include definitions for your event schema, including its label, name, CRM object associations, and custom properties. The request body below provides a basic example of an event definition: ```json { "label": "My event label", "name": "unique_event_name", "description": "An event description that helps users understand what the event tracks.", "primaryObject": "COMPANY", "propertyDefinitions": [ { "name": "choice-property", "label": "Choice property", "type": "enumeration", "options": [ { "label": "Option 1", "value": "1" }, { "label": "Option 2", "value": "2" } ] }, { "name": "string-property", "label": "String property", "type": "string" } ], "includeDefaultProperties": true } ``` If you want to automatically link event completions to records of a specified CRM object type, you can include a `customMatchingId` in your `POST` request. Within this field, define a `primaryObjectRule` object with two fields: the unique object property that you've set up beforehand as the `targetObjectPropertyName`, and one of the properties you defined in the `propertyDefinitions` from your event definition. For example, the following request body specifies a `customMatchingId` that matches on a CRM object property name of `"unique_object_property"` and the event property name of `"string_property"`: ```json { "label": "My event label", "name": "unique_event_name", "description": "An event description that helps users understand what the event tracks.", "primaryObject": "COMPANY", "propertyDefinitions": [ { "name": "string-property", "label": "String property", "type": "string" } ], "customMatchingId": { "primaryObjectRule": { "targetObjectPropertyName": "unique_object_property", "eventPropertyName": "event_property" } } } ``` A full list of available parameters for the request body are detailed in the table below: | Parameter | Type | Description | | --- | --- | --- | | `label` | String | The event's human readable label, which will display in HubSpot (up to 100 characters). Long labels may be cut off in certain parts of HubSpot's UI. | | `name` | String | The unique, internal name of the event, which you'll use to reference the event through the API. If no value is provided, HubSpot will automatically generate one based on the label.<ul><li>This property cannot be changed after the event definition is created.</li><li>This property can only contain lowercase letters, numbers, underscores, and hyphens (up to 50 characters).</li><li>The first character must be a letter.</li></ul> | | `description` | String | The event's description, which will display in HubSpot. | | `primaryObject` | String | The type of CRM object that event data will be associated with. Event completions will appear on the CRM records of that object type. Can be one of: `"CONTACT"` (default), `"COMPANY"`, `"DEAL"`, `"TICKET"`, `"<CUSTOM_OBJECT_NAME>"`. This cannot be changed after the event definition is created. | | `propertyDefinitions` | Array | In addition to the [HubSpot default event properties](#hubspot-s-default-event-properties), you can include this array to define custom event properties (up to 50). For each property object, include the following fields:<ul><li>`name`: the property's internal name, which you'll use when sending event data through the API.</li><li>`label`: the property's in-app label.</li><li>`type`: the [type of property](#event-properties). Default is `string`.</li><li>`options`: for `enumeration` type properties, this array defines the available values. Must include both a `label` and `value` for each option.</li><li>`description`: text that describes the property.</li></ul> | | `customMatchingId` | Object | As an alternative to including the target object's `objectId` in the event completion data, this optional field defines a rule to automatically link event completions to records of the specified CRM object type. This is done by matching the value of a property in the event data with the value of a <u>unique</u> property on the target object. This object must include a nested `primaryObjectRule` object, which in turn must include two fields: <ul><li>`targetObjectPropertyName`: a string that specifies the name of the unique property on the target CRM object to use for matching.</li><li>`eventPropertyName`: a string that specifies the name of the property in the custom event data to use for matching.</li></ul> | | `includeDefaultProperties` | Boolean | An optional field that specifies whether the event should include the set of [default event properties](#hubspot-s-default-event-properties). If no value is provided, this field will automatically be set to `true`. | ## Event properties Custom event properties are used to store information on individual custom event completions. These properties should be used when appropriate for sending event completions, but they are not required for an event completion to be valid. For each event definition, HubSpot provides a default set of 32 properties. In addition, You can create up to 50 custom properties per event definition. Properties can be of the following types: - `string`: a property that receives plain text strings. If the property name contains the words `url`, `referrer`, or `link`, the property value can be up to 1024 characters. Otherwise, property values can be up to 256 characters. - `number`: a property that receives numeric values with up to one decimal. - `enumeration`: A property with pre-defined options. When creating this property type, include an `options` array to set the available values. - `datetime`: A property that receives epoch millisecond or ISO8601 values representing a timestamp. Below, learn about HubSpot's default event properties, how to [define new properties for existing events](#define-new-properties), and how to [update existing custom event properties](#update-existing-custom-properties). ### HubSpot's default event properties - `hs_asset_description` - `hs_asset_type` - `hs_browser` - `hs_campaign_id` - `hs_city` - `hs_country` - `hs_device_name` - `hs_device_type` - `hs_element_class` - `hs_element_id` - `hs_element_text` - `hs_language` - `hs_link_href` - `hs_operating_system` - `hs_operating_version` - `hs_page_content_type` - `hs_page_id` - `hs_page_title` - `hs_page_url` - `hs_parent_module_id` - `hs_referrer` - `hs_region` - `hs_screen_height` - `hs_screen_width` - `hs_touchpoint_source` - `hs_tracking_name` - `hs_user_agent` - `hs_utm_campaign` - `hs_utm_content` - `hs_utm_medium` - `hs_utm_source` - `hs_utm_term` ### Define new properties To define a new property on an existing custom event, make a `POST` request to `events/v3/event-definitions/{eventName}/property`. In the request body, include the definition for your property. ```json { "name": "property-name", "label": "Property name", "type": "enumeration", "options": [ { "label": "label", "value": "value" } ] } ``` When naming your property, keep the following in mind: - Once you create a property, the property’s name cannot be changed. - The name can only contain lowercase letters, numbers, underscores, and hyphens. - The first character of the property name must be a letter. - The property name and label can each be up to 50 characters long. - If an property name is not provided, one will be auto-generated using the property label. - Long labels may be cut off in certain parts of the HubSpot UI. ### Update existing custom properties To update an existing property on a custom event, make a `PATCH` request to `events/v3/event-definitions/{eventName}/property`. The only fields that can be updated on a property are the `label`, `description`, and `options` for enumeration properties. To change the type of property, use the `DELETE` endpoint to delete the property and recreate it with the correct type. ### Delete a property To delete an existing property on a custom event, make a `DELETE` request to `events/v3/event-definitions/{eventName}/property/{propertyName}`. When a property is deleted, it will no longer be available for use in future event completions. Past completions will still have the property values. ## Update an event To update an existing custom event schema, make a `PATCH` request to `events/v3/event-definitions/{eventName}`. The only event definition fields that can be updated are `label` and `description`. ```json { "label": "Event label", "description": "A description of the event" } ``` ## Delete an event To delete a custom event, make a `DELETE` request to `events/v3/event-definitions/{eventName}`. Deleting a custom event will remove it from any other HubSpot tools that are referencing it, such as workflows and reports. 1. All of the events for that event definition will be deleted and unrecoverable. 2. Previously deleted `eventName`'s <u>cannot</u> be used again, so exercise caution when deleting an event. ## Get existing event definitions To fetch a single event definition, make a `GET` request to `events/v3/event-definitions/{eventName}`. To search event definitions by specific criteria, make a `GET` request to `events/v3/event-definitions`. You can supply the following query parameters to refine your search: - `searchString`: searches for events that contain the specified characters in the `name` field. Searching is <u>not</u> fuzzy, but is instead a naive _contains_ search. - `after`: a hashed string provided in paged responses for viewing the next page of search results. - `limit`: the maximum number of results to return. - `includeProperties`: a boolean value that specifies whether to include event properties in the returned results. # Email Events API Overview | Email Events API @ The Email Events API is used to get information about events generated by marketing emails or email campaigns sent through a HubSpot account. $ guides & api/analytics-and-events --- # Email Events API Overview The Email Events API is used to get information about events generated by marketing emails or email campaigns sent through a HubSpot account. **Use case for this API:** if a member of your marketing team wants to optimize email campaigns, you can use the Email Events API to gather data and power a machine learning model that determines the best time to send emails to different contact segments. ### Email events Every email sent via HubSpot generates a number of events detailing its lifecycle and how the recipient interacts with its content. An event is uniquely identified by its `EventId`, which is comprised of the following properties: | Property name | Type | Description | | --- | --- | --- | | id | string | A randomly-generated ID for this event. | | created | 64-bit integer | The timestamp (in milliseconds since epoch) when this event was created. | These properties can be used to look up a specific event via [this endpoint](/reference/api/analytics-and-events/email-analytics#get-email-event-by-id). Further, all but one event type (`UNBOUNCE`) have the following properties: | Property name | Type | Description | | --- | --- | --- | | type | string (enumeration) | The type of event. See below for more information on event types. | | recipient | string | The email address of the recipient of the email message. | | portalId | 32-bit integer | An ID referencing the HubSpot account that sent the email message. This will correspond to your account. | | appId | 32-bit integer | An ID referencing the HubSpot application that sent the email message. | | appName | string | The name of the HubSpot application that sent the email message. Note that this is a derived value, and may be modified at any time. | | emailCampaignId | 64-bit integer | An ID referencing the email campaign which the email message is part of. Additional information on the email campaign can be found via [this endpoint](/reference/api/analytics-and-events/email-analytics#get-campaign-data-for-a-given-campaign). | Events can be looked up in bulk via [this endpoint](/reference/api/analytics-and-events/email-analytics#get-email-events) using `'recipient'`, both `'appId'` and `'campaignId'`, or any combination of the above properties. The following additional properties are also available for all event types (including `UNBOUNCE`): | Property name | Type | Description | | --- | --- | --- | | sentBy | JSON | The EventId which uniquely identifies the email message's `SENT` event. If not applicable, this property is omitted. | | obsoletedBy | JSON | The EventId which uniquely identifies the follow-on event which makes this current event obsolete. If not applicable, this property is omitted. | | causedBy | JSON | The EventId which uniquely identifies the event which directly caused this event. If not applicable, this property is omitted. | The event reference properties -- `'sentBy'`, `'obsoletedBy'`, and `'causedBy'` -- are discussed in detail later in this document. ## Event types There are 12 event types that can be generated by HubSpot's Email API during the lifecycle of an email message. They are broadly grouped into categories: Submission, Delivery, User Engagement, and User Status. Event types, event categories, and their relationships are diagrammed below. | Event type | Description | | --- | --- | | SENT | The message was sent to and received by our delivery provider, which has queued it for further handling. | | DROPPED | The message was rejected, either by HubSpot or by our delivery provider, and no attempt will be made to deliver the message. | | PROCESSED | The message has been received by our delivery provider, which has indicated it will attempt to deliver the message to the recipient's email server. | | DELIVERED | The recipient's email server has accepted the message and the message has been successfully delivered to the recipient. | | DEFERRED | The recipient’s email server has temporarily rejected message, and subsequent attempts will be made to deliver the message. | | BOUNCE | The recipient's email server couldn't or wouldn't accept the message, and no further attempts will be made to deliver the message. | | OPEN | The recipient opened the message. | | CLICK | The recipient clicked on a link within the message. | | STATUSCHANGE | The recipient changed their email subscriptions in some way. | | SPAMREPORT | The recipient flagged the message as spam. | | SUPPRESSION | The message was not sent, the recipient has been unengaged with previous marketing emails. | Some event types, such as bot events, may be included in the response to the `/events` API but won't appear when you're analyzing the [_Recipients_ tab for a marketing email in your HubSpot account](https://knowledge.hubspot.com/email/analyze-your-marketing-email-campaign-performance), which filters out certain raw event types and limits the total events shown to 30 events per event type. ### Submission events When an email message is created and sent by HubSpot on behalf of a customer, we first verify whether the recipient is eligible to receive it. If not, we reject the message, triggering the creation of a `DROPPED` event. Otherwise, we submit it to our delivery provider for further handling, triggering a `SENT` event. An email message will almost always have exactly one submission event associated with it; for example, there will never be multiple `SENT` events for a message. We make every effort to reject messages before passing them along to our delivery provider. However, sometimes our delivery provider will decide to reject a message even after we have verified its eligibility. This follow-on rejection results in a `DROPPED` event being created, in addition to the previously-created `SENT` event. Submission events all share the following properties: | Property name | Type | Description | | ------------- | --------------- | ------------------------------------------ | | subject | string | The subject line of the email message. | | from | string | The 'from' field of the email message. | | replyTo | list of strings | The 'reply-to' field of the email message. | | cc | list of strings | The 'cc' field of the email message. | | bcc | list of strings | The 'bcc' field of the email message. | Additionally, `DROPPED` events have the following properties: | Property name | Type | Description | | --- | --- | --- | | dropReason | string (enumeration) | The reason why the email message was dropped. See below for the possible values. | | dropMessage | string | The raw message describing why the email message was dropped. This will usually provide additional details beyond `'dropReason'`. | #### Drop reasons | Drop reason | Description | | --- | --- | | PREVIOUSLY_BOUNCED | A previous message to the recipient resulted in a bounce. | | PREVIOUS_SPAM | A previous message to the recipient was flagged as spam. | | PREVIOUSLY_UNSUBSCRIBED_MESSAGE | The recipient previously unsubscribed from this subscription. | | PREVIOUSLY_UNSUBSCRIBED_PORTAL | The recipient previously unsubscribed from all subscriptions from the account. | | INVALID_TO_ADDRESS | The email address in the 'to' field failed validation. | | INVALID_FROM_ADDRESS | The email address in the 'from' field failed validation. | | BLOCKED_DOMAIN | The recipient's domain was blocked. | | BLOCKED_ADDRESS | The recipient explicitly requested to not receive any emails via HubSpot. | | EMAIL_UNCONFIRMED | Double opt-in was enabled and the recipient had not yet confirmed their subscription. | | CAMPAIGN_CANCELLED | The associated email campaign was canceled. | | MTA_IGNORE | Our delivery provider decided to drop the message. Any additional details will be included in `'dropMessage'`. | | PORTAL_OVER_LIMIT | Your account went over its monthly limit for email sends. | | PORTAL_SUSPENDED | Your account was suspended for non-compliance or deliverability issues. | | QUARANTINED_ADDRESS | The recipient has been quarantined due to repeated hard bounce rates. | | ADDRESS_LIST_BOMBED |  The recipient has been quarantined due to suspicious form activity. [Learn more](https://knowledge.hubspot.com/marketing-email/remediate-list-bombing). | ### Delivery events Once our delivery provider has accepted an email message, we create a `PROCESSED` event. At this point, the delivery provider has queued the message for delivery. If everything goes smoothly, the delivery provider will dequeue the message and deliver it to the recipient's email server, generating a `DELIVERED` event. Occasionally, things don't go smoothly, and one of two things happens: delivery is deferred because of a temporary rejection, or delivery fails and won't be retried. In the first case, the message could not be delivered to the recipient's email server for some non-fatal (usually transient reason, such as a spurious time-out. The delivery provider will re-queue the message for later delivery, and we create a `DEFERRED` event. A message can be deferred multiple times before it completes the delivery phase, with a new event created on each attempt. If delivery fails, no further attempts will be made to deliver the message, and we create a `BOUNCE` event. This can occur for a variety of reasons, such as the recipient being unknown by the email server. The specific delivery event types have the following properties: #### Delivered | Property name | Type | Description | | --- | --- | --- | | response | string | The full response from the recipient's email server. | | smtpId | string | An ID attached to the message by HubSpot. | #### Deferred | Property name | Type | Description | | --- | --- | --- | | response | string | The full response from the recipient's email server. | | attempt | 32-bit integer | The delivery attempt number. | #### Bounce | Property name | Type | Description | | --- | --- | --- | | category | string (enumeration) | The best-guess of the type of bounce encountered. If an appropriate category couldn't be determined, this property is omitted. See below for the possible values. Note that this is a derived value, and may be modified at any time to improve the accuracy of classification. | | response | string | The full response from the recipient's email server. | | status | string | The status code returned from the recipient's email server. | #### Bounce categories | Bounce category | Description | | --- | --- | | UNKNOWN_USER | The recipient didn't exist. | | MAILBOX_FULL | The recipient's mailbox was full and couldn't receive any messages. | | CONTENT | The recipient's filters identified content in the body of the email as suspicious or spammy. | | SPAM | The message was flagged as spam. | | POLICY | The message was flagged as spam. | | GREYLISTING | The email server requires a longer history of email activity from the sender. | | MAILBOX_MISCONFIGURATION | A mailbox misconfiguration was detected. | | ISP_MISCONFIGURATION | An ISP misconfiguration was detected. | | DOMAIN_REPUTATION | The sending domain has a poor reputation or a reputation that doesn't meet the standards of the recipient server. | | DMARC | The sender’s domain does not pass a DMARC check. Please review your SPF and DMARC policies. | | SENDING_DOMAIN_MISCONFIGURATION | Domain authentication failed due to a policy on the recipient's end. | | TIMEOUT | The receiving email server timed out and is no longer accepting email. | | THROTTLED | The recipient's email server was throttling messages. | | UNCATEGORIZED | An uncategorized error was detected. | | IP_REPUTATION | The message originated from a suspicious (or previously unknown) IP address. | | DNS_FAILURE | The recipient’s domain name server settings were misconfigured at the time the email was sent. | | TEMPORARY_PROBLEM | Some temporary problem occurred. | ### User engagement events Once an email message reaches its recipient, there are four different event types that can occur: `OPEN`, `CLICK`, `PRINT`, and `FORWARD`. These represent the recipient's interaction with the message and its content, and each can occur multiple times. For example, each time any URL is clicked, a new `CLICK` event is created, even if that URL has previously been clicked and generated such an event. User engagement events all share the following properties: | Property name | Type | Description | | --- | --- | --- | | userAgent | string | The user agent responsible for the event, e.g. “Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.95 Safari/537.36” | | browser | JSON | A JSON object representing the browser which serviced the event. Its comprised of the properties: `'name'`, `'family'`, `'producer'`, `'producer_url'`, `'type'`, `'url'`, `'version'`. | | location | JSON | A JSON object representing the location where the event occurred. It's comprised of the properties: `'city'`, `'state'`, `'country'`. | | filteredEvent | boolean | A boolean representing whether the event has been filtered out of reporting based on customer reports settings or not. | Additionally, `CLICK` events have the following properties: | Property name | Type | Description | | --- | --- | --- | | url | string | The URL within the message that the recipient clicked. | | referer | string | The URL of the webpage that linked to the URL clicked. Whether this is provided, and what its value is, is determined by the recipient's email client. | And `OPEN` events may have the following property: | Property name | Type | Description | | --- | --- | --- | | duration | 64-bit integer (milliseconds) | If provided and nonzero, the approximate number of milliseconds the user had opened the email. | ### User status events A recipient can also update their communication preferences via the email message. By clicking on the subscription preferences link in the message, they can change their subscriptions, either subscribing or unsubscribing from various lists, triggering a `STATUSCHANGE` event. Note that a status change can be for any list(s), not just the one which is associated with the current email message. An email message may also be flagged as spam by the recipient, resulting in a `SPAMREPORT` event. Note that this is independent of subscription status — flagging a message as spam does not simply unsubscribe the recipient from the list in question. Rather, the subscription status is left unchanged, and a flag is set indicating that recipient should never receive another email message from HubSpot. Once this happens, you'll need manual intervention by HubSpot to remove the flag. A `STATUSCHANGE` event has the following additional properties: | Property name | Type | Description | | --- | --- | --- | | source | string (enumeration) | The source of the subscription change. See below for the possible values. | | requestedBy | string | The email address of the person requesting the change on behalf of the recipient. If not applicable, this property is omitted. | | portalSubscriptionStatus | string (enumeration, `'SUBSCRIBED'` or `'UNSUBSCRIBED'`) | The recipient's portal subscription status. (Note that if this is `'UNSUBSCRIBED'`, the property `'subscriptions'` is _not_ necessarily an empty array, nor are all subscriptions contained in it necessarily going to have their statuses set to `'UNSUBSCRIBED'`.) | | subscriptions | JSON | An array of JSON objects representing the status of subscriptions for the recipient. Each JSON subscription object is comprised of the properties: `'id'`, `'status'`. | | bounced | boolean (`true`, or omitted entirely) | A HubSpot employee explicitly initiated the status change to block messages to the recipient. (Note this usage has been deprecated in favor of dropping messages with a `'dropReason'` of `BLOCKED\_ADDRESS`.) | #### Subscription change sources | Source | Description | | --- | --- | | SOURCE_LIST_UNSUBSCRIBE | The recipient used a list-unsubscribe header. | | SOURCE_RECIPIENT | The recipient used the subscription UI. | | SOURCE_IMPORT | The customer imported the subscriptions into their portal. | | SOURCE_HUBSPOT_CUSTOMER | The customer used the subscription UI. | | SOURCE_SPAM_REPORT | A spam report generated by an automated system was received. | | SOURCE_NON_DELIVERY_REPORT | A non-delivery report (typically a bounce) was received. | | SOURCE_DIRECT_COMPLAINT | A direct complaint via `abuse@hubspot.com` was received. | | SOURCE_MTA_RECORD | Our delivery provider provided the change, during our normal synchronization with their system-of-record. | | SOURCE_HUBSPOT | A HubSpot employee made the change. | | SOURCE_MIGRATION | HubSpot migrated the subscriptions from a previous version of the product. | | SOURCE_HUBSPOT_CLEANUP | HubSpot cleaned up the subscriptions. | | SOURCE_KNOWN_SPAM_TRAP | This recipient address is a known spam trap, and should not receive emails. | ### 'Unbounce' events There is a 13th event type, which is unrelated to a specific email message. `UNBOUNCE` events occur when a particular email address is either automatically or manually _unbounced_ by HubSpot. This resets the bounce status of the recipient, potentially allowing them to receive emails from your portal. | Property name | Type | Description | | --- | --- | --- | | user | string | The email address of the user who submitted the unbounce request. | ## Event references Many events are related to other events that occurred either before or after it. As described in the first section above, we use EventIds to build this reference chain. Note that event references are relatively new, and older events may not have them populated. ### 'sentBy' As discussed previously, each email message has either a `SENT` or `DROPPED` event (or one of each) associated with it. This will be the first event generated for any given message. If a message generates a `SENT` event, all subsequently generated events will reference that event via the property `'sentBy'`. This backward-reference can be useful to get more information on the parent `SENT` event, or to manually find all events associated with a given message. ### 'obsoletedBy' Sometimes, a follow-on event occurs for a given message, signifying that an earlier event should be ignored. This relationship is captured in a forward-reference in the property `'obsoletedBy'`. For instance, in the case where we generate both a `SENT` event and a subsequent `DROPPED` event, the `SENT` event is ultimately irrelevant, and is _obsoleted by_ the `DROPPED` event. Accordingly, the `SENT` event will reference the `DROPPED` event via `'obsoletedBy'`. ### 'causedBy' Certain events occur precisely because of some previous event, often for a different message. This relationship is captured in a backward-reference in the property `'causedBy'`. It can be used to get additional details on why a particular event caused the following event. For example, a `DROPPED` event will occur when there was a previous `BOUNCE` event for the same recipient. In this case, the `DROPPED` event will have its `'dropReason'` set to `PREVIOUSLY\_BOUNCED`, and it's `'causedBy'` will reference that previous `BOUNCE` event. # Events API | Event Analytics @ Event Analytics endpoint allows you to find and filter events associated with a CRM object of any type. $ guides & api/analytics-and-events --- Use the event analytics API to fetch events associated with CRM records of any type (**_Marketing Hub_** _Enterprise_, **_Sales Hub_** _Enterprise_, **_Service Hub_** _Enterprise_, or **_CMS Hub_** _Enterprise_ only). This includes standard events, such as website page views and email opens, as well as [custom events](/guides/api/analytics-and-events/custom-events/custom-event-completions). For example, use this API to view a timeline of the interactions that a contact has had with your content. You can then use this timeline to build a dataset for custom analytics or present a contact timeline in an external application. ## Query individual event completions This API returns events for one CRM record at a time. You can select the record by specifying the `objectType` and including either the `objectId` or `objectProperty` query parameter. ### Select by object ID To specify a record by its ID, add the `objectId` query parameter. For example, to specify a contact record with the ID of _2832_, you would make the following `GET` request: `/events/v3/events/?objectType=contact&objectId=224834` ### Select by object property To specify a record by a unique property instead of contact ID, add the `objectProperty` parameter. Reference the property by including the property name and the value in the following format: `objectProperty.{propname}={propvalue}` For example, to specify a contact by their email address, you would make the following `GET` request: `/events/v3/events/?objectType=contact&objectProperty.email=user12@dev.com` ## Querying and filtering for event types When querying for the events associated with a given CRM object, the response will include all event types, including custom behavioral events. To only return event completions for a specific event type, you can include an `eventType` parameter, followed by the event name. To get a list of all available event types, you can make a `GET` request to `/events/v3/events/event-types`. The response will return all event types in your account. You can then use one of these event names as a query parameter in a `GET` request to the `/events/v3/events` endpoint. For example: `/events/v3/events/eventType={EVENT_NAME}&objectType=contact&objectId=224834` # Analytics API Overview @ The Analytics API allows you to pull detailed marketing performance data from your HubSpot account. $ guides & api/analytics-and-events --- # Analytics API Overview The Analytics API allows you to export analytics and reporting data from HubSpot. It’s primarily used to connect metrics tracked in HubSpot to those stored in other business intelligence tools. **Use case for this API:** You're using HubSpot’s blogging and landing page tools, but not the CRM. You want to see the traffic and leads tracked in HubSpot next to the sales activity and deal progression from your CRM to get a fuller sense of your business performance. You use the Analytics API to integrate HubSpot's reporting data with your CRM. ## Using the analytics data The Analytics API is designed around the functionality of HubSpot's various reporting tools. The data returned by these APIs will mimic the data you would see in those tools. For example, when [pulling sources data](/reference/api/analytics-and-events/reporting#get-analytics-data-breakdowns), the results would be similar to what you would see in the [sources report](https://knowledge.hubspot.com/sources-user-guide-v2/how-to-analyze-sources-data). For more details about how the sources report works, please see the article [linked here](https://knowledge.hubspot.com/sources-user-guide-v2/how-to-analyze-sources-data). For more details about the page performance report, please see the article [linked here](https://knowledge.hubspot.com/page-performance-user-guide-v2/a-quick-tour-of-page-performance). Many endpoints allow you to drill down into the results using the `d1` and `d2` parameters. The available drill down options are going to depend on the data being requested. For example, when [pulling sources data](/reference/api/analytics-and-events/reporting#get-analytics-data-breakdowns), the first level drill down options (using the `d1` parameter) would be the different source types you'd see in the in the [sources report](https://knowledge.hubspot.com/sources-user-guide-v2/how-to-analyze-sources-data) (such as organic, direct, or offline). When drilling down into organic search (d1=organic), the second level drilldown (the d2 parameter) would be a specific keyword, allowing you to get the details for which search engine led to traffic for that specific keyword. When looking for which values you can use for drilling down, it's a good idea to start with the total summary (using `total` for the `:time_period`) and looking at the breakdowns in that summary data. ## Access to analytics data Access to data from the Analytics API will depend on the connected HubSpot account's permissions. For example, to pull analytics data for landing pages, the account would need access to create landing pages. To get analytics data broken down by category, the account would need access to the data in the sources report. **Note**: All accounts will be able to access certain features of the Analytics API using [OAuth](https://developers.hubspot.com/docs/methods/oauth2/oauth2-overview) based on the table below. If the account does not have access to the requested data, you will receive a 403 error response. For the full breakdown of the required plan for each feature, see the table below. | Analytics API feature | [Required plan](https://www.hubspot.com/pricing/marketing) | | --- | --- | | [Get analytics data breakdowns](/reference/api/analytics-and-events/reporting#get-analytics-data-breakdowns) | | | <ul><li>totals</li></ul> | Marketing Hub Professional, or Enterprise, or HubSpot CMS | | <ul><li>sessions</li></ul> | Marketing Hub Professional, or Enterprise, or HubSpot CMS | | <ul><li>sources</li></ul> | Marketing Hub Starter, Professional, or Enterprise, or HubSpot CMS <br /><br /> **Note**: Starter plans will not be able to use the **&d1=** or **&d2=** drilldown parameters. | | <ul><li>geolocation</li></ul> | Marketing Hub Starter, Professional, or Enterprise, or HubSpot CMS <br /><br /> **Note**: Starter plans will not be able to use the **&d1=** or **&d2=** drilldown parameters. | | <ul><li>utm-:utm_type</li></ul> | Marketing Hub Professional, or Enterprise, or HubSpot CMS | | [Get analytics data for specific objects](/reference/api/analytics-and-events/reporting#get-analytics-data-for-specific-objects) | | | <ul><li>event-completions</li></ul> | Marketing Hub Enterprise | | <ul><li>forms</li></ul> | HubSpot Free CRM | | <ul><li>pages</li></ul> | Marketing Hub Professional or Enterprise | | <ul><li>social-assists</li></ul> | Marketing Hub Professional or Enterprise | | [Get data for HubSpot hosted content](/reference/api/analytics-and-events/reporting#get-data-for-hubspot-hosted-content) | | | <ul><li>landing-pages</li></ul> | Marketing Hub Professional or Enterprise, or HubSpot CMS | | <ul><li>standard-pages</li></ul> | Marketing Hub Professional or Enterprise, or HubSpot CMS | | <ul><li>blog-posts</li></ul> | Marketing Hub Professional or Enterprise, or HubSpot CMS | | <ul><li>listing-pages</li></ul> | Marketing Hub Professional or Enterprise, or HubSpot CMS | | <ul><li>knowledge-articles</li></ul> | Marketing Hub Professional or Enterprise, or HubSpot CMS | | [Get all analytics views](/reference/api/analytics-and-events/reporting#get-all-analytics-views) | Marketing Hub Professional or Enterprise | ## Error responses and data limits The Analytics API limits how much data can be returned in a request. If you exceed those limits, you'll receive one of the following errors: - 403 response code - The account does not have access to feature required to access this data. See the table above for a full breakdown of the plans required for each feature. - 413 response code with RESULT_POINT_LIMIT_EXCEEDED - Returned when there are too many time data points. When using a `:time_period` of daily, weekly, or monthly, the results are limited to 500 time points. Try narrowing the time range between the start and end dates, or using a wider time period (monthly instead of weekly for example). - 501 response code with RESULT_LIMIT_EXCEEDED - Returned when there are too many breakdowns in the response. When using a `total` or `summarize` option for the `:time_period`, there is a limit of 2 million breakdowns for the response. Try using filters, or removing filters if you're including a large number of filters, or narrowing the time range between the start and end dates. - 501 response code with SCAN_TIME_EXCEEDED - Returned when the processing time out is hit. Try narrowing the amount of data you're requesting by using filters or narrowing the range between the start and end dates. ## Response details Below you can see examples of responses you'd receive from the Analytics API. The actual stats that you may get are heavily dependent on the data requested. (See the Knowledge Base articles linked above for data descriptions.) Example data when pulling total stats or stats broken down by specific criteria: ```javascript { "offset": 6, // Integer; Use this value with the 'offset' parameter in the next request to get the next set of results. "total": 6, // Integer; The total number of results. // Offsets are sequential and exclusive (i.e. offset=6 would get you the 7th result), allowing you to use the total results to build a paging footer in your app. "totals": { // A set of rolled-up stats from all breakdowns. // The specific stats will depend on the breakdowns or objects you are getting data for. "rawViews": 10709, "visits": 4430, "visitors": 1779, "leads": 956, "contacts": 7328, "subscribers": 3779, "marketingQualifiedLeads": 6, "salesQualifiedLeads": 6, "opportunities": 1860, "customers": 53, "pageviewsPerSession": 2.4173814898419863, "bounceRate": 0.6419864559819413, "timePerSession": 245.64672686230247, "newVisitorSessionRate": 0.40158013544018056, "sessionToContactRate": 1.654176072234763, "contactToCustomerRate": 0.0072325327510917034 }, "breakdowns": [ // A list of stats broken down by the specified criteria or object. // The specific stats in each item will depend on the breakdowns or objects you are getting data for. { "breakdown": "direct", // String; The ID for a specific breakdown. // These IDs can be used with the 'd1' or 'd2' parameters to further break down the results within a specified breakdown. // When pulling data for objects, each breakdown will be the ID of a specific object. "rawViews": 8142, "visits": 3179, "visitors": 1008, "leads": 498, "contacts": 543, "subscribers": 4, "opportunities": 39, "customers": 2, "pageviewsPerSession": 2.5611827618748033, "bounceRate": 0.6247247562126454, "timePerSession": 296.14878892733566, "newVisitorSessionRate": 0.31708084303240014, "sessionToContactRate": 0.17080843032400125, "contactToCustomerRate": 0.003683241252302026 }, { "breakdown": "paid", "rawViews": 1000, "visits": 648, "visitors": 604, "pageviewsPerSession": 1.5432098765432098, "bounceRate": 0.7947530864197531, "timePerSession": 45.53086419753087, "newVisitorSessionRate": 0.9320987654320988 }, { "breakdown": "referrals", "rawViews": 1526, "visits": 567, "visitors": 137, "contacts": 1, "subscribers": 1, "pageviewsPerSession": 2.691358024691358, "bounceRate": 0.5502645502645502, "timePerSession": 205.9241622574956, "newVisitorSessionRate": 0.24162257495590828, "sessionToContactRate": 0.001763668430335097 }, { "breakdown": "organic", "rawViews": 26, "visits": 25, "visitors": 21, "pageviewsPerSession": 1.04, "bounceRate": 0.96, "timePerSession": 18.8, "newVisitorSessionRate": 0.84 }, { "breakdown": "other", "rawViews": 15, "visits": 11, "visitors": 9, "pageviewsPerSession": 1.3636363636363635, "bounceRate": 0.6363636363636364, "timePerSession": 2.272727272727273, "newVisitorSessionRate": 0.8181818181818182 }, { "breakdown": "offline", "leads": 458, "contacts": 6784, "subscribers": 3774, "marketingQualifiedLeads": 6, "salesQualifiedLeads": 6, "opportunities": 1821, "customers": 51, "contactToCustomerRate": 0.007517688679245283 } ] } ``` Example data when pulling data for a specific time period (in this case, daily): ```javascript // The returned data will be a set of results for each time period (in this case, day). // Each entry is keyed by the start of the time period in YYYY-MM-DD format. { "2018-01-01": [ // The start date the specific time period. // The specific data in each set will depend on the data being requested. { "rawViews": 5, "visits": 7, "visitors": 5, "submissions": 2, "leads": 2, "contacts": 12, "subscribers": 10, "pageviewsPerSession": 0.7142857142857143, "newVisitorSessionRate": 0.7142857142857143, "sessionToContactRate": 1.7142857142857142 } ], "2018-01-02": [ { "rawViews": 105, "visits": 55, "visitors": 24, "submissions": 16, "leads": 7, "contacts": 87, "subscribers": 76, "opportunities": 4, "pageviewsPerSession": 1.9090909090909092, "newVisitorSessionRate": 0.43636363636363634, "sessionToContactRate": 1.5818181818181818 } ], "2018-01-03": [ { "rawViews": 163, "visits": 105, "visitors": 19, "submissions": 96, "leads": 7, "contacts": 120, "subscribers": 112, "opportunities": 1, "pageviewsPerSession": 1.5523809523809524, "newVisitorSessionRate": 0.18095238095238095, "sessionToContactRate": 1.1428571428571428 } ] } ``` # OAuth | Managing tokens @ Get OAuth access and refresh tokens ensuring your ability to securely perform CRUD actions with HubSpot APIs. $ guides & api/app-management --- # Manage OAuth tokens Use the OAuth tokens API to generate and manage tokens needed for authorizing your [public app](/guides/apps/public-apps/overview) and the requests it makes. For example, you'll need to use this API to fetch the initial access and refresh tokens during the app installation process. You'll then use it to continue generating new tokens when the old ones expire. Learn more about [working with OAuth](/guides/apps/authentication/working-with-oauth). Before you can use these endpoints, you'll have to [create a public app](/guides/apps/public-apps/overview). A user will then need to install it into their account to [initiate OAuth access](/guides/apps/authentication/working-with-oauth#initiating-an-integration-with-oauth-2-0). ## Initiating OAuth access After creating your app, a user can install it into their HubSpot account using they'll use the install URL located in the app's settings, which will include the `client_id`, `redirect_uri`, and `scopes` as query parameters. You may also include `optional_scopes` and `state`, if needed. Learn more about [initiating OAuth for your app](/guides/apps/authentication/working-with-oauth#initiating-an-integration-with-oauth-2-0). After a user authorizes your app and installs it into their account, the redirect URL will be appended with a `code` value, which you can use to [generate an access token and a refresh token](#generate-initial-access-and-refresh-tokens). The access token will be used to authenticate requests that your app makes, while the refresh token will be used to get a new access token when the current one expires. ## Generate initial access and refresh tokens To get OAuth access and refresh tokens, make a URL-form encoded `POST` request to `/oauth/v1/token`. In the request body, you'll specify various auth parameters, such as `client_id` and `client_secret`, along with the `code` passed back through the redirect URL. After a user authorizes your app, the redirect URL will be appended with a `code` value. Using this code, you'll generate the initial access token and refresh token. Access tokens are short-lived, and you can check the `expires_in` parameter when generating an access token to determine its lifetime (in seconds). For example, your request may look similar to the following: ```shell curl --request POST \ --url https://api.hubapi.com/oauth/v1/token \ --header 'content-type: application/x-www-form-urlencoded' \ --data 'grant_type=authorization_code&code=bcf33c57-dd7a-c7eb-4179-9241-e01bd&redirect_uri=https://www.domain.com/redirect&client_id=7933b042-0952-4e7d-a327dab-3dc&client_secret=7a572d8a-69bf-44c6-9a34-416aad3ad5' ``` | Parameter | Type | Description | | --- | --- | --- | | `grant_type` | String | Must be `authorization_code` for the request to generate initial access and refresh tokens. | | `code` | String | The `code` returned in the redirect URL after the user installs the app. | | `redirect_uri` | String | The app's set redirect URL. | | `client_id` | String | The app's client ID. | | `client_secret` | String | The app's client secret. | In the response, you'll receive the access token along with the refresh token, which you can use to refresh the access token. The `expires_in` field specifies how long the access token will last (in seconds). ```json // Example response { "token_type": "bearer", "refresh_token": "1e8fbfb1-8e96-4826-8b8d-c8af73715", "access_token": "CIrToaiiMhIHAAEAQAAAARiO1ooBIOP0sgEokuLtAEaOaTFnToZ3VjUbtl46MAAAAEAAAAAgAAAAAAAAAAAACAAAAAAAOABAAAAAAAAAAAAAAAQAkIUVrptEzQ4hQHP89Eoahkq-p7dVIAWgBgAA", "expires_in": 1800 } ``` ## Refresh an access token Using a refresh token, you can generate a new access token by making a URL-form encoded `POST` request to `/oauth/v1/token`. In the request body, you'll specify the `grant_type`, `client_id`, `client_secret`, and `refresh_token`. ```shell curl --request POST \ --url https://api.hubapi.com/oauth/v1/token \ --header 'content-type: application/x-www-form-urlencoded' \ --data 'grant_type=refresh_token&refresh_token=1e8fbfb1-8e96-4826-8b8d-c8af73715&client_id=7933b042-0952-4e7d-a327dab-3dc&client_secret=7a572d8a-69bf-44c6-9a34-416aad3ad5' ``` | Parameter | Type | Description | | --- | --- | --- | | `grant_type` | String | Must be `refresh_token` for the request to generate new access tokens from the refresh token. | | `refresh_token` | String | The refresh token value. | | `client_id` | String | The app's client ID. | | `client_secret` | String | The app's client secret. | ## Retrieve access token metadata To get information about an OAuth access token, including the user that the token was created for and their corresponding Hub ID, make a `GET` request to `/oauth/v1/access-tokens/{token}`. You'll receive a response containing information about the user their HubSpot account. ```json // Example response { "token": "CNaKSIHAAEAQAAAARiO1ooBIOP0sgEokuLtATIU5m7Kzmjj0ihJJuKFq1TcIiHCqwE6MAAAAEEAAAAAAAAAAgAIUfmerBenQwc07ZHXy6atYNNW8XCVKA25hMVIAWgBgAA", "user": "user@domain.com", "hub_domain": "meowmix.com", "scopes": [ "oauth", "crm.objects.contacts.read", "crm.objects.contacts.write" ], "signed_access_token": { "expiresAt": 1727190403926, "scopes": "AAEAAAAQ==", "hubId": 1234567, "userId": 293199, "appId": 111111, "signature": "5m7ihJJuKFq1TcIiHCqwE=", "scopeToScopeGroupPks": "AAAAQAAAAAAAAAACAAAAAAAAAAAAAIAAAAAAA4AEAAAAAAAAAAAAAABAC", "newSignature": "fme07ZHXy6atYNNW8XCU=", "hublet": "na1", "trialScopes": "", "trialScopeToScopeGroupPks": "", "isUserLevel": false }, "hub_id": 1234567, "app_id": 111111, "expires_in": 1754, "user_id": 293199, "token_type": "access" } ``` HubSpot access tokens are expected to fluctuate in size over time, as updates will be made to the information that is encoded in them. It's recommended to allow for tokens to be up to 512 characters to account for any changes. ## Delete a refresh token If a user uninstalls your app, you can delete the refresh token by making a `DELETE` request to `/oauth/v1/refresh-tokens/{token}`. This will only delete the refresh token. Access tokens generated with the refresh token will not be deleted. Additionally, this will not uninstall the application from HubSpot accounts or inhibit data syncing between the app and account. # Webhooks API @ Overview for the Webhooks API, which allows you to subscribe to events happening in a HubSpot account with your integration installed. $ guides & api/app-management --- # Webhooks The Webhooks API allows you to subscribe to events happening in a HubSpot account with your integration installed. Rather than making an API call when an event happens in a connected account, HubSpot can send an HTTP request to an endpoint you configure. You can configure subscribed events in your app’s settings or using the endpoints detailed below. Webhooks can be more scalable than regularly polling for changes, especially for apps with a large install base. Using the Webhooks API requires the following: - You must set up a HubSpot app to use webhooks by subscribing to the events you want to be notified about, and by specifying a URL to send those notifications. See the [prerequisites documentation](https://developers.hubspot.com/docs) for more details about creating an app. - You must deploy a publicly available and secure (HTTPS) endpoint for that URL that can handle the webhook payloads specified in this documentation. Webhooks are set up for a [HubSpot app](/guides/apps/public-apps/overview), not individual accounts. Any account that install your app by going through the [OAuth flow](/reference/api/app-management/oauth-tokens#initiate-an-integration-with-oauth-2.0) will be subscribed to its webhook subscriptions. You can subscribe to CRM object events, which includes contacts, companies, deals, tickets, products and line items, as well as conversations events. - You can also [manage webhooks in a private app](/guides/apps/private-apps/create-and-edit-webhook-subscriptions-in-private-apps). In private apps, webhook settings can only be edited in the in-app settings for your private app, and cannot currently be edited through the API. - To subscribe to conversations webhooks, you need access to the [conversations inbox and messages APIs](/guides/api/conversations/inbox-and-messages), which is currently <u>in beta</u>. ## Scopes In order to use webhooks to subscribe to CRM events, your app will need to be configured to authorize the [_Required scopes_](/guides/apps/public-apps/overview#scope-types) that corresponds to the CRM object type you want to subscribe to. For example, if you want to subscribe to contact events, you would need to request the `crm.objects.contacts.read` scope. - If you're creating subscriptions in the settings UI of your public app, you'll be prompted to add the required scope in the _Create new webhook subscriptions_ panel before you finish creating your subscription. - If you're creating a subscription by making a `POST` request to the `/webhooks/v3/{appId}/subscriptions` endpoint, the response will include an error that will provide the name of the scope you'll need to configure in the settings UI of your public app. - If your app is already using webhooks, you won't be able to remove any scopes required by active webhook subscriptions without first pausing and removing the subscriptions. - You can review the scopes required for each webhook subscription type in the [table below.](#webhook-subscriptions) Review the OAuth documentation for [more details about scopes](/reference/api/app-management/oauth-tokens#initiate-an-integration-with-oauth-2.0#scopes) and [setting up the authorization URL](/reference/api/app-management/oauth-tokens#initiate-an-integration-with-oauth-2.0) for your app. ## Webhook settings Before setting up your webhook subscriptions, you need to specify a URL to send those notifications to. Follow the instructions in the sections below to learn how to fully configure subscriptions for your app. - Webhook settings can be cached for up to five minutes. When making changes to the webhook URL, concurrency limits, or subscription settings, it may take up to five minutes to see your changes go into effect. - HubSpot sets a concurrency limit of 10 requests when sending subscription event data associated with an account that installed your app. This concurrency limit is the maximum number of in-flight requests that HubSpot will attempt at a time. Each request can contain up to 100 events. ### Manage settings in your developer account You can manage your URL and event throttling limit in your app’s configuration page in your developer account: - In your developer account, navigate to your **App** dashboard. - Click the **name** of the app you want to set up webhooks for. - In the left sidebar menu, navigate to **Webhooks**. - In the _Target URL_ field, enter the **URL** that HubSpot will make a POST request to when events trigger. - Use the _Event throttling_ setting to adjust the maximum number of events HubSpot will attempt to send. - Click **Save**. ### Manage settings via API You can use the following endpoints and your [developer API key](https://developers.hubspot.com/docs) to programmatically configure webhook settings for an app. To view any webhook settings currently configured for an app, make a `GET` request to `webhooks/v3/{appId}/settings`. You'll need to include the [app ID](https://developers.hubspot.com/docs) in the request, which you can find below the name of the app in your _Apps_ dashboard, or on the _Auth_ tab in your app's settings. The settings object contains the following fields: | Field | Description | | --- | --- | | `webhookUrl` | The URL that HubSpot will send webhook notifications to. This URL must be served over HTTPS. | | `maxConcurrentRequests` | The concurrency limit for the webhook URL. This value must be a number greater than five. | To make edits to these settings, make a `PUT` request to `webhooks/v3/{appId}/settings` and include the following fields in the request body: | Field | Description | | --- | --- | | `targetUrl` | The publicly available URL for HubSpot to call where event payloads will be delivered. | | `throttling` | Configure webhook throttling details in this object. The throttling object includes the `period` and `maxConcurrentRequests` fields. | | `period` | Time scale for this setting. Can be either `SECONDLY` (per second) or `ROLLING_MINUTE` (per minute). | | `maxConcurrentRequests` | The maximum number of HTTP requests HubSpot will attempt to make to your app in a given time frame determined by `period`. | For example, your request might look similar to the following: ```json // PUT request to https://api.hubapi.com/webhooks/v3/{appId}/settings { "throttling": { "period": "SECONDLY", "maxConcurrentRequests": 10 }, "targetUrl": "https://www.example.com/hubspot/target" } ``` ## Webhook subscriptions Once you’ve set up your webhook URL and event throttling limit, you’ll need to create one or more subscriptions. Webhook subscriptions tell HubSpot which events your particular app would like to receive. Subscriptions apply to all customers who have installed your integration. This means that you only need to specify what subscriptions you need once. Once you've turned on a subscription for an application, it will automatically start getting webhooks for all customers that have installed your application, and your integration will start receiving webhook triggers from any new customers. For all `associationChange` webhook subscriptions, the webhook will fire two events for both sides of the association. - When associating two contacts, a subscription to `contact.associationChange` will fire two events, representing `contact 1 to contact 2` and `contact 2 to contact 1`. - When associating a company, if you have two webhook subscriptions `contact.associationChange` and `company.associationChange`, you will receive two events. These will represent `contact 1 to company 1` and `company 1 to contact 1`. The following subscription types are supported and can be used as the value for the `eventType` field when creating subscriptions via API: | Subscription type | Scope required | Description | | --- | --- | --- | | `contact.creation` | `crm.objects.contacts.read` | Get notified if any contact is created in a customer's account. | | `contact.deletion` | `crm.objects.contacts.read` | Get notified if any contact is deleted in a customer's account. | | `contact.merge` | `crm.objects.contacts.read` | Get notified if a contact is merged with another. | | `contact.associationChange` | `crm.objects.contacts.read` | Get notified if a contact has an association added or removed between itself and another supported webhook object (contact, company, deal, ticket, line item, or product). | | `contact.restore` | `crm.objects.contacts.read` | Get notified if a contact is restored from deletion. | | `contact.privacyDeletion` | `crm.objects.contacts.read` | Get notified if a contact is deleted for [privacy compliance reasons](/guides/api/app-management/webhooks). | | `contact.propertyChange` | `crm.objects.contacts.read` | Get notified if a specified property is changed for any contact in an account. | | `company.creation` | `crm.objects.companies.read` | Get notified if any company is created in a customer's account. | | `company.deletion` | `crm.objects.companies.read` | Get notified if any company is deleted in a customer's account. | | `company.propertyChange` | `crm.objects.companies.read` | Get notified if a specified property is changed for any company in a customer's account. | | `company.associationChange` | `crm.objects.companies.read` | Get notified if a company has an association added or removed between itself and another supported webhook object (contact, company, deal, ticket, line item, or product). | | `company.restore` | `crm.objects.companies.read` | Get notified if a company is restored from deletion. | | `company.merge` | `crm.objects.companies.read` | Get notified if a company is merged with another. | | `deal.creation` | `crm.objects.deals.read` | Get notified if any deal is created in a customer's account. | | `deal.deletion` | `crm.objects.deals.read` | Get notified if any deal is deleted in a customer's account. | | `deal.associationChange` | `crm.objects.deals.read` | Get notified if a deal has an association added or removed between itself and another supported webhook object (contact, company, deal, ticket, line item, or product). | | `deal.restore` | `crm.objects.deals.read` | Get notified if a deal is restored from deletion. | | `deal.merge` | `crm.objects.deals.read` | Get notified if a deal is merged with another. | | `deal.propertyChange` | `crm.objects.deals.read` | Get notified if a specified property is changed for any deal in a customer's account. | | `ticket.creation` | `tickets` | Get notified if a ticket is created in a customer's account. | | `ticket.deletion` | `tickets` | Get notified if any ticket is deleted in a customer's account. | | `ticket.propertyChange` | `tickets` | Get notified if a specified property is changed for any ticket in a customer's account. | | `ticket.associationChange` | `tickets` | Get notified if a ticket has an association added or removed between itself and another supported webhook object (contact, company, deal, ticket, line item, or product). | | `ticket.restore` | `tickets` | Get notified if a ticket is restored from deletion. | | `ticket.merge` | `tickets` | Get notified if a ticket is merged with another. | | `product.creation` | `e-commerce` | Get notified if any product is created in a customer's account. | | `product.deletion` | `e-commerce` | Get notified if any product is deleted in a customer's account. | | `product.restore` | `e-commerce` | Get notified if a product is restored from deletion. | | `product.merge` | `e-commerce` | Get notified if a product is merged with another. | | `product.propertyChange` | `e-commerce` | Get notified if a specified product is changed for any product in a customer's account. | | `line_item.creation` | `crm.objects.line_items.read` | Get notified if any line item is created in a customer's account. | | `line_item.deletion` | `crm.objects.line_items.read` | Get notified if any line item is deleted in a customer's account. | | `line_item.associationChange` | `crm.objects.line_items.read` | Get notified if a line item has an association added or removed between itself and another supported webhook object (contact, company, deal, ticket, line item, or product). | | `line_item.restore` | `crm.objects.line_items.read` | Get notified if a line item is restored from deletion. | | `line_item.merge` | `crm.objects.line_items.read` | Get notified if a line item is merged with another. | | `line_item.propertyChange` | `crm.objects.line_items.read` | Get notified if a specified property is changed for any line item in a customer's account. | The following conversations subscription types are available to you to subscribe to if you're using the [conversations inbox and messages API](/guides/api/conversations/inbox-and-messages), which is currently <u>in beta</u>: | Subscription type | Scope | Description | | --- | --- | --- | | `conversation.creation` | `conversations.read` | Get notified if a new thread is created in an account. | | `conversation.deletion` | Get notified if a thread is archived or soft-deleted in an account. | | `conversation.privacyDeletion` | Get notified if a thread is permanently deleted in an account. | | `conversation.propertyChange` | Get notified if a property on a thread has been changed. | | `conversation.newMessage` | Get notified if a new message on a thread has been received. | For property change subscriptions, you will need to specify which property you want to be notified of. You can specify multiple property change subscriptions. If a customer's account doesn't have the property you specify in a subscription, you will not get any webhooks from that customer for that property. Certain properties are not available for CRM property change subscriptions. These properties are: - `num_unique_conversion_events` - `hs_lastmodifieddate` If you are using the [conversations messages and inbox API](/guides/api/conversations/inbox-and-messages), which is currently <u>in beta</u>, the following properties are available: - `assignedTo`**:** the conversation thread has been reassigned or unassigned. If the thread was reassigned, the `propertyValue` will be an actor ID in the webhooks payload; if unassigned, it will be empty. - `status`**:** the status of the conversation thread has changed. In the webhooks payload, the `propertyValue` will either be `OPEN` or `CLOSED`. - `isArchived`**:** the conversation thread has been restored. The `propertyValue` in the webhooks payload will always be `FALSE`. ### Create subscriptions in your developer account You can create webhook subscriptions in your HubSpot developer account. - In your HubSpot developer account, navigate to the **Apps** dashboard. - Click the **name** of an app. - In the left sidebar menu, navigate to **Webhooks**. - Click **Create subscription**. - In the right panel, click the **Which object types?** dropdown menu and select the **objects** you want to create a subscription for. - Click the **Listen for which events?** dropdown menu and select the **event types**. - If you're creating a subscription for property change events, click the **Which properties?** dropdown menu and select the **properties** to listen for. - Click **Subscribe**. The subscription will appear in your webhooks settings. New subscriptions are created in a paused state, so you will need to activate the subscription for webhooks to send: - In the _Event subscriptions_ section, hover over the object type and click **View subscriptions**. - Select the **checkbox** next to the event, then in the table header click **Activate**. ### Manage subscriptions via API You can programmatically manage subscriptions for public apps using the endpoints listed in the sections below. You will need to use your developer API key when making requests to these endpoints. A full list of webhook endpoints for public apps can be found in the [reference documentation](/reference/api/app-management/webhooks). A subscription object can include the following fields: | Field | Description | | --- | --- | | `id` | A number representing the unique ID of a subscription. | | `createdAt` | The time in milliseconds when this subscription was created. | | `createdBy` | The user ID associated with the user who created the subscription. | | `active` | This indicates whether or not the subscription is turned on and actively triggering notifications. The value can be `true` or `false`. | | `eventType` | The type of subscription. The [table](/guides/api/app-management/webhooks#webhook-subscriptions) at the start of this section includes the available subscription types. | | `propertyName` | The name of the property the subscription will listen for changes to. This is only needed for property change subscription types. | ### Get subscriptions To retrieve the list of subscriptions, make a `GET` request to `webhooks/v3/{appId}/subscriptions`. The response will be an array of objects representing your subscriptions. Each object will include information on the subscription like the ID, create date, type, and whether or not it’s currently active. Here’s what an example response would look like: ```json // Example GET request to https://api.hubapi.com/webhooks/v3/{appId}/subscriptions [ { "id": 25, "createdAt": 1461704185000, "createdBy": 529872, "eventType": "contact.propertyChange", "propertyName": "lifecyclestage", "active": false }, { "id": 59, "createdAt": 1462388498000, "createdBy": 529872, "eventType": "company.creation", "active": false }, { "id": 108, "createdAt": 1463423132000, "createdBy": 529872, "eventType": "deal.creation", "active": true } ] ``` ### Create a new subscription To create a new subscription, make a `POST` request to `webhooks/v3/{appId}/subscriptions`. In the request body, you can include the following fields: | Field | Description | | --- | --- | | `eventType` | The type of subscription. | | `propertyName` | The name of the property the subscription will listen for changes to. This is only needed for property change subscription types. | | `active` | This indicates whether or not the subscription is turned on and actively triggering notifications. The value can be `true` or `false`. | You don't need to include `id`, `createdAt`, or `createdBy`, as those fields are set automatically. For example, your request body may appear similar to the following: ```json // Example POST request to https://api.hubapi.com/webhooks/v3/{appId}/subscriptions { "eventType": "company.propertyChange", "propertyName": "companyname", "active": false } ``` The `eventType` must be a valid subscription type as defined in the above section and the `propertyName` must be a valid property name. If a customer has no property defined that matches this value, then this subscription will not result in any notifications. ### Update a subscription To activate or pause a subscription, make a `PUT` request to `webhooks/v3/{appId}/subscriptions/{subscriptionId}`. In the request body, include the following: | Field | Description | | --- | --- | | `active` | This indicates whether or not the subscription is turned on and actively triggering notifications. The value can be `true` or `false`. | ### Delete a subscription To delete a subscription, make a `DELETE` request to `webhooks/v3/{appId}/subscriptions/{subscriptionId}`. ## Webhook payloads The endpoint at the target URL that you specify in your app's webhooks settings will receive `POST` requests containing JSON formatted data from HubSpot. To ensure that the requests you're getting at your webhook endpoint are actually coming from HubSpot, HubSpot populates a `X-HubSpot-Signature` header with a SHA-256 hash built using the client secret of your app combined with details of the request. Learn more about [validating request signatures](/guides/apps/authentication/validating-requests). Use the tables below to view details about fields that may be contained in the payload. | Field | Description | | --- | --- | | `objectId` | The ID of the object that was created, changed, or deleted. For contacts this is the contact ID; for companies, the company ID; for deals, the deal ID; and for conversations the [thread ID](/guides/api/conversations/inbox-and-messages). | | `propertyName` | This is only sent for property change subscriptions and is the name of the property that was changed. | | `propertyValue` | This is only sent for property change subscriptions and represents the new value set for the property that triggered the notification. | | `changeSource` | The source of the change. This can be any of the change sources that appear in contact property histories. | | `eventId` | The ID of the event that triggered this notification. This value is not guaranteed to be unique. | | `subscriptionId` | The ID of the subscription that triggered a notification about the event. | | `portalId` | The customer's [HubSpot account ID](https://knowledge.hubspot.com/account-management/manage-multiple-hubspot-accounts#check-your-current-account) where the event occurred. | | `appId` | The ID of your application. This is used in case you have multiple applications pointing to the same webhook URL. | | `occurredAt` | When this event occurred as a millisecond timestamp. | | `subscriptionType` | The type of subscription this notification is for. Review the list of supported subscription types in the [webhooks subscription section above](#webhook-subscriptions). | | `attemptNumber` | Starting at 0, which number attempt this is to notify your service of this event. If your service times-out or throws an error as describe in the _Retries_ section below, HubSpot will attempt to send the notification again. | | `messageId` | This is only sent when a webhook is listening for new messages to a thread. It is the ID of the new message. | | `messageType` | This is only sent when a webhook is listening for new messages to a thread. It represents the type of message you're sending. This value can either be `MESSAGE` or `COMMENT`. | | Field | Description | | --- | --- | | `primaryObjectId` | The ID of the merge winner, which is the record that remains after the merge. In the HubSpot merge UI, this is the record on the right. | | `mergedObjectIds` | An array of IDs that represent the records that are merged into the merge winner. In the HubSpot merge UI, this is the record on the left. | | `newObjectId` | The ID of the record that is created as a result of the merge. This is separate from `primaryObjectId` because in some cases a new record is created as a result of the merge. | | `numberOfPropertiesMoved` | An integer representing how many properties were transferred during the merge. | | Field | Description | | --- | --- | | `associationType` | The type of association, which will be one of the following:<ul><li>`CONTACT_TO_COMPANY`</li><li>`CONTACT_TO_DEAL`</li><li>`CONTACT_TO_TICKET`</li><li>`CONTACT_TO_CONTACT`<br /><br /></li><li>`COMPANY_TO_CONTACT`</li><li>`COMPANY_TO_DEAL`</li><li>`COMPANY_TO_TICKET`</li><li>`COMPANY_TO_COMPANY`<br /><br /></li><li>`DEAL_TO_CONTACT`</li><li>`DEAL_TO_COMPANY`</li><li>`DEAL_TO_LINE_ITEM`</li><li>`DEAL_TO_TICKET`</li><li>`DEAL_TO_DEAL`<br /><br /></li><li>`TICKET_TO_CONTACT`</li><li>`TICKET_TO_COMPANY`</li><li>`TICKET_TO_DEAL`</li><li>`TICKET_TO_TICKET`<br /><br /></li><li>`LINE_ITEM_TO_DEAL`</li></ul> | | `fromObjectId` | The ID of the record that the association change was made from. | | `toObjectId` | The ID of the secondary record in the association event. | | `associationRemoved` | A boolean that represents the following:<ul><li>`true`: the webhook was triggered by removing an association.</li><li>`false`: the webhook was triggered by creating an association.</li></ul> | | `isPrimaryAssociation` | A boolean that represents the following:<ul><li>`true`: the secondary record is the [primary association](https://knowledge.hubspot.com/records/associate-records#primary-company-information) of the record that the association change was made from.</li><li>`false`: the record is <u>not</u> the primary association of the record that the association change was made from. </li></ul>**Please note:** creating a primary association instance between two object records will cause the corresponding non-primary association to also be created. This may result in two webhook messages. | ```json // [ { "objectId": 1246965, "propertyName": "lifecyclestage", "propertyValue": "subscriber", "changeSource": "ACADEMY", "eventId": 3816279340, "subscriptionId": 25, "portalId": 33, "appId": 1160452, "occurredAt": 1462216307945, "eventType": "contact.propertyChange", "attemptNumber": 0 }, { "objectId": 1246978, "changeSource": "IMPORT", "eventId": 3816279480, "subscriptionId": 22, "portalId": 33, "appId": 1160452, "occurredAt": 1462216307945, "eventType": "contact.creation", "attemptNumber": 0 } ] ``` As shown above, you should expect to receive an array of objects in a single request. The batch size can vary, but will be under 100 notifications. HubSpot will send multiple notifications when a lot of events have occurred within a short period of time. For example, if you've subscribed to new contacts and a customer imports a large number of contacts, HubSpot will send you the notifications for these imported contacts in batches and not one per request. HubSpot does not guarantee that you'll receive these notifications in the order they occurred. Use the `occurredAt` property for each notification to determine when the event that triggered the notification occurred. HubSpot also does not guarantee that you'll only get a single notification for an event. Though this should be rare, it is possible that HubSpot will send you the same notification multiple times. ## Privacy compliant contact deletions HubSpot users have the ability to permanently delete a contact record to comply with privacy laws. Learn more about performing a [GDPR compliant delete](https://knowledge.hubspot.com/privacy-and-consent/how-do-i-perform-a-gdpr-delete-in-hubspot). You can subscribe to the `contact.privacyDeletion` subscription type to receive webhook notifications when a user performs a privacy compliant contact deletion. Privacy deletion notifications have some special behavior: - A privacy deletion event will also trigger the contact deletion event, so you will receive two notifications if you are subscribed to both events. - These notifications will not necessarily be sent in any specific order or in the same batch of messages. You will need to use the object ID to match the separate messages. ## Security To ensure that the requests you're getting at your webhook endpoint are actually coming from HubSpot, HubSpot populates a `X-HubSpot-Signature` header with a SHA-256 hash of the concatenation of the app-secret for your application and the request body HubSpot is sending. To verify this signature, concatenate the app secret of your application and the un-parsed request body of the request you're handling, and get a SHA-256 hash of the result. Compare the resulting hash with the value of the `X-HubSpot-Signature`. If these values match, then this verifies that this request came from HubSpot. Or, the request came from someone else who knows your application secret. It's important to keep this value secret. If these values do not match, then this request may have been tampered with in-transit or someone may be spoofing webhook notifications to your endpoint. Learn more about [validating signature requests](/guides/apps/authentication/validating-requests). ## Retries If your service has problems handling notifications at any time, HubSpot will attempt to re-send failed notifications up to 10 times. HubSpot will retry in the following cases: - **Connection failed:** HubSpot cannot open an HTTP connection to the provided webhook URL. - **Timeout:** your service takes longer than five seconds to send back a response to a batch of notifications. - **Error codes:** your service responds with any HTTP status code (4xx or 5xx). Notifications will be retried up to 10 times. These retries will be spread out over the next 24 hours, with varying delays between requests. Individual notifications will have some randomization applied, to prevent a large number of concurrent failures from being retried at the exact same time. ## Limits `POST` requests that HubSpot sends to your service via your webhook subscriptions will <u>not</u> count against your [app's API rate limits](/guides/apps/api-usage/usage-details). You can create a maximum of 1000 subscriptions per application. If you attempt to create more you will receive a 400 bad request in return with the following body: ```json // { "status": "error", "message": "Couldn't create another subscription. You've reached the maximum number allowed per application (1000).", "correlationId": "2c9beb86-387b-4ff6-96f7-dbb486c00a95", "requestId": "919c4c84f66769e53b2c5713d192fca7" } ``` # Workflows API Overview | Workflows API @ Workflows are one of the most essential pieces to the HubSpot marketing software, because it, along with Contacts and email, is at the heart of marketing automation. Using workflows, users can set up sophisticated marketing automation campaigns that rely on triggering relevant and timely actions (like emails) for contacts in their database. $ guides & api/automation --- # Workflows API Overview Use workflows to automate your marketing, sales, and service processes. Set enrollment criteria to enroll specific records or manually enroll your contacts. Add actions for your contacts, companies, deals, quotes, and tickets. You can also take action on associated records. For example, updating an enrolled contact’s associated company. To test the workflows tool or the Workflows API, you can [sign up for a developer portal](https://app.hubspot.com/signup-hubspot/developers). This will give you access to a sandboxed instance of the tool. Learn more about [creating and using workflows](https://knowledge.hubspot.com/automation/topics#workflows). - This API is currently in <u>maintenance mode;</u> there will not be any updates to the API and many features are not supported. For a list of supported actions, refer to the [Supported Actions table](#supported-actions). - The Workflows API currently only supports <u>contact-based</u> workflows. ## Using the Workflows API The Workflows API allows for full CRUD access to any workflow in a given portal. The most common use case for the API is to [create a workflow](/reference/api/automation/create-manage-workflows/v3#create-a-workflow) for marketing automation. Then, [enroll contacts](/reference/api/automation/create-manage-workflows/v3#create-a-workflow) in the workflow based on triggers in the external application. You can also use [webhooks in workflows](https://developers.hubspot.com/docs/reference/api/automation/create-manage-workflows/v3) to trigger notifications to external applications.. If polling the API becomes too much of a hassle while developing an integration with HubSpot, this is a great alternative. For questions about using the Workflows API, connect with HubSpot's product team and other developers working in HubSpot on the [HubSpot Developer's Forum.](https://community.hubspot.com/t5/HubSpot-Developers/ct-p/developers) ## Supported Actions When using this API, only certain actions are supported. Any actions not in the following list will be considered an UNSUPPORTED_ACTION. - When using the [Create a workflow API](/reference/api/automation/create-manage-workflows#create-a-workflow), if an UNSUPPORTED_ACTION is included, the workflow will not be created. - When using the [Get workflow API](/reference/api/automation/create-manage-workflows#get-a-workflow), if unsupported actions are included, the action type returned will be UNSUPPORTED_ACTION. A list of supported actions can be found below: | Type | Definition | | --- | --- | | ADD_SUBTRACT_PROPERTY | Increase or decrease a value in a number type property for the enrolled contact. | | COPY_COMPANY_PROPERTY | Copy a property value from the enrolled contact's associated company to another property in the same contact. | | COPY_PROPERTY | Copy a property value of the enrolled contact to another property in the same record. | | CREATE_SFDC_TASK | Set a customized Salesforce task for the enrolled contact. | | DEAL | Create a new deal record. This deal will be associated with the enrolled contact. | | EMAIL | Send marketing emails that have been saved for automation to the enrolled contact. | | LEAD_ASSIGNMENT | Assign the enrolled contacts to users equally within a selected team or between specified users. This action is only compatible with activated, paid users. | | NOTIFICATION | Send internal marketing emails that have been saved for automation to specified email addresses, or a contact property that stores an email address, including any custom properties. | | NOTIFICATION_STATION | Send an in-app notification to specified teams or users. | | SET_COMPANY_PROPERTY | Set a property value on the enrolled contact's associated company. | | SET_CONTACT_PROPERTY | Set a property value on the enrolled contact. | | SET_SALESFORCE_CAMPAIGN_MEMBERSHIP | Set a Salesforce campaign for the enrolled contact. | | SMS_NOTIFICATION | Send a text message to a HubSpot user or specified number. | | TASK | Create a new task. This task will be associated with the enrolled contact. | | TICKET | Create a new ticket record. This ticket will be associated with the enrolled contact. | | UPDATE_EMAIL_SUBSCRIPTION | Customize the subscription status of the enrolled contact. | | UPDATE_LIST | Add or remove contacts from a static list. | | WEBHOOK | Trigger a webhook to an external application. | | WORKFLOW_ENROLLMENT | Enroll the contact in another workflow. | # Automation API | Custom Workflow Actions @ An introduction and overview of workflow extensions. $ guides & api/automation --- # Custom Workflow Actions Use [HubSpot's workflows tool](https://knowledge.hubspot.com/workflows/create-workflows) to automate business processes and allow your team to be more efficient. You can create custom workflow actions to integrate your service with HubSpot's workflows. After setting up your custom action, when users install your application, they can add the custom action to their workflows. When those workflows execute, HTTPS requests will send to the configured URL with the payload that you set up. Requests made for your custom action will use the v2 version of the _X-HubSpot-Signature_. Learn more about [validating requests from HubSpot](/guides/apps/authentication/validating-requests). [](#add) You can also skip forward to the following sections: - [Before you get started](#before-you-get-started) - [Define your custom action](#define-your-custom-action) - [Functions](#functions) - [Input fields](#input-fields) - [Fetch external data fields](#using-external-data) - [Output fields](#output-fields) - [Labels](#labels) - [Execution](#execution) - [Asynchronous custom action execution](/guides/api/automation/custom-workflow-actions#asynchronous-execution) - [Add custom execution messages with execution rules](#add-custom-execution-messages-with-rules) - [Test and publish your custom action](#test-and-publish-your-custom-action) The final section in this article provides [several custom action examples](#custom-action-examples). ## Before you get started - You'll need a [HubSpot developer account](https://app.hubspot.com/signup-hubspot/developers) with a [HubSpot app](/guides/apps/public-apps/overview). Your app logo will be used as the icon for the custom action. - When making requests to the custom workflow action endpoints, you must authenticate the calls using [OAuth](/guides/apps/authentication/working-with-oauth) or [private app](/guides/apps/private-apps/overview) access tokens. Learn more about [authentication methods on HubSpot](/guides/apps/authentication/intro-to-auth). ## Define your custom action To create a custom workflow action, you'll need to define the action using the following fields. This also specifies the request format for requests coming from HubSpot, as well as the handling of responses from your service. - `actionUrl`: the URL where an HTTPS request is sent when the action is executed. The request body will contain information about which user the action is executing on behalf of, and what values were entered for the input fields. - `objectTypes`: which CRM objects this action can be used with. - `published`: by default, custom actions are created in an unpublished state. Unpublished actions are only visible in the developer portal associated with your HubSpot application. To make a custom action visible to users, update the published flag on your action definition to true. - `inputFields`: the inputs that the action receives. These will be filled by the user. - `inputFieldDependencies`: these rules allow fields to be grayed out until other fields meet specific conditions. - `outputFields`: the values that the action will output that can be used by later actions in the workflow. A custom action can have zero, one, or many outputs. - `objectRequestOptions`: properties of the enrolled object included in the payload to the actionUrl. - `labels`: copy that describes to the user what the action's fields represent and what the action does. English labels are required, but labels can be specified in any of the following supported languages as well: - French (`fr`) - German (`de`) - Japanese (`ja`) - Spanish (`es`) - Brazilian Portuguese (`pt-br`) - Dutch (`nl`) - Polish (`pl`) - Swedish (`sv`) - Italian (`it`) - Danish - Denmark (`da_dk`) - Finnish (`fi`) - Norwegian (`no`) - Traditional Chinese - Taiwan (`zh-tw`) - `executionRules`: a list of definitions you can specify to surface errors from your service to the user creating the workflow. - `functions`: code snippets that are run in order to transform the payload being sent to a url and/or transform the response from that url. ### Example custom action definition ```json // { "actionUrl": "https://webhook.site/94d09471-6f4c-4a7f-bae2-c9a585dd41e0", "objectTypes": ["CONTACT"], "inputFields": [ { "typeDefinition": { "name": "staticInput", "type": "string", "fieldType": "text" }, "supportedValueTypes": ["STATIC_VALUE"], "isRequired": true }, { "typeDefinition": { "name": "objectInput", "type": "string", "fieldType": "text" }, "supportedValueTypes": ["OBJECT_PROPERTY"], "isRequired": true }, { "typeDefinition": { "name": "optionsInput", "type": "enumeration", "fieldType": "select", "optionsUrl": "https://webhook.site/94d09471-6f4c-4a7f-bae2-c9a585dd41e0" }, "supportedValueTypes": ["STATIC_VALUE"] } ], "inputFieldDependencies": [ { "dependencyType": "SINGLE_FIELD", "dependentFieldNames": ["objectInput"], "controllingFieldName": "staticInput" } ], "outputFields": [ { "typeDefinition": { "name": "myOutput", "type": "string", "fieldType": "text" }, "supportedValueTypes": ["STATIC_VALUE"] } ], "objectRequestOptions": { "properties": ["email"] }, "labels": { "en": { "inputFieldLabels": { "staticInput": "Static Input", "objectInput": "Object Property Input", "optionsInput": "External Options Input" }, "actionName": "My Extension", "actionDescription": "My Extension Description", "appDisplayName": "My App Display Name", "actionCardContent": "My Action Card Content" } }, "functions": [ { "functionType": "POST_ACTION_EXECUTION", "functionSource": "exports.main = (event, callback) => {\r\n callback({\r\n outputFields: {\r\n myOutput: \"example output value\"\r\n }\r\n });\r\n}" }, { "functionType": "POST_FETCH_OPTIONS", "functionSource": "exports.main = (event, callback) => {\r\n callback({\r\n \"options\": [{\r\n \"label\": \"Big Widget\",\r\n \"description\": \"Big Widget\",\r\n \"value\": \"10\"\r\n },\r\n {\r\n \"label\": \"Small Widget\",\r\n \"description\": \"Small Widget\",\r\n \"value\": \"1\"\r\n }\r\n ]\r\n });\r\n}" } ] } ``` The above definition will render the following in the workflows tool: There are two types of calls made for custom workflow actions: - **Field option fetches:** populate a list of valid options when a user is configuring a field. Learn more about using field option fetches to fetch external data fields. - **Action execution requests:** made when an action is being executed by a workflow that includes your custom action. ## Functions Functions are snippets of code used to modify payloads before sending them to an API. You can also use functions to parse results from an API. HubSpot's functions are backed by [AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html). In the following code: - `event` contains the data that is passed to the function - `exports.main` is the method that will be called when the function is run. - The `callback` function can be used to return a result. The code should be formatted as follows: ```json exports.main = (event, callback) => { callback({ "data": { "field": "email", "phone": "1234567890" } }); } ``` When setting up a function, the `functionSource` format will be in string. Ensure that the characters in the code have been escaped. Generally, the definition of a function will follow the format: ```json // { "functionType": "PRE_ACTION_EXECUTION", "functionSource": "exports.main = (event, callback) => {\r\n callback({\r\n \"data\": {\r\n \"field\": \"email\",\r\n \"phone\": \"1234567890\" \r\n }\r\n });\r\n" } ``` ### Example function In the example below, examine the input code, the function used, and the output produced. **Function input:** ```json // { "callbackId": "ap-102670506-56777914962-11-0", "origin": { "portalId": 102670506, "actionDefinitionId": 10860211, "actionDefinitionVersion": 1, "extensionDefinitionId": 10860211, "extensionDefinitionVersionId": 1 }, "context": { "source": "WORKFLOWS", "workflowId": 192814114 }, "object": { "objectId": 614, "objectType": "CONTACT" }, "inputFields": { "widgetOwner": "10887165", "widgetName": "My Widget Name" } } ``` **Function used: ** ```json // exports.main = (event, callback) => { callback({ "data": { "myObjectId": event["object"]["objectId"], "myField": event["inputFields"]["widgetName"] } }); } ``` **Output expected:** ```json // { "data": { "myObjectId": 614, "myField": "My Widget Name" } } ``` ## Input fields Input field definitions will adhere to the following format: - `name`: the internal name of the input field, separate from its label. The label displayed in the UI must be defined using the [labels section](#labels) of the custom action definition. - `type`: the type of value required by the input. - `fieldType`: how the input field should be rendered in the UI. Input fields mimic CRM properties, learn more about [valid `type` and `fieldType` combinations](/guides/api/crm/properties#property-type-and-fieldtype-values) - `supportedValueTypes` have two valid values: - `OBJECT_PROPERTY`: the user can select a property from the enrolled object or an output from a previous action to use as the value of the field. - `STATIC_VALUE`: this should be used in all other cases. It denotes that the user must enter a value themselves. - `isRequired`: this determines whether the user must give a value for this input or not Input field definitions should be formatted as follows: ```json // { "typeDefinition": { "name": "staticInput", "type": "string", "fieldType": "text" }, "supportedValueTypes": ["STATIC_VALUE"], "isRequired": true } ``` You can also hard code options for the user to select: ```json // { "typeDefinition": { "name": "widgetColor", "type": "enumeration", "fieldType": "select", "options": [ { "value": "red", "label": "Red" }, { "value": "blue", "label": "Blue" }, { "value": "green", "label": "Green" } ] }, "supportedValueTypes": ["STATIC_VALUE"] } ``` ### Using external data Instead of hard coding field options, you can also fetch external data with external data fields. For example, you can retrieve a list of meeting projects or a list of products to serve as inputs. The input field should be formatted as follows: ```json // { "typeDefinition": { "name": "optionsInput", "type": "enumeration", "fieldType": "select", "optionsUrl": "https://your-url-here.com" }, "supportedValueTypes": ["STATIC_VALUE"] } ``` The payload sent to the `optionsURL` will be formatted as follows: ```json // { "origin": { // The customer's portal ID "portalId": 1, // Your custom action definition ID "actionDefinitionId": 2, // Your custom action definition version "actionDefinitionVersion": 3 }, // The workflow object type the action is being used in "objectTypeId" : "0-1" // The input field you are fetching options for "inputFieldName": "optionsInput", // The values for the fields that have already been filled out by the workflow user "inputFields": { "widgetName": { "type": "OBJECT_PROPERTY", "propertyName": "widget_name" }, "widgetColor": { "type": "STATIC_VALUE", "value": "blue" } }, "fetchOptions": { // The search query provided by the user. This should be used to filter the returned // options. This will only be included if the previous option fetch returned // `searchable: true` and the user has entered a search query. "q": "option label", // The pagination cursor. This will be the same pagination cursor that was returned by // the previous option fetch; it can be used to keep track of which options have already // been fetched. "after": "1234=" } } ``` The expected response should be formatted as follows: ```json // { "options": [ { "label": "Big Widget", "description": "Big Widget", "value": "10" }, { "label": "Small Widget", "description": "Small Widget", "value": "1" } ], // Optional. The pagination cursor. If this is provided, the Workflows app will render // a button to load more results at the bottom of the list of options when a user is // selecting an option, and when the next page is loaded this value will be included in // the request payload under `fetchOptions.after`. "after": "1234=", // Optional. Default is false. If this is true, the Workflows app will render a search // field to allow a user to filter the available options by a search query, and when // a search query is entered by the user, options will be re-fetched with that search // term in the request payload under `fetchOptions.q`. "searchable": true } ``` In the code above, note that there's pagination being set to limit the number of returned options. This instructs the workflow that more options can be loaded. In addition, the list of options is made searchable by including `searchable:true`. ### Modify external data To manage external data, you can include two hooks to customize the field option fetch lifecycle: - **`PRE_FETCH_OPTIONS`:** a function that configures the payload sent from HubSpot. - **`POST_FETCH_OPTIONS`:** a function that transforms the response from your service into a format that's understood by HubSpot. #### PRE_FETCH_OPTIONS When included, this function will apply to each input field. You can apply it to a specific input field by specifying an `id` in the function definition. ```json // { "functionType": "PRE_FETCH_OPTIONS", "functionSource": "...", "id": "inputField" } ``` The payload sent from HubSpot will be formatted as follows: ```json // { "origin": { // The customer's portal ID "portalId": 1, // Your custom action definition ID "actionDefinitionId": 2, // Your custom action definition version "actionDefinitionVersion": 3 }, // The input field you are fetching options for "inputFieldName": "optionsInput", // Your configured external data field webhook URL "webhookUrl": "https://myapi.com/hubspot/widget-sizes", // The values for the fields that have already been filled out by the workflow user "inputFields": { "widgetName": { "type": "OBJECT_PROPERTY", "propertyName": "widget_name" }, "widgetColor": { "type": "STATIC_VALUE", "value": "blue" }, "fetchOptions": { // The search query provided by the user. This should be used to filter the returned // options. This will only be included if the previous option fetch returned // `searchable: true` and the user has entered a search query. "q": "option label", // The pagination cursor. This will be the same pagination cursor that was returned by // the previous option fetch; it can be used to keep track of which options have already // been fetched. "after": "1234=" } } } ``` The response should then be formatted as follows: ```json // { // The webhook URL for HubSpot to call "webhookUrl": "https://myapi.com/hubspot", // Optional. The request body. "body": "{\"widgetName\": \"My new widget\", \"widgetColor\": \"blue\"}", // Optional. A map of custom request headers to add. "httpHeaders": { "My-Custom-Header": "header value" }, // Optional. The Content-Type of the request. Default is application/json. "contentType": "application/json", // Optional. The Accept type of the request. Default is application/json. "accept": "application/json", // Optional. The HTTP method with which to make the request. // Valid values are GET, POST, PUT, PATCH, and DELETE. // Default is POST. "httpMethod": "POST" } ``` #### POST_FETCH_OPTIONS To parse the response into an expected format, per the external data fields, use a `POST_FETCH_OPTIONS` function. The definition for a `POST_FETCH_OPTIONS` function is the same as a `PRE_FETCH_OPTIONS` function. When external external data fetch options are defined, a dropdown menu will be rendered in the input options for the action. ```json // { "functionType": "POST_FETCH_OPTIONS", "functionSource": "...", "id": "inputField" } ``` The function input will be formatted as follows: ```json // { // The requested field key "fieldKey": "widgetSize", // The webhook response body from your service "responseBody": "{\"widgetSizes\": [10, 1]}" } ``` The function output will be formatted as follows: ```json // { "options": [ { "label": "Big Widget", "description": "Big Widget", "value": "10" }, { "label": "Small Widget", "description": "Small Widget", "value": "1" } ] } ``` ## Output fields Use output fields to output values from your custom action to use in other actions. The definition for output fields is similar to the definition for input fields: - **name:** how this field is referenced in other parts of the Custom Action. The label displayed in the UI must be defined using the \`labels\` section of the Custom Action - **type:** the type of value required by the input. - **fieldType:** is how the input field should be rendered in the UI. Input fields mimic CRM properties, learn more about [valid `type` and `fieldType` combinations](/guides/api/crm/properties#property-type-and-fieldtype-values) The output field should be formatted as follows: ```json // { "outputFields": [ { "typeDefinition": { "name": "myOutput", "type": "string", "fieldType": "text" } } ] } ``` When using an output field, values are parsed from the response from the `actionURL`. For example, you can copy output fields to an existing property in HubSpot. **** ## Labels Use labels to add text to your outputs or inputs in the workflow editor. Labels are loaded into HubSpot's language service and may take a few minutes to display. Portals [set to different regions or languages](https://knowledge.hubspot.com/account-management/change-your-language-and-region-settings#customize-default-user-language-date-and-number-format) will display the label in the corresponding language, if available. - **`labels`:** copy describing what the action's fields represent and what the action does. English labels are required, but labels can be specified in any of the following supported languages as well: French (`fr`), German (`de`), Japanese (`ja`), Spanish (`es`), Brazilian Portuguese (`pt-br`), and Dutch (`nl`). - **`actionName`:** the action's name shown in the Choose an action panel in the workflow editor. - **`actionDescription`:** a detailed description for the action shown when the user is configuring the custom action. - **`actionCardContent`:** a summarized description shown in the action's card. - **`appDisplayName`:** The name of the section in the “Choose an Action” panel where all the actions for the app are displayed. If appDisplayName is defined for multiple actions, the first one found is used. - **`inputFieldLabels`:** an object that maps the definitions from inputFields to the corresponding labels the user will see when configuring the action. - **`outputFieldLabels`:** an object that maps the definitions from outputFields to the corresponding labels shown in the workflows tool. - **`inputFieldDescriptions`:** an object that maps the definitions from inputFields to the descriptions below the corresponding labels. - **`executionRules`:** an object that maps the definitions from your executionRules to messages that will be shown for action execution results on the workflow history. There is a separate section in these docs for execution rules. Label definitions should be formatted as follows: ```json // { "labels":{ "en":{ "actionName":"Create Widget", "actionDescription":"This action will create a new widget in our system. So cool!", "actionCardContent":"Create widget {{widgetName}}", "appDisplayName":"My App Display Name", "inputFieldLabels":{ "widgetName":"Widget Name", "widgetOwner":"Widget Owner" }, "outputFieldLabels":{ "outputOne":"First Output" }, "inputFieldDescriptions":{ "widgetName":"Enter the full widget name. I support <a href=\"https://hubspot.com\">links</a> too." }, "executionRules":{ "alreadyExists":"The widget with name {{ widgetName }} already exists" } } } } ``` ## Execution When an execution executes, a https request is sent to the `actionUrl`. - `callbackId`**:** a unique ID for the specific execution. If the custom action execution is [blocking](#block), use this ID. - **`object`:** the values of the properties requested in `objectRequestOptions`. - **`InputFields`:** the values for the inputs that the user has filled out. The execution payload will be formatted as follows: ```json // { "callbackId": "ap-102670506-56776413549-7-0", "origin": { "portalId": 102670506, "actionDefinitionId": 10646377, "actionDefinitionVersion": 1 }, "context": { "source": "WORKFLOWS", "workflowId": 192814114 }, "object": { "objectId": 904, "properties": { "email": "ajenkenbb@gnu.org" }, "objectType": "CONTACT" }, "inputFields": { "staticInput": "My Static Input", "objectInput": "995", "optionsInput": "1" } } ``` The expected response should be formatted as follows: ```json // { "outputFields": { "myOutput": "Some value", "hs_execution_state": "SUCCESS" } } ``` When looking at the execution response: - **`outputFields`:** the values of the output fields defined earlier. These values can be used in later actions. - **`hs_execution_state`:** an optional special value that can added to outputFields. It is not possible to specify a retry, only the following values can be added: - SUCCESS - FAIL_CONTINUE - BLOCK - ASYNC `SUCCESS` and `FAIL_CONTINUE` indicate that the action has completed and the workflow should move on to the next action to execute. If no execution state is specified, status codes will be used to determine the result of an action: - **2xx status codes:** the action has completed successfully. - **4xx status codes:** the action has failed. The exception is 429 Rate Limited status codes, which are re-treated as retries, and the Retry-After header is respected. - **5xx status codes:** there was a temporary problem with the service, and the action will be retried at a later time. An exponential backoff system is used for retries, retries will continue for up to 3 days before failing. ## Execution functions Use execution functions to format data before sending to the `actionURL` and parse data from the `actionURL`. There are two types of execution functions: - `PRE_ACTION_EXECUTION` - `POST_ACTION_EXECUTION` #### PRE_ACTION_EXECUTION function Use `PRE_ACTION_EXECUTION` functions to format data before sending it to the `actionURL` The function definition will be formatted as follows: ```json // { "functionType": "PRE_ACTION_EXECUTION", "functionSource": "..." } ``` The function input should be formatted as follows: ```json // { "webhookUrl": "https://actionurl.com/", "callbackId": "ap-102670506-56776413549-7-0", "origin": { "portalId": 102670506, "actionDefinitionId": 10646377, "actionDefinitionVersion": 1 }, "context": { "source": "WORKFLOWS", "workflowId": 192814114 }, "object": { "objectId": 904, "properties": { "email": "ajenkenbb@gnu.org" }, "objectType": "CONTACT" }, "inputFields": { "staticInput": "My Static Input", "objectInput": "995", "optionsInput": "1" } } ``` The function output should be formatted as follows: ```json // { // The webhook URL for HubSpot to call "webhookUrl": "https://myapi.com/hubspot", // Optional. The request body. "body": "{\"widgetName\": \"My new widget\", \"widgetColor\": \"blue\"}", // Optional. A map of custom request headers to add. "httpHeaders": { "My-Custom-Header": "header value" }, // Optional. The Content-Type of the request. Default is application/json. "contentType": "application/json", // Optional. The Accept type of the request. Default is application/json. "accept": "application/json", // Optional. The HTTP method with which to make the request. // Valid values are GET, POST, PUT, PATCH, and DELETE. // Default is POST. "httpMethod": "POST" } ``` #### POST_ACTION_EXECUTION function After receiving a response from the `actionURL`, use a `POST_ACTION_EXECUTION` function to format data for HubSpot. The function definition will be formatted as follows: ```json // { "functionType": "POST_ACTION_EXECUTION", "functionSource": "..." } ``` The function input should be formatted as follows: ```json // { "responseBody": "{\r\n \"returnValue\":\"Hello World!\"\r\n}" } ``` The function output should be formatted as follows: ```json // { "outputFields": { "myOutput": "Some value", "hs_execution_state": "SUCCESS" } } ``` ## Asynchronous execution Execute custom workflow actions asynchronously by blocking and later completing the action. ### Blocking action execution Use custom actions to block workflow execution. Instead of executing the next action in the workflow after your custom action after receiving a `completed (2xx or 4xx status code)` response from your service, the workflow will stop executing ("block") that enrollment until you tell the workflow to continue. When blocking, you can specify a value for the `hs_default_expiration` field, after which your custom action will be considered expired. The execution of the workflow will then resume and the action following your custom action will be executed, even if the action is not completed. To block a custom action, your action execution response must have the following format: ```json // { "outputFields": { // Required. Must be BLOCK for your custom action to block execution. "hs_execution_state": "BLOCK", // Optional. If not provided, a default expiration of 1 week is used. // Must be specified in ISO 8601 Duration format. // See https://en.wikipedia.org/wiki/ISO_8601#Durations "hs_expiration_duration": "P1WT1H" } } ``` ### Complete a blocked execution To complete a blocked custom action execution, use the following endpoint: `/callbacks/{callbackId}/complete` Format the request body as follows: ```json // { "outputFields": { // Required. The final execution state. Valid values are SUCCESS // (to indicate that your custom action completed successfully) or // FAIL_CONTINUE (to indicate that there was a problem with your // custom action execution) "hs_execution_state": "SUCCESS" } } ``` ## Add custom execution messages with rules Specify rules on your action to determine which message displays on the workflow's history page when the action executes. The rules will be matched against the output values from your action. These output values should be provided in the `actionURL`'s response body, in the following format: ```json // { "outputFields": { "errorCode": "ALREADY_EXISTS", "widgetName": "Test widget" } } ``` The actual messages can be specified in the labels section of the custom action: ```json // { "labels": { "executionRules": { "alreadyExists": "The widget with name {{ widgetName }} already exists", "widgetWrongSize": "Wrong widget size", "widgetInvalidSize": "Invalid widget size" } } } } ``` The `executionRules` will be tested in the order provided. If there are multiple matches, only the message from the first rule that matches is displayed to the user. The rule matches when the execution output corresponds to a specified value in the rule. For example, consider this set of `executionRules`: ```json // [ { // This matches the key of a label on the action's `labels.LANGUAGE.executionRules` map "labelName": "alreadyExists", "conditions": { "errorCode": "ALREADY_EXISTS" } }, { "labelName": "widgetWrongSize", "conditions": { "errorCode": "WIDGET_SIZE", "sizeError": ["TOO_SMALL", "TOO_BIG"] } }, { "labelName": "widgetInvalidSize", "conditions": { "errorCode": "WIDGET_SIZE" } } ] ``` With the above, the following matches would occur: - `{"errorCode": "ALREADY_EXISTS", "widgetName": "Test widget"}`: This would match the first rule, since `errorCode` is equal to `ALREADY_EXISTS`. In this instance, even though there is a `widgetName` output, it isn't used in the rule definition so any value is allowed. - `{"errorCode": "WIDGET_SIZE", "sizeError": "TOO_SMALL"}`: This would match the second rule, since `TOO_SMALL` is one of the matching `sizeError`s, and `errorCode` is `WIDGET_SIZE`. - `{"errorCode": "WIDGET_SIZE", "sizeError": "NOT_A_NUMBER"}`: This would match the third rule, since even though the `errorCode` is `WIDGET_SIZE`, the `sizeError` does not match any of the values specified by the second rule (`TOO_SMALL` or `TOO_BIG`). This matching mechanism allows you to specify fallback errors, so that you can have specific errors for important error cases, but fall back to more generic error messages for less common errors. Here is an example of how the custom message would display: ## Test and publish your custom action After creating your new custom action, you can test and publish it. ### Testing custom actions before publishing Before publishing your custom action, you can test action execution and fetching options by pointing the URL to [webhook.site](https://webhook.site/). This allows you to inspect the payload and return a specific response. You can also test the action in your developer portal by creating a workflow in the workflows tool. Then, [adding your new action](https://knowledge.hubspot.com/workflows/choose-your-workflow-actions#add-actions). When you complete your testing, it's recommended to archive your test actions. Learn more about archiving actions in the [reference documentation](/reference/api/automation/custom-workflow-actions). ### Publishing custom actions By default, custom actions are created in an unpublished state. Unpublished custom actions are only visible in the developer portal associated with the corresponding HubSpot application. To make a custom action visible to users, update the `published` flag on your action definition to `true`. If an action is unpublished, portals that have already added the action to their workflow will still be able to edit and execute already added actions. But, they won't be able to add the action again. ## Custom action examples The code snippets below provide examples of several common use-cases for custom actions, such as defining a widget or invoking a serverless function. ### Example #1 This example features the following input fields, created for contact and deal workflows: - `widgetName`: a static input field - `widgetColor`: a dropdown field with options - `widgetOwner`: a field representing a HubSpot owner. - `widgetQuantity`: a field whose derived from a property (that the user creating the workflow selects) on the enrolled object. ```json // { "actionUrl": "https://example.com/hubspot", "inputFields": [ { "typeDefinition": { "name": "widgetName", "type": "string", "fieldType": "text" }, "supportedValueTypes": ["STATIC_VALUE"], "isRequired": true }, { "typeDefinition": { "name": "widgetColor", "type": "enumeration", "fieldType": "select", "options": [ { "value": "red", "label": "Red" }, { "value": "blue", "label": "Blue" }, { "value": "green", "label": "Green" } ] }, "supportedValueTypes": ["STATIC_VALUE"] }, { "typeDefinition": { "name": "widgetOwner", "type": "enumeration", "referencedObjectType": "OWNER" }, "supportedValueTypes": ["STATIC_VALUE"] }, { "typeDefinition": { "name": "widgetQuantity", "type": "number" }, "supportedValueTypes": ["OBJECT_PROPERTY"] } ], "labels": { "en": { "actionName": "Create Widget - Example 1", "actionDescription": "This action will create a new widget in our system. So cool!", "actionCardContent": "Create widget {{widgetName}}", "inputFieldLabels": { "widgetName": "Widget Name", "widgetColor": "Widget Color", "widgetOwner": "Widget Owner", "widgetQuantity": "Widget Quantity" }, "inputFieldDescriptions": { "widgetName": "Enter the full widget name. I support <a href=\"https://hubspot.com\">links</a> too.", "widgetColor": "This is the color that will be used to paint the widget." } } }, "objectTypes": ["CONTACT", "DEAL"] } ``` ### Example #2 The following custom action uses a serverless function to transform the payload that is sent to the configured actionUrl. Since the `objectTypes` field isn't specified in the custom action definition, this action will be available in all workflows types. ```json // { "actionUrl": "https://example.com", "inputFields": [ { "typeDefinition": { "name": "widgetName", "type": "string", "fieldType": "text" }, "supportedValueTypes": ["STATIC_VALUE"], "isRequired": true } ], "labels": { "en": { "actionName": "Create Widget - Example 2", "actionCardContent": "Create widget {{widgetName}}", "inputFieldLabels": { "widgetName": "Widget Name" } } }, "functions": [ { "functionType": "PRE_ACTION_EXECUTION", "functionSource": "exports.main = function(event, callback) { return callback(transformRequest(event)); }\nfunction transformRequest(request) { return { webhookUrl: request.webhookUrl, body: JSON.stringify(request.fields), contentType: 'application/x-www-form-urlencoded', accept: 'application/json', httpMethod: 'POST' }; }" } ] } ``` ### Example #3 The following custom action has field dependencies and options that are fetched from an external API. Because the widget size depends on the widget color, the user won't be able to input a value for the widget size until a widget color is chosen. The widget cost is also dependent on the widget color, but it is conditional on the value that the user selects for the widget color; the user won't be able to input a value for the widget cost unless Red is selected as the widget color. ```json // { "actionUrl": "https://example.com/hubspot", "inputFields": [ { "typeDefinition": { "name": "widgetName", "type": "string", "fieldType": "text" }, "supportedValueTypes": ["STATIC_VALUE"], "isRequired": true }, { "typeDefinition": { "name": "widgetColor", "type": "enumeration", "fieldType": "select", "options": [ { "value": "red", "description": "red", "label": "Red" }, { "value": "blue", "description": "blue", "label": "Blue" }, { "value": "green", "description": "green", "label": "Green" } ] }, "supportedValueTypes": ["STATIC_VALUE"] }, { "typeDefinition": { "name": "widgetSize", "type": "enumeration", "fieldType": "select", "optionsUrl": "https://api.example.com/v1/widget-sizes" }, "supportedValueTypes": ["STATIC_VALUE"] }, { "typeDefinition": { "name": "widgetCost", "type": "number", "fieldType": "number" }, "supportedValueTypes": ["OBJECT_PROPERTY"] } ], "inputFieldDependencies": [ { "dependencyType": "SINGLE_FIELD", "controllingFieldName": "widgetColor", "dependentFieldNames": ["widgetSize"] }, { "dependencyType": "CONDITIONAL_SINGLE_FIELD", "controllingFieldName": "widgetColor", "controllingFieldValue": "red", "dependentFieldNames": ["widgetCost"] } ], "labels": { "en": { "actionName": "Create Widget - Example 3", "actionCardContent": "Create widget {{widgetName}}", "inputFieldLabels": { "widgetName": "Widget Name", "widgetColor": "Widget Color", "widgetSize": "Widget Size", "widgetCost": "Widget Cost" } } }, "objectTypes": ["CONTACT", "DEAL"], "functions": [ { "functionType": "PRE_FETCH_OPTIONS", "id": "widgetSize", "functionSource": "exports.main = function(event, callback) { return callback(transformRequest(event)); }\nfunction transformRequest(request) { return { webhookUrl: request.webhookUrl + '?color=' + request.fields.widgetColor.value, body: JSON.stringify(request.fields), httpMethod: 'GET' }; }" } ] } ``` # Automation API | Sequences API (BETA) @ Learn how to use the v4 automation API for sequences (BETA). $ guides & api/automation --- # Sequences API To send a series of targeted, timed email templates to nurture contacts over time, you can [create a sequence](https://knowledge.hubspot.com/sequences/create-and-edit-sequences). You can use the sequences tool to automatically create tasks to remind you to follow up with your contacts. To use this API, the user must have an [assigned _**Sales Hub**_ _Professional_ or _Enterprise_](https://knowledge.hubspot.com/account-management/manage-seats) [or _**Service Hub** Professional_ or _Enterprise_ seat](https://knowledge.hubspot.com/account-management/manage-seats). With the sequences API, you can: - Get a list of sequences - Get a specific sequence - Enroll a contact in a sequence - View a contact’s enrollment status This could be useful if you have an app that maintains a list of contacts and you want to enroll them in a HubSpot sequence. With the sequences API, from your app, you could: - Get a list of sequences - Enroll a contact in a sequence - Review the enrollment status The sections below provide a walkthrough of how to use the v4 endpoints. For a full reference of the available endpoints and their required fields, click the **Endpoints** tab at the top of this article. ## Fetch a sequence To get details about a specific sequence in your account, make a `GET` request to `/automation/v4/sequences/{sequenceId}?userId={userId}`, where the sequenceId is the ID of an existing sequence in your account. For example, to get details about a sequence that ID is `123456789` and your [user ID](/guides/api/settings/users/user-details) is `2222222`, make a `GET` request to `/automation/v4/sequences/123456789?userId=2222222`. Use the ID of the specific sequence as the sequenceId in the request URL and use your userId as a query parameter. The response for fetching a sequence would resemble the following: ```json // Example response for GET request to https://api.hubapi.com/automation/v4/sequences/{sequenceId}?userId={userId} { "id": "123456789", "name": "Melinda's simple call sequence", "createdAt": "2024-06-26T18:37:01.146Z", "updatedAt": "2024-06-26T18:37:01.146Z", "userId": "8698664", "steps": [ { "id": "198771535", "stepOrder": 0, "delayMillis": 0, "actionType": "TASK", "createdAt": "2024-06-26T18:37:01.146Z", "updatedAt": "2024-06-26T18:37:01.146Z", "taskPattern": { "id": "23594004", "taskType": "CALL", "taskPriority": "LOW", "subject": "Call contact to follow up", "createdAt": "2024-06-26T18:37:01.146Z", "updatedAt": "2024-06-26T18:37:01.146Z" } }, { "id": "198771536", "stepOrder": 1, "delayMillis": 86400000, "actionType": "TASK", "createdAt": "2024-06-26T18:37:01.146Z", "updatedAt": "2024-06-26T18:37:01.146Z", "taskPattern": { "id": "23594005", "taskType": "EMAIL", "taskPriority": "LOW", "subject": "Send follow-up email", "notes": "<p>Send an email to thank them for their time and follow up. Use one of our templates based on the call outcome.</p>", "createdAt": "2024-06-26T18:37:01.146Z", "updatedAt": "2024-06-26T18:37:01.146Z" } }, { "id": "198771537", "stepOrder": 2, "delayMillis": 0, "actionType": "FINISH_ENROLLMENT", "createdAt": "2024-06-26T18:37:01.146Z", "updatedAt": "2024-06-26T18:37:01.146Z" } ], "settings": { "id": "18167737", "eligibleFollowUpDays": "BUSINESS_DAYS", "sellingStrategy": "LEAD_BASED", "sendWindowStartMinute": 480, "sendWindowEndMinute": 1080, "taskReminderMinute": 480, "individualTaskRemindersEnabled": false, "createdAt": "2024-06-26T18:37:01.146Z", "updatedAt": "2024-06-26T18:37:01.146Z" }, "dependencies": [ { "id": "15241774", "createdAt": "2024-06-26T18:37:01.146Z", "updatedAt": "2024-06-26T18:37:01.146Z", "dependencyType": "TASK_COMPLETION", "requiredBySequenceStepId": "198771536", "reliesOnSequenceStepId": "198771535", "requiredByStepOrder": 1, "reliesOnStepOrder": 0 }, { "id": "15241775", "createdAt": "2024-06-26T18:37:01.146Z", "updatedAt": "2024-06-26T18:37:01.146Z", "dependencyType": "TASK_COMPLETION", "requiredBySequenceStepId": "198771537", "reliesOnSequenceStepId": "198771536", "requiredByStepOrder": 2, "reliesOnStepOrder": 1 } ] } ``` The details of each response field are outlined in the table below: | Field | Type | Description | | --- | --- | --- | | id | String | The sequence ID. | | name | String | The name of the sequence. | | steps | Array | The list of steps in your sequence. Each step is an object that includes details like the type, priority, and creation details. | ## List sequences To get details about a specific sequence in your account, make a `GET` request to `/automation/v4/sequences/?limit=#&userId={userId}`. Use the amount of sequences you want returned as the limit in the request URL and use your [userID](/guides/api/settings/users/user-details) as a query parameter. For example, to get a list of four sequences and your user ID is `2222222`, make a `GET` request to `/automation/v4/sequences?limit=4&userId=2222222`. The response for fetching a list of sequences would resemble the following: ```json //Example response for GET request to https://api.hubapi.com/automation/v4/sequences?limit=4&userId={userId} { "total": 7, "results": [ { "id": "88180366", "name": "Melinda's call sequence", "createdAt": "2023-07-20T15:40:04.364Z", "updatedAt": "2023-07-20T15:40:04.364Z", "userId": "2222222" }, { "id": "76853632", "name": "Melinda's Follow-up Sequence", "createdAt": "2023-01-26T15:38:05.812Z", "updatedAt": "2023-01-27T16:06:12.562Z", "userId": "2222222" }, { "id": "76897322", "name": "Melinda's Marketing Pro Demo Sequence", "createdAt": "2023-01-27T16:42:29.487Z", "updatedAt": "2023-05-03T14:40:42.507Z", "userId": "2222222" }, { "id": "76849666", "folderId": "67555200", "name": "Melinda's Sales Sequence", "createdAt": "2023-01-26T15:26:56.574Z", "updatedAt": "2023-05-26T17:16:57.866Z", "userId": "2222222" } ], "paging": { "next": { "after": "NA%3D%3D", "link": "https://api.hubspot.com/automation/v4/sequences/?limit=4&userId=8698664&portalId=6331522&hs_static_app=api-goggles&hs_static_app_version=1.7830&after=NA%3D%3D" } } } ``` The details of each response field are outlined in the table below: | Field | Type | Description | | --------- | ------ | ----------------------------------------------------- | | total | String | The number of sequences in your account. | | id | String | The sequence ID. | | name | String | The name of the sequence. | | createdAt | String | The time the sequence was created in UTC format. | | updatedAt | String | The time the sequence was last updated in UTC format. | | userID | String | The userID of the user who created the sequence. | ## Enroll a contact in a sequence To enroll a contact in a sequence, make a `POST` request to `/automation/v4/sequences/enrollments/`. Use your [user ID](/guides/api/settings/users/user-details) as the query parameter. Specify the sequenceId, contactId, and senderEmail in the body. The senderEmail must be an email address that's [connected](https://knowledge.hubspot.com/connected-email/connect-your-inbox-to-hubspot) to your HubSpot account. For example, to enroll a contact whose ID is `33333` in sequence that’s ID is `44444444`, you’d make a `POST` request to `/automation/v4/sequences/enrollments`. The body would resemble the following: ```json // Example of request body for POST request { "sequenceId": "33333", "contactId": "44444444", "senderEmail": "menelson@hubspot.com" } ``` The response for enrolling a contact in a sequence would resemble the following: ```json // Example response for POST request to https://api.hubapi.com//automation/v4/sequences/enrollments/ { "id": "2435404604", "toEmail": "RachelGreen.com", "enrolledAt": "2024-06-27T20:11:02.824Z", "updatedAt": "2024-06-27T20:11:02.824Z" } ``` The details of each response field are outlined in the table below: | Field | Type | Description | | --- | --- | --- | | id | String | The ID for the enrollment object. | | toEmail | String | The email of the contact | | enrolledAt | String | The time the contact was enrolled in the sequence in UTC format. | | updatedAt | String | The last time the enrollment was updated (paused, unpaused, etc.) | There is a limit of 1000 enrollments per portal inbox per day. ## View a contact's sequence enrollment status A contact's enrollment status will indicate if the contact is enrolled in any sequences at the time of the request. To get a contact's enrollment status, make a `GET` request to /`automation/v4/sequences/enrollments/contact/{contactId}`. For example, to view the enrollment status of a contact whose contact ID is `33333`, make a `GET` request to `/automation/v4/sequences/enrollments/contact/33333`. Use the contact ID as the query parameter. The response for viewing a contact's sequence enrollment status would resemble the following: ```json // Example response for GET request to https://api.hubapi.com/automation/v4/enrollments/contact/{contactId} { "id": "2435404604", "toEmail": "RachelGreen@gmail.com", "enrolledAt": "2024-06-27T20:11:02.824Z", "updatedAt": "2024-06-27T20:11:02.824Z", "sequenceId": "76853632", "sequenceName": "Melinda's Sales Hub Sequence" "enrolledBy": "8698664", "enrolledByEmail": "menelson@hubspot.com" } ``` The details of each response field are outlined in the table below: | Field | Type | Description | | --- | --- | --- | | id | String | The ID for the enrollment object. | | toEmail | String | The email of the contact | | enrolledAt | String | The time the contact was enrolled in the sequence in UTC format. | | updatedAt | String | The last time the enrollment was updated (paused, unpaused, etc.) | | sequenceID | String | The ID of the sequence the contact is enrolled in. | | sequenceName | String | The title of the sequence the contact is enrolled in. | | enrolledBy | String | The userId of the user who enrolled the contact in the sequence. | | enrolledByEmail | String | The email of the user who enrolled the contact in the sequence or the email address that email messages are sent from. | # Automation API | Automation v4 API (BETA) @ Learn how to use the v4 automation API (BETA). $ guides & api/automation --- # Automation v4 API (BETA) You can use the automation v4 API to fetch data from existing [workflows](https://knowledge.hubspot.com/workflows/create-workflows), create new workflows, as well as delete workflows you no longer need. The sections below provide a walkthrough of how to use the v4 endpoints. For a full reference of the available endpoints and their required fields, check out the [endpoint reference documentation](/reference/api/automation/create-manage-workflows). This API is currently in beta and is subject to change based on testing and feedback. By using these endpoints you agree to adhere to HubSpot's [Developer Terms](https://legal.hubspot.com/hs-developer-terms)& [Developer Beta Terms](https://legal.hubspot.com/hubspot-beta-terms). You also acknowledge and understand the risk associated with testing an unstable API. ## Scope requirements To use any of the v4 endpoints, your app will need to be authorized to use the `automation` scope. In addition, if you need to access sensitive data properties in a workflow, enroll records in a workflow that references sensitive data, or retrieve enrollment and performance data for workflows with sensitive data properties, you'll need to authorize sensitive data scopes that are detailed in the sections below. Learn more about working with sensitive data in the [HubSpot Knowledge Base](https://knowledge.hubspot.com/properties/store-sensitive-data). ### Retrieving workflows that reference sensitive data To retrieve enrollment data and performance stats for existing workflows that reference sensitive data properties, your app will need to request and be granted one of the scopes below, based on the enrolled record type in your workflow: - `crm.objects.contacts.sensitive.read`: view enrollment and performance data for existing contact-based workflows. - `crm.objects.companies.sensitive.read`: view enrollment and performance data for existing company-based workflows. - `crm.objects.deals.sensitive.read`: view enrollment and performance data for existing deal-based workflows. - `crm.objects.custom.sensitive.read`: view enrollment and performance data for workflows based on custom objects an account. The scopes above are <u>not</u> sufficient to create or delete workflows using the v4 API, nor enroll records in existing workflows that access sensitive data properties. Consult the sensitive data write scopes in the section below if you need to perform these operations via the v4 API. ### Creating and managing workflows that reference sensitive data To create or delete workflows that reference sensitive data, or enroll records in an existing workflow, your app will need to request and be granted one of the scopes below, based on the enrolled record type of your workflow: - `crm.objects.contacts.sensitive.write`: create and delete contact-based workflows via the v4 API, as well as enroll contacts in existing workflows that access sensitive data. - `crm.objects.companies.sensitive.write`: create and delete company-based workflows via the v4 API, as well as enroll companies in existing workflows that access sensitive data. - `crm.objects.deals.sensitive.write`: create and delete deal-based workflows via the v4 API, as well as enroll deals in existing workflows that access sensitive data. - `crm.objects.custom.sensitive.write`: create and delete workflows based on custom objects via the v4 API, as well as enroll custom object records in existing workflows that access sensitive data. ## Fetch workflows The sections below detail how to fetch details about specific workflows, or getting details about multiple workflows in a single call. ### Fetch a specific workflow by ID To fetch data from an existing workflow, make a `GET` request to `/automation/v4/flows/{flowId}`, where `flowId` is the ID an existing workflow in your account. For example, if you made a `GET` request to `/automation/v4/flows/585051946` to get the workflow with an ID of `585051946`, the resulting response would be: ```json // Example workflow response from GET request to /automations/v4/flows/585051946 { "id": "585051946", "isEnabled": true, "flowType": "WORKFLOW", "revisionId": "7", "name": "New form submission workflow", "createdAt": "2024-06-07T17:27:08.101Z", "updatedAt": "2024-06-07T17:31:11.263Z", "startActionId": "1", "nextAvailableActionId": "3", "actions": [ { "type": "SINGLE_CONNECTION", "actionId": "1", "actionTypeVersion": 0, "actionTypeId": "0-13", "connection": { "edgeType": "STANDARD", "nextActionId": "2" }, "fields": { "operation": "ADD", "list_id": "178" } }, { "type": "SINGLE_CONNECTION", "actionId": "2", "actionTypeVersion": 0, "actionTypeId": "0-9", "fields": { "user_ids": ["2620022"], "delivery_method": "APP", "subject": "New form submission", "body": "Check out the new form submission we received!" } } ], "enrollmentCriteria": { "shouldReEnroll": false, "type": "EVENT_BASED", "eventFilterBranches": [ { "filterBranches": [], "filters": [], "eventTypeId": "4-1639801", "operator": "HAS_COMPLETED", "filterBranchType": "UNIFIED_EVENTS", "filterBranchOperator": "AND" } ], "listMembershipFilterBranches": [] }, "timeWindows": [], "blockedDates": [], "customProperties": {}, "crmObjectCreationStatus": "COMPLETE", "type": "CONTACT_FLOW", "objectTypeId": "0-1", "suppressionListIds": [], "canEnrollFromSalesforce": false } ``` The response above includes the specification for a contact-based workflow with two actions, and contacts are enrolled if they filled out a form on your website. ### Fetch workflows in bulk using their IDs To fetch multiple workflows using their IDs, you can make a `POST` request to the `/automation/v4/flows/batch/read` endpoint, and provide the ID of each workflow as the `flowId`, along with a `type` of `"FLOW_ID"` within an `inputs` array provided in the request body. For example, the request body below would fetch the workflow details for two workflows with IDs of `"619727002"` and `"617969780"`: ```json // Example request body for POST request to /automation/v4/flows/batch/read { "inputs": [ { "flowId": "619727002", "type": "FLOW_ID" }, { "flowId": "617969780", "type": "FLOW_ID" } ] } ``` ### Fetch all workflows To fetch all workflows, you can make a `GET` request to `/automation/v4/flows`. Keep in mind that the response will only include key fields for the workflow, such as `id`, `isEnabled`, `objectTypeId`, and the latest `revisionId`. If you want to get a full list of properties for a specific workflow, make a `GET` request to `/automation/v4/flows/{flowId}` and provide the specific ID of the workflow as the `flowId`. You can consult the sections below on how `actions` and enrollmentCriteria for a workflow are specified. Once you review these specifications, you can follow the steps in the [_Create a workflow_](#create-a-workflow) section to define and create a workflow using the v4 API. ## Action types Each workflow specification consists of a list of actions. Each action contains a set of required properties: - `actionId`: a unique ID that identifies this action, provided as a string. - `actionTypeId`: a predefined value that specifies the action type (e.g., `"0-1"` designates a `DELAY` action). A full list of actions and their associated `actionTypeId` is provided in the section below. - `actionTypeVersion`: a number that designates the version of the action. For all built-in action types (e.g., a delay, email notification, etc.), the `actionTypeVersion` will be `0`. - `connection`: for non-branching actions, this property is an object that specifies the subsequent action, and contains two nested properties: - `edgeType`: to proceed directly to the next action, this property should be set to `"STANDARD"`. If you have a branch in your workflow, you can go to an action in a different branch by setting the `edgeType` to `"GO-TO"`. - `nextActionId`: the ID of the subsequent action in the workflow. - `type`: for non-branching actions, this should be set to `SINGLE_CONNECTION`. - `fields`: an object that specifies the data required by the action (e.g., how long to delay an enrolled object for a `DELAY` action). The structure of this property depends on the action type. Consult the sections and example actions below for the fields each action type expects. ### Action type IDs The table below details the available action types along with the associated `actionTypeId`: | Action | Action type ID | Description | | --- | --- | --- | | Delay until a specific date or date-based property | `0-35` | Delay until a preconfigured calendar date or date property of the enrolled record. | | Delay a set amount of time, until a specific day of the week, or time of day. | `0-1` | Delay for a preconfigured amount of time (e.g., 3 hours, 5 days, etc.), until a specific day (e.g., Tuesday), or time of day (12:00 AM EST). | | Send automated email to a contact associated with the enrolled record | `0-4` | Send an automated marketing email to the enrolled record. | | Send email notification to a user or team | `0-8` | Send an internal email notification to a user or team in your account. | | Send an in-app notification to a user or team | `0-9` | Trigger an in-app notification to a user or team in your account. | | Set property | `0-5` | Set a property on an enrolled object. | | Create task | `0-3` | Create a new task. | | Create a new record | `0-14` | Create a new record (contact, company, deal, ticket, or lead). | | Add enrolled object to list | `0-63809083` | Add an enrolled contact to a static list. | | Remove enrolled object from list | `0-63863438` | Remove an enrolled contact from a static list. | ### Action fields The `fields` property of your action depends on the corresponding action type. - The actions available depend on your account subscription. Learn about all available actions in [this knowledge base article](https://knowledge.hubspot.com/workflows/choose-your-workflow-actions). - To confirm the fields required for a specific action, you can [create the workflow](https://knowledge.hubspot.com/workflows/create-workflows) with that action using the workflows tool, then make a `GET` request to `/automations/v4/flows/{flowId}` for that workflow. You can find the `flowId` of the workflow by editing the workflow then checking the URL for the second ID in the URL path, or you can make a `GET` request to `/automations/v4/flows` to get all workflows and filter the resulting response by the name of your workflow. The sections below outline the required fields for common actions in a workflow. ### Delay until a specific date or date-based property The code below specifies an example delay action until a specific date. Based on whether you're delaying until a preconfigured date or a date-based property of the enrolled record, you'll need to specify the sub-properties of the `date` field accordingly: - If you're delaying until a specific calendar date, provide `STATIC_VALUE` for the `type` sub-property, then provide the calendar date for the `staticValue` sub-property as a unix timestamp. - To delay until a date-based property of the enrolled record, provide `OBJECT_PROPERTY` for the `type` sub-property, then specify the property to use as the `propertyName`. ```json // Example of a delay action to delay until a specific date or date-based property { "type": "SINGLE_CONNECTION", "actionId": "5", "actionTypeVersion": 0, "actionTypeId": "0-35", "connection": { "edgeType": "STANDARD", "nextActionId": "7" }, "fields": { "date": { "type": "STATIC_VALUE", "staticValue": "1719446400000" }, "delta": "0", "time_unit": "DAYS", "time_of_day": { "hour": 12, "minute": 0 } } } ``` ### Delay a set amount of time The code below specifies an example delay action for a preconfigured amount of time. ```json // Example of a delay action to delay for a specific amount of time (e.g., 6 hours) { "type": "SINGLE_CONNECTION", "actionId": "5", "actionTypeVersion": 0, "actionTypeId": "0-35", "connection": { "edgeType": "STANDARD", "nextActionId": "7" }, "fields": { "delta": "720", "time_unit": "MINUTES" } } ``` ### Send an automated marketing email to an enrolled contact The following example action demonstrates how to send an automated marketing email with an ID of `113782603056` to an enrolled contact. ```json // Example action to send an automated marketing email to an enrolled contact { "type": "SINGLE_CONNECTION", "actionId": "4", "actionTypeVersion": 0, "actionTypeId": "0-4", "fields": { "content_id": "113782603056" } } ``` ### Set property The action definition below provides an example of setting the `hs_lead_status` property to `"IN_PROGRESS"`. ```json // Example action to set the hs_lead_status property to IN_PROGRESS { "actionId": "2", "actionTypeVersion": 0, "actionTypeId": "0-5", "connection": { "edgeType": "STANDARD", "nextActionId": "4" }, "fields": { "property_name": "hs_lead_status", "association": { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 1 }, "value": { "staticValue": "IN_PROGRESS" } } } ``` ### Create task The action definition below is an example of creating an unassigned task: ```json // Example action definition for creating a new unassigned task { "type": "SINGLE_CONNECTION", "actionId": "1", "actionTypeVersion": 0, "actionTypeId": "0-3", "fields": { "task_type": "TODO", "subject": "Check in with lead", "body": "<p>Remember to sync up with new lead!</p>", "associations": [ { "target": { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 10 }, "value": { "type": "ENROLLED_OBJECT" } } ], "use_explicit_associations": "true", "priority": "NONE" } } ``` ### Create a new record The example action below creates a new deal and associates it with the contact enrolled in the workflow: ```json // Example action definition for creating a new deal and associating it with the enrolled contact { "type": "SINGLE_CONNECTION", "actionId": "2", "actionTypeVersion": 0, "actionTypeId": "0-14", "connection": { "edgeType": "STANDARD", "nextActionId": "3" }, "fields": { "object_type_id": "0-3", "properties": [ { "targetProperty": "dealstage", "value": { "type": "STATIC_VALUE", "staticValue": "appointmentscheduled" } }, { "targetProperty": "dealname", "value": { "type": "STATIC_VALUE", "staticValue": "New deal" } }, { "targetProperty": "amount", "value": { "type": "STATIC_VALUE", "staticValue": "1000" } } ], "associations": [ { "target": { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 3 }, "value": { "type": "ENROLLED_OBJECT" } } ], "use_explicit_associations": "true" } } ``` ### Add an enrolled object to a static list The following action definition is an example of adding an enrolled contact to a static list, specified as the `listId`. ```json // Example action to add an enrolled contact to a list { "actionId": "3", "actionTypeVersion": 0, "actionTypeId": "0-63809083", "fields": { "listId": "123" }, "type": "SINGLE_CONNECTION" } ``` ### Remove an enrolled object from a static list The following action definition is an example of removing an enrolled contact from a static list, specified as the `listId`. ```json // Example action to remove an enrolled contact from a list { "actionId": "3", "actionTypeVersion": 0, "actionTypeId": "0-63863438", "fields": { "listId": "123" }, "type": "SINGLE_CONNECTION" } ``` ### Branching actions Branching actions differ from other actions in that they don't follow the standard action structure. Branching action definitions don't have `fields` or `connection` properties. There are two types of branching actions: list branch actions and static branch actions. Both types must also define a default branch using the `defaultBranchName` and `defaultBranch` properties. #### List branch actions List branch actions include a `listBranches` property that specifies a set of filter branches to segment enrolled objects. Each `filterBranch` is configured using the syntax and formatting outlined in the [list filters documentation](/guides/api/crm/lists/lists-filters). ```json // Example list branch action { "actionId": "6", "listBranches": [ { "filterBranch": {}, "connection": { "edgeType": "STANDARD", "nextActionId": "7" } }, { "filterBranch": {}, "branchName": "Some branch name", "connection": { "edgeType": "GOTO", "nextActionId": "4" } } ], "defaultBranchName": "Fall-through branch", "defaultBranch": { "edgeType": "STANDARD", "nextActionId": "8" } } ``` #### Static branch actions Static branch actions include an `inputValue` definition, which supports different shapes for the input values of the branch. It also includes a list of `staticBranches`, which defines which actions come next in the branch. ```json // Example static branch action { "actionId": "1", "inputValue": { "propertyName": "example_property" }, "staticBranches": [ { "branchValue": "example_value_1", "connection": { "edgeType": "STANDARD", "nextActionId": "2" } }, { "branchValue": "example_value_1", "connection": { "edgeType": "STANDARD", "nextActionId": "3" } } // ... ], "defaultBranchName": "Fall-through branch", "defaultBranch": { "edgeType": "STANDARD", "nextActionId": "4" } } ``` ## Enrollment criteria You can configure the conditions for objects to be enrolled in your workflow within the `enrollmentCriteria` property of your workflow specification. - The data you specify varies based on whether your enrollment is event-based or list-based. You can specify the enrollment type by setting the `type` of `enrollmentCriteria` to either `EVENT_BASED` or `LIST_BASED`. - You can specify the re-enrollment settings for objects enrolled in your workflow by setting the `shouldReEnroll` field to `true` or `false`. Learn more about workflow enrollment in this [knowledge base article](https://knowledge.hubspot.com/workflows/set-your-workflow-enrollment-triggers). ### Event-based enrollment Event-based workflows will enroll objects when specific events occur, such as when a form is submitted. - You can configure the criteria for which events will trigger enrollment by defining a list of `eventFilterBranches`. Each `eventFilterBranch` definition specifies a qualifying event (e.g., a form submission) using an `eventTypeId` that corresponds to that event. - Most events will have the same values for the following fields: - `operator`: `"HAS_COMPLETED"` - `filterBranchType`: `"UNIFIED_EVENTS"` - `filterBranchOperator`: `"AND"` The table below defines each `eventTypeId` that corresponds to the event you can configure for your workflow: | Event | Event type ID | | ---------------------------- | ------------- | | Ad interaction | `4-1553675` | | Email open | `4-666440` | | Email reply | `4-665538` | | Email click | `4-666288` | | Email delivery | `4-665536` | | Form submission | `4-1639801` | | Form view | `4-1639797` | | Form interaction | `4-1639799` | | Marketing event registration | `4-68559` | | Marketing event cancellation | `4-69072` | | Call start | `4-1733817` | | Call end | `4-1741072` | | SMS shortlink click | `4-1722276` | | CTA view | `4-1555804` | | CTA click | `4-1555805` | | Media play on webpage | `4-675783` | The example code below defines the `enrollmentCriteria` to enroll contacts who successfully submitted a form: ```json // Example of an enrollmentCriteria definition to enroll contacts who submitted a form "enrollmentCriteria": { "shouldReEnroll": true, "type": "EVENT_BASED", "eventFilterBranches": [ { "filterBranches": [], "filters": [], "eventTypeId": "4-1639801", "operator": "HAS_COMPLETED", "filterBranchType": "UNIFIED_EVENTS", "filterBranchOperator": "AND" } ], "listMembershipFilterBranches": [] } ``` ### Filter-based enrollment Workflows with filter-based enrollment will enroll objects when the criteria you configure is met. - Criteria is configured setting the `listFilterBranch` field of your `enrollmentCriteria` based which objects should qualify for enrollment in your workflow. Within a listFilterBranch, you can define the specific filter criteria using a list of `filterBranches`. - You can learn more about the syntax and formatting for defining a `listFilterBranch` in the [list filters documentation](/guides/api/crm/lists/lists-filters). For example, the `enrollmentCriteria` below defines the criteria for when a contact's _City_ property is equal to _Dublin_: ```json // Example of an enrollmentCriteria definition to filter contacts based on whether their city property is equal to 'Dublin' "enrollmentCriteria": { "shouldReEnroll": false, "type": "LIST_BASED", "listFilterBranch": { "filterBranches": [ { "filterBranches": [], "filters": [ { "property": "city", "operation": { "operator": "IS_EQUAL_TO", "includeObjectsWithNoValueSet": false, "values": [ "Dublin" ], "operationType": "MULTISTRING" }, "filterType": "PROPERTY" } ], "filterBranchType": "AND", "filterBranchOperator": "AND" } ], "filters": [], "filterBranchType": "OR", "filterBranchOperator": "OR" }, "unEnrollObjectsNotMeetingCriteria": false, "reEnrollmentTriggersFilterBranches": [] } ``` ## Create a workflow To create a workflow, make a `POST` request to `/automations/v4/flows` and provide a workflow specification in the body of your request. - Consult the _Action types_ and _Enrollment criteria_ sections above for a full reference on specifying the `actions` in your workflow and configuring the `enrollmentCriteria`. - For contact-based workflows, set the `type` to `"CONTACT_FLOW"`. For all other workflow types (e.g., deal-based, goal-based, etc.), set the `type` to `"PLATFORM_FLOW"`. For example, if you wanted to create a workflow that executed the following: - Once a contact submits a specific form on your website, a ticket will be created in your account. - After a 1 day delay, the enrolled contact will be sent an [automated marketing email](https://knowledge.hubspot.com/marketing-email/create-automated-emails-to-use-in-workflows). The request body of your `POST` request would resemble the following: ```json // Example request body for POST request to create a workflow { "isEnabled": true, "flowType": "WORKFLOW", "name": "New form submission from interested contact", "startActionId": "1", "nextAvailableActionId": "4", "actions": [ { "type": "SINGLE_CONNECTION", "actionId": "1", "actionTypeVersion": 0, "actionTypeId": "0-14", "connection": { "edgeType": "STANDARD", "nextActionId": "3" }, "fields": { "object_type_id": "0-5", "properties": [ { "targetProperty": "subject", "value": { "type": "STATIC_VALUE", "staticValue": "Review new form submission" } }, { "targetProperty": "hs_pipeline_stage", "value": { "type": "STATIC_VALUE", "staticValue": "1" } }, { "targetProperty": "source_type", "value": { "type": "STATIC_VALUE", "staticValue": "FORM" } }, { "targetProperty": "content", "value": { "type": "STATIC_VALUE", "staticValue": "[Triage required] new form submitted. Next available rep should review." } } ], "associations": [ { "target": { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 16 }, "value": { "type": "ENROLLED_OBJECT" } }, { "target": { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 339 }, "value": { "type": "COPY_ASSOCIATION", "sourceSpec": { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 279 } } } ], "use_explicit_associations": "true" } }, { "type": "SINGLE_CONNECTION", "actionId": "2", "actionTypeVersion": 0, "actionTypeId": "0-4", "fields": { "content_id": "113782603056" } }, { "type": "SINGLE_CONNECTION", "actionId": "3", "actionTypeVersion": 0, "actionTypeId": "0-1", "connection": { "edgeType": "STANDARD", "nextActionId": "2" }, "fields": { "delta": "1440", "time_unit": "MINUTES" } } ], "enrollmentCriteria": { "shouldReEnroll": false, "type": "EVENT_BASED", "eventFilterBranches": [ { "filterBranches": [], "filters": [ { "property": "hs_form_id", "operation": { "operator": "IS_ANY_OF", "includeObjectsWithNoValueSet": false, "values": ["2f5cc7f8-d359-4e9c-a770-dd42ea07d217"], "operationType": "ENUMERATION" }, "filterType": "PROPERTY" } ], "eventTypeId": "4-1639801", "operator": "HAS_COMPLETED", "filterBranchType": "UNIFIED_EVENTS", "filterBranchOperator": "AND" } ], "listMembershipFilterBranches": [] }, "timeWindows": [], "blockedDates": [], "customProperties": {}, "crmObjectCreationStatus": "COMPLETE", "type": "CONTACT_FLOW", "objectTypeId": "0-1", "suppressionListIds": [], "canEnrollFromSalesforce": false } ``` ## Update a workflow To update an existing workflow, make a PUT request to `/automation/v4/{flowId}` using the ID of your workflow you want to update as the flowId, and providing any properties you want to change in the request body. - You must include the `revisionId` and `type` properties in your request body. - The `revisionId` must correspond to the most current revision of your workflow, which you can retrieve by making a `GET` request to `/automation/v4/{flowId}`. For the example, to enable a workflow that's currently turned off, the request body below would enable the workflow, using its most recent revisionId of `"5"`: ```json // Example request body for PUT request to update a workflow { "isEnabled": true, "revisionId": "5", "type": "CONTACT_FLOW" } ``` ## Delete a workflow To delete a workflow, make a `DELETE` request to `/automations/v4/{flowId}` using the ID of the workflow you want to delete as the `flowId`. Once deleted, you cannot restore the workflow via the automation API. You must [contact HubSpot Support](https://knowledge.hubspot.com/help-and-resources/get-help-with-hubspot) to get assistance with restoring your workflow. # CMS API | Blog Authors @ The blog authors endpoints are used to create and manage hubspot blog authors $ guides & api/cms/blogs --- Use the blog authors API to manage author information for your blog posts. Learn more about how to create and maintain your blog on the [HubSpot Knowledge Base.](https://knowledge.hubspot.com/website/topics#blog) ## Changes in V3 The following properties are deprecated and will not be included in the response of any of the V3 endpoints: - `user_id` - `username` - `googlePlus` - `gravatarUrl` - `twitterUsername` - `hasSocialProfiles` ## Search blog authors To retrieve an account's blog authors, make a `GET` request to `/cms/v3/blogs/authors`. You can filter and sort the authors returned in the response using the operators and properties described below. You can also use the standard filters using the _createdAt_ and _updatedAt_ dates. ### Filtering Provide any filters as query parameters in your request, by adding the **property name**, followed by **two underscore characters**, then include the associated **operator** as a suffix. For example, you can filter the results to only include authors where the `displayName` property contains the word _J\_\_ohn_ using the parameter: `&displayName__icontains=john`. You can include any number of filters as query parameters in the request URL. All filters are ANDed together. ORing filters is not currently supported. The available filter types are listed below: | Operator | Description | | ------------ | ------------------------- | | `eq` | Equal to | | `ne` | Not equal to | | `contains` | Contains | | `lt` | Less than | | `lte` | Less than or equal to | | `gt` | Greater than | | `gte` | Greater than or equal to | | `is_null` | Null | | `not_null` | Not null | | `like` | Is like | | `not_like` | Not like | | `icontains` | Contains (case sensitive) | | `startswith` | Starts with | | `in` | In | The table below lists the properties that can be filtered on, along with their supported filter types. | Operator | Description | | ------------------ | ------------------------------ | | `id` | `eq`, `in`, `not_in` | | `fullName` | `eq`, `in`, `contains` | | `email` | `eq`, `ne`, `not_null` | | `slug` | `eq` | | `createdAt` | `eq`, `gt`, `gte`, `lt`, `lte` | | `updatedAt` | `eq`, `gt`, `gte`, `lt`, `lte` | | `name` | `eq`, `in`, `contains` | | `deletedAt` | `eq`, `gt`, `gte`, `lt`, `lte` | | `createdById` | `eq` | | `updatedById` | `eq` | | `language` | `in`, `not_null` | | `translatedFromId` | `null`, `not_null` | To filter blog authors based on a multi-language group, you can include one of the query parameters in the table below. For example, to get blog authors associated with the German variation of your blog, you'd include `language_in=de` as a query parameter. | Parameter | Description | | --- | --- | | `translatedFromId__is_null` | Primary blog author in a multi-language group. | | `translatedFromId__not_null` | Variation blog author in a multi-language group. | | `language__in` | Blog author with a specific language. | ### Sorting and paginating You can provide sorting and pagination options as query parameters. Specify the **property name** as the value to the sort query parameter to return the blog authors in the natural order of that property. You can reverse the sorting order by including a dash character before the property name (e.g., `sort=-createdAt`). By combining query parameters for filtering, sorting, and paging, you can retrieve blog authors that match more advanced search criteria. For example, the request below fetches blog authors that don't have a language assigned, ordered by the most recently updated. The limit and offset parameters below return the second page of results. ```shell curl https://api.hubapi.com/cms/v3/blogs/authors?sort=-updatedAt&&language__not_null&limit=10&offset=10 \ --request POST \ --header "Content-Type: application/json" ``` ## Create blog authors To create a blog author, make a `POST` request to `/cms/v3/blogs/authors`, and include a JSON payload that represents the blog author model. The `fullName` field is required when creating a blog author. To set the URL of a blog author profile page, you must provide the `slug` field in your request. Review the required parameters and the structure of the blog author model in the [reference documentation](https://developers.hubspot.com/docs/reference/api/cms/blogs/blog-authors#post-%2Fcms%2Fv3%2Fblogs%2Fauthors). You can also [create blog authors directly in your HubSpot account](https://knowledge.hubspot.com/blog/create-and-manage-your-blog-authors#create-a-new-blog-author). ## Edit blog authors To update a blog author, make a `PATCH` request to `/cms/v3/blogs/authors/{objectId}`, where `objectId` is the ID of the associated author. Include a JSON payload representing the blog author model in your request. Review the required parameters and the structure of the blog author model in the [reference documentation](https://developers.hubspot.com/docs/reference/api/cms/blogs/blog-authors#patch-%2Fcms%2Fv3%2Fblogs%2Fauthors%2F%7Bobjectid%7D). You can also [edit a blog author's profile directly in your HubSpot account](https://knowledge.hubspot.com/blog/create-and-manage-your-blog-authors#edit-a-blog-author-s-profile). ## Multi-language management To help you maintain blog authors across multiple languages, HubSpot's CMS allows you to group together blog authors of language variants of the same content. A blog author with a language set may only be used on blog posts of the same language. Blog authors that do not have a language set are considered global and may be used on all blog posts. To learn more about working with multi-language blog authors, check out [this Knowledge Base article](https://knowledge.hubspot.com/blog/create-a-multi-language-blog#create-blog-authors-in-multiple-languages). ### Create a new language variant To create a new language variant for an existing blog author, make a `POST` request to `/multi-language/create-language-variant` and include a JSON payload containing the ID of the blog author to clone and the language identifier of the new variant. ### Attach a blog author to an existing multi-language group To add a blog author to an existing multi-language group, make a `POST` request to `/multi-language/attach-to-lang-group` and include a JSON payload containing the ID of the target blog author, the language identifier of the blog author being added, and the `primaryId` of the blog author designated as the primary author in the target multi-language group. ### Detach a blog author from a multi-language group To remove a blog author from a multi-language group, make a `POST` request to `/multi-language/detach-from-lang-group` endpoint, and include a JSON payload that contains the ID of the target blog author. # CMS API | Blog Posts @ The blog post endpoints are used to create and manage hubspot blog posts $ guides & api/cms/blogs --- # Blog Posts You can use the blog post API to publish and manage blog posts. Learn more about how to create and maintain your blog on the [HubSpot Knowledge Base.](https://knowledge.hubspot.com/web-content/topics#blog) ## Changes in V3 - The following properties are deprecated and will not be included in the response of any of the V3 endpoints: - `campaign_name` - `is_draft` - `keywords` - The `topicIds` property has been renamed to `tagIds`. ## Retrieve blog posts You can retrieve blog posts either individually by ID or by retrieving all blog posts: - To retrieve an individual blog post, make a `GET` request to `/cms/v3/blogs/posts/<postId>`. - To retrieve all blog posts, make a `GET` request to `/cms/v3/blogs/posts`. When retrieving all blog posts, you can [filter](#filtering) and [sort](#sorting-and-paginating) the returned posts using the operators and properties below. You can also use the standard filters using the `createdAt` and `updatedAt` dates. Learn more about the available filtering, sorting, and pagination options below. ### Filtering Provide any filters as query parameters in your request, by adding the `property name`, followed by two underscore characters, then include the associated `operator` as a suffix. For example, you can filter the results to only include blog posts where the `name` property contains the word `marketing` using the parameter: `&name__contains=marketing`. You can include any number of the following filters as query parameters in the request URL. All specified filters will be applied to narrow down results. | Filter | Operator | | --------------------------- | -------------- | | Equals | `eq (or none)` | | Not equal | `ne` | | Contains | `contains` | | Less than | `lt` | | Less than or equal | `lte` | | Greater than | `gt` | | Greater than or equal | `gte` | | Null | `is_null` | | Not null | `not_null` | | Like | `like` | | Not like | `not_like` | | Contains (case insensitive) | `icontains` | | Starts with | `startswith` | | In | `in` | | Not in | `nin` | The table below lists the properties that can be filtered on, along with their supported filter types. | Property | Supported filters | | ------------------ | ------------------------------------------ | | `id` | `eq`, `in` | | `slug` | `eq`, `in`, `nin`, `icontains` | | `campaign` | `eq`, `in` | | `state` | `eq`, `Not equal`, `in`, `nin`, `contains` | | `publishDate` | `eq`, `gt`, `gte` , `lt` ,`lte` | | `createdAt` | `eq`, `gt`, `gte` , `lt` ,`lte` | | `updatedAt` | `eq`, `gt`, `gte` , `lt` ,`lte` | | `name` | `eq`, `in`, `icontains` | | `archivedAt` | `eq`, `gt`, `gte`, `lt` ,`lte` | | `createdById` | `eq` | | `updatedById` | `eq` | | `blogAuthorId` | `eq`, `in` | | `translatedFromId` | `is_null`, `not_null` | | `contentGroupId` | `eq`, `in` | | `tagId` | `eq`, `in` | The table below lists the query parameters you can use to filter by publish state. | Publish State | Query parameters | | ------------- | ----------------- | | Draft | `state=DRAFT` | | Scheduled | `state=SCHEDULED` | | Published | `state=PUBLISHED` | The `currentState` field on the blog post object is a generated field which also reflects the blog's publish state, but you cannot use it as a property to filter against in your requests. To filter blog posts based on a multi-language group, you can include one of the query parameters in the table below. For example, to get blog posts associated with a German variation of your blog, you'd include `contentGroupId__eq=<germanBlogId>` as a query parameter. | Description | Query parameters | | --------------------------------------------- | ---------------------------- | | Primary blog post in a multi-language group | `translatedFromId__is_null` | | Variation blog post in a multi-language group | `translatedFromId__not_null` | | Blog post with specific language | `contentGroupId__eq` | ### Sorting and paginating You can provide sorting and pagination options as query parameters. Specify the property name as the value to the `sort` query parameter to return the blog posts in the natural order of that property. You can reverse the sorting order by including a dash character before the property name (e.g., `sort=-createdAt`). By combining query parameters for filtering, sorting, and paging, you can retrieve blog posts that match more advanced search criteria. For example, the request below fetches blog posts that don't have a language assigned, ordered by the most recently updated. Including the `limit` and `offset` parameters below returns the second page of results. ```shell curl https://api.hubapi.com/cms/v3/blogs/posts?sort=-updatedAt&&language__not_null&limit=10&offset=10 \ --request POST \ --header "Content-Type: application/json" ``` ## Editing and publishing blog posts Blog posts in HubSpot have both draft and live versions. - Drafted blog posts appear in HubSpot's editor, but are not live on the website. They can be reviewed and edited by users in HubSpot or via the API, and can be published when needed. After a blog post is published, the draft version can be updated as needed, then later published to update the live content. - Live blog posts are blog posts that appear on the website. The draft version can be updated without affecting the live blog post content. Published posts can be unpublished to remove them from the website and return them to a draft version. ### Create a blog post To create a blog post, make a `POST` request to the `/cms/v3/blogs/posts`, specifying the blog post's details in the request body. By default, the post will be created as an unpublished draft. If needed, a blog post can be published at the time of creation as long as the [properties necessary for publishing](#publishing-blog-posts) are set. ```json { "name": "Example blog post", "contentGroupId": "184993428780", "slug": "slug-at-the-end-of-the-url", "blogAuthorId": "4183274253", "metaDescription": "My meta description.", "useFeaturedImage": false, "postBody": "<p>Welcome to my blog post! Neat, huh?</p>" } ``` | Parameter | Type | Description | | --- | --- | --- | | `name` | String | Description | | `contentGroupId` | String | The ID of the parent blog to publish the post to. You can retrieve the ID using the [blog details API](/reference/api/cms/blogs/blog-details). | | `slug` | String | The URL slug of the blog post. HubSpot will automatically generate the full URL using this value, along with the parent blog domain and slug. If no value is provided, the `name` value will be used as a temporary slug (hyphenated). You will need to set a specific slug before the post can be published. | | `blogAuthorId` | String | The ID of the blog author. You can retrieve this value using the [blog authors API](/reference/api/cms/blogs/blog-authors). | | `metaDescription` | String | The post's meta description. | | `useFeaturedImage` | Boolean | Whether to include a featured image for the blog post. By default, this field is set to `true`. To include a featured image, include a value for the `featuredImage` parameter. | | `postBody` | String | The HTML content of the blog post. | Editing blog post content directly in HubSpot using the content editor is the simplest way to modify content. While you can use the the API to create and update blog post, it's not recommended over using the editor, especially for blogs that rely on more complex modules. Learn more about the structure of the blog post model in the [blog posts endpoint reference](/reference/api/cms/blogs/blog-posts). ### Update a blog post You can update the draft version of a blog post by making a `PATCH` request to `/cms/v3/blogs/posts/<postId>/draft`. You must include a JSON payload that represents the blog post model. Properties you provide in the request payloud will override existing draft properties without any complex merging logic. As a result, if you're updating nested properties, you should provide the full definition of the object. Partial updates are not supported. For example, the following request body would update the title and URL slug of a blog post: ```json { "name": "Example blog post", "slug": "my-updated-post" } ``` Because this request would only update the draft, you would need to make a second request to [publish the changes](#publishing-blog-posts) to the website. To instead update a blog post and immediately publish those changes (updating both the draft and live versions of the post), make a `PATCH` request to `/cms/v3/blogs/posts/<postId>`, along with a request body containing the content you want to update. ### Publishing blog posts Depending on the state of the blog post, there are a few endpoints you can use to publish it. - If the blog post is currently a draft, not yet published, make a `PATCH` request to the `/cms/v3/blogs/posts/<postId>` endpoint. In the request body, include a JSON payload that sets the `state` to `PUBLISHED`. The post must have the following properties set in order to be published: | Property | Type | Description | | --- | --- | --- | | `name` | String | The title of the blog post. | | `contentGroupId` | String | The ID of the parent blog to publish the post to. You can retrieve the ID using the [blog details API](/reference/api/cms/blogs/blog-details). | | `slug` | String | The URL slug of the blog post. If no slug was specified at the time of creation, HubSpot will have assigned a temporary slug to the post. This temporary slug must be updated to a specific value in order for the post to be published. | | `blogAuthorId` | String | The ID of the blog author. You can retrieve this value using the [blog authors API](/reference/api/cms/blogs/blog-authors). | | `metaDescription` | String | The post's meta description. | | `featuredImage` | String | An image to use as the post's featured image. Alternatively, you can opt to not include a featured image by omitting this field and instead setting `useFeaturedImage` to `false`. | | `state` | String | The publish state of the post. Must be set to `PUBLISHED`. | - If the blog post is currently published, you can publish any content that's currently drafted by making a `POST` request to `/cms/v3/blogs/posts/<postId>/draft/push-live`. This endpoint does not require a request body. #### Schedule a draft to be published at a future time As an alternative to immediate publishing, you can schedule the draft version of your blog post to be published later by making a `POST` request to `/cms/v3/blogs/posts/schedule`. In the request body, include a JSON payload that contains the `id` of the target blog post and a `publishDate` (ISO8601 format). ### Reset a draft To reset the draft version of a blog post back to its current live version, make a `POST` request to `/cms/v3/blogs/posts/<postId>/draft/reset`. This endpoint does not require a request body. ## Multi-language management To help you maintain blog posts across multiple languages, HubSpot's CMS allows you to group together language variants of the same content. You can learn more about working with multi-language blog posts in [on HubSpot's Knowledge Base](https://knowledge.hubspot.com/blog/create-a-multi-language-blog#create-a-blog-post-in-multiple-languages). ### Create a new language variant You can create a new language variant for an existing blog post by making a `POST` request to the `/multi-language/create-language-variant` endpoint. The endpoint accepts a JSON payload containing the `id` of the blog post to clone and the `language` identifier of the new variant. ### Attach a blog post to an existing multi-language group You can add a blog post to an existing multi-language group by making a `POST` request to the `/multi-language/attach-to-lang-group` endpoint. The endpoint accepts a JSON payload containing the `id` of the target blog post, the `language` identifier of the blog post being added, and the `primaryId` of the blog post designated as the primary blog post in the target multi-language group. ### Detach a blog post from a multi-language group To detach a blog post from a multi-language group, make a `POST` request to the `/multi-language/detach-from-lang-group` endpoint. The endpoint accepts a JSON payload containing the `id` of the target blog post. # CMS API | Blog Tags @ Perform create, read, update, and delete actions on CMS Hub blog tags using the blog tags API. $ guides & api/cms/blogs --- # Blog Tags Use the blog tags API to manage tags for your blog posts. Learn more about how to create and maintain your blog on the [HubSpot Knowledge Base.](https://knowledge.hubspot.com/web-content/topics#blog) ## Changes in V3 The `description` property has been deprecated and will not be included in the response of any of the V3 endpoints. ## Search blog tags To retrieve blog tags, make a `GET` request to `/cms/v3/blogs/tags`. You can filter and sort the tags returned in the response using the operators and properties below. You can also use the standard filters using the `createdAt` and `updatedAt` dates. ### Filtering Provide any filters as query parameters in your request by adding the **property name**, followed by **two underscore characters**, then include the associated **operator** as a suffix. For example, you can filter the results to only include blog tags where the `name` property contains the word _marketing_ using the parameter: `&name__icontains=marketing`. You can include any number of filters as query parameters in the request URL. All filters are ANDed together. ORing filters is not currently supported. The available filter types are listed below: | Operator | Description | | ------------ | ------------------------- | | `eq` | Equal to | | `ne` | Not equal to | | `contains` | Contains | | `icontains` | Contains (case sensitive) | | `lt` | Less than | | `lte` | Less than or equal to | | `gt` | Greater than | | `gte` | Greater than or equal to | | `is_null` | Null | | `not_null` | Not null | | `like` | Like | | `not_like` | Not like | | `startswith` | Value starts with | | `in` | In | The table below lists the properties that can be filtered on, along with their supported filter types. | Property | Supported filters | | ------------------ | -------------------- | | `id` | eq, in | | `name` | eq, contains | | `slug` | eq | | `createdAt` | eq, gt, gte, lt, lte | | `deletedAt` | eq, gt, gte, lt, lte | | `createdById` | eq | | `updatedById` | eq | | `language` | in, not_null | | `translatedFromId` | null, not_null | To filter blog tags based on a multi-language group, you can include one of the query parameters from the table below. For example, to get blog tags from the German variation of your blog, you'd include `language__in=de` as a query parameter. Languages with locales (e.g., `en-us`) are not supported with the `language` filter. ### Sorting and paginating You can provide sorting and pagination options as query parameters. Specify the **property name** as the value to the sort query parameter to return the blog tags in the natural order of that property. You can reverse the sorting order by including a dash character before the property name (e.g., `sort=-createdAt`). By combining query parameters for filtering, sorting, and paging, you can retrieve blog tags that match more advanced search criteria. For example, the request below fetches blog tags that have a language assigned, ordered by the most recently updated. Including the `limit` and `offset` parameters below returns the second page of results. ```shell curl https://api.hubapi.com/cms/v3/blogs/tags?sort=-updatedAt&&language__not_null&limit=10&offset=10 \ --request POST \ --header "Content-Type: application/json" ``` ## Create blog tags To create a blog tag, make a `POST` request to `/cms/v3/blog/posts` and include a JSON payload that represents the blog tag model, as shown in the [reference documentation](https://developers.hubspot.com/docs/reference/api/cms/blogs/blog-tags#post-%2Fcms%2Fv3%2Fblogs%2Ftags). The `name` field is required when creating a blog tag. To set the URL of a blog tag listing page, you must include the `slug` field in your request. ## Edit blog tags To update a blog tag, make a `PATCH` request to `/cms/v3/blog/posts/{objectId}` where `objectId` is the ID of the tag you want to update. In your request, include a JSON payload should include the blog tag model, as shown in the [reference documentation](https://developers.hubspot.com/docs/reference/api/cms/blogs/blog-tags#patch-%2Fcms%2Fv3%2Fblogs%2Ftags%2F%7Bobjectid%7D). ## Multi-language management To help you maintain blog tags across multiple languages, HubSpot's CMS allows you to group together blog tags of language variants of the same content. A tag with a language set may only be used on blog posts of the same language. Tags that do not have a language set are considered global and may be used on all blog posts. To learn more about working with multi-language blog tags, check out [this Knowledge Base article](https://knowledge.hubspot.com/blog/create-a-multi-language-blog#create-blog-authors-in-multiple-languages). ### Create a new language variant To create a new language variant for an existing blog tag, make a `POST` request to `/multi-language/create-language-variant` and include a JSON payload containing the ID of the blog tag to clone and the language identifier of the new variant. ### Attach a blog tag to an existing mutli-language group To add a blog tag to an existing multi-language group, make a `POST` request to the `/multi-language/attach-to-lang-group` and include a JSON payload containing the ID of the target blog tag, the language identifier of the blog tag being added, and the `primaryId` of the blog tag designated as the primary blog tag in the target multi-language group. ### Detach a blog tag from a multi-language group To remove a blog tag from a multi-language group, make a `POST` request to `/multi-language/detach-from-lang-group` and include a JSON payload containing the ID of the target blog tag. # CMS Content Audit API @ The CMS Content Audit API allows you to filter and sort on content object changes by type, time period, or HubSpot user ID, so you’re never left wondering what happened. $ guides & api/cms --- The CMS Content Audit API allows you to filter and sort content object changes by type, time period, or HubSpot user ID. This endpoint is only available in accounts with a _**CMS Hub** Enterprise_ subscription. **Example use case:** find out which user most recently made changes to a list of pages. ## CMS Event Type Listing | Event Type | Description | | ------------- | ------------------------------------------- | | `CREATED` | An object has been created. | | `DELETED` | An object has been deleted or disconnected. | | `PUBLISHED` | An object has been published. | | `UNPUBLISHED` | An object has been unpublished. | | `UPDATED` | An object has been updated. | ## CMS Object Types Listing | Object Type | Description | | --- | --- | | `BLOG` | Changes made to your blog settings in your account settings. | | `BLOG_POST` | Blog posts associated with your blog(s). | | `CONTENT_SETTINGS` | Changes made to your website settings in your account settings. The values changed will appear in the `meta` JSON array. | | `CTA` | Changes made to your [Calls-to-action (CTAs).](https://knowledge.hubspot.com/cta/get-started-with-calls-to-action-ctas) | | `DOMAIN` | Changes made to the domains connected in your Domains & URLs settings in your account settings. | | `EMAIL` | Changes made to emails in the email editor. | | `FILE` | Changes made to files in the [files tool](https://knowledge.hubspot.com/cos-general/organize-edit-and-delete-files). | | `GLOBAL_MODULE` | Changes made to [global modules.](https://knowledge.hubspot.com/cos-general/can-i-make-changes-to-a-global-module-in-only-one-template) | | `HUBDB_TABLE` | Changes made to HubDB tables. | | `KNOWLEDGE_BASE` | Changes made to your knowledge base settings in your account settings. | | `KNOWLEDGE_BASE_ARTICLE` | Changes made to knowledge base articles in the content editor. | | `LANDING_PAGE` | Changes made to landing pages in the content editor. | | `MODULE` | Changes made to [modules](/guides/cms/content/modules/overview). | | `SERVERLESS_FUNCTION` | Changes made to [serverless functions](/guides/cms/content/data-driven-content/serverless-functions/overview). | | `TEMPLATE` | Changes made to [templates](/guides/cms/content/templates/overview). | | `THEME` | Changes made to [Theme Settings](https://knowledge.hubspot.com/website-pages/edit-your-global-theme-settings) and when [Themes](https://knowledge.hubspot.com/website-pages/edit-your-global-theme-settings) are created. | | `URL_MAPPING` | Changes made to your URL Redirects in URL Redirects settings in your account settings. | | `WEBSITE_PAGE` | Changes made to website pages in the content editor. | # CMS API | Domain management @ This endpoint can be used to get a list of all the domains and their associated properties for a HubSpot account $ guides & api/cms --- These endpoints allow you to return information about the domains connected to a particular HubSpot CMS site. You can return data for a list of domains or specify a domain by ID. [Learn more about setting up domains for your site.](https://knowledge.hubspot.com/reports/set-up-your-site-domains) # CMS API | HubDB @ The HubDB endpoints are used to get and manage data in your HubDB data tables. $ guides & api/cms --- # CMS API | HubDB HubDB is a relational data store that presents data as rows, columns, and cells in a table, much like a spreadsheet. HubDB tables can be added or modified [within your HubSpot account](/guides/cms/storage/hubdb/overview#creating-your-first-table), but you can also use the HubDB API to create and manage your HubDB tables. For information on using data from HubDB tables on your website or in [programmable emails](/guides/cms/content/data-driven-content/emails-with-programmable-content), check out [HubSpot's CMS developer documentation](/guides/cms/storage/hubdb/overview). Similar to HubSpot website pages, HubDB tables support `draft` and `published` versions. This allows you to update data in the table, either for testing or to allow for a manual approval process, without affecting any live pages. Learn more about [drafted versus live tables](#draft-vs-live-tables). If a table is set to be [allowed for public access](https://knowledge.hubspot.com/website-and-landing-pages/create-and-hubdb-tables#manage-table-settings-content-hub-professional-and-enterprise-only), you can access the published version of the table and rows without any authentication by specifying your HubSpot account ID via the query parameter `portalId`. If you're migrating from v2 of the HubDB API, learn more about the [changes in the current (v3) API](#changes-in-v3). Endpoints that support `GET` also support `CORS`, so you can access data in a table client-side using JavaScript and your account ID. Other methods and the _Get all tables_ endpoint require authentication and do not support `CORS`. ## Rate limits HubDB API requests have different rate limits, depending on the type of request: - Any `GET` requests made that don't require authentication (including client-side JavaScript requests) are limited to 10 requests per second. These requests won't count towards the daily limit. - All other requests [using authentication](/guides/api/app-management/oauth-tokens) follow the [standard limits](/reference/api). ## Draft vs live tables HubDB tables have both draft and live versions and live versions can be published or unpublished. This will allow you to update data in the table, either for page previews or testing or to allow for a manual approval process, without affecting any live pages. In this API, separate endpoints are designated for the draft and published versions of a table. For example, you can retrieve the published version of a table by making a `GET` request to [`/cms/v3/hubdb/tables/{tableIdOrName}`](/reference/api/cms/hubdb#get-%2Fcms%2Fv3%2Fhubdb%2Ftables%2F%7Btableidorname%7D). To retrieve any content that has been drafted but not yet published, you would add `/draft` to the end of the URL: [`/cms/v3/hubdb/tables/{tableIdOrName}/draft`](/reference/api/cms/hubdb#get-%2Fcms%2Fv3%2Fhubdb%2Ftables%2F%7Btableidorname%7D%2Fdraft). Draft data can be reviewed and then pushed in HubSpot, or with the `/push-live` endpoint. The draft data can also be discarded via the [`/reset`](/reference/api/cms/hubdb#post-%2Fcms%2Fv3%2Fhubdb%2Ftables%2F%7Btableidorname%7D%2Fdraft%2Freset) endpoint, allowing you to revert to the current live version of the data without disruption. ## Create a HubDB table To create a HubDB table, make a `POST` request to `/cms/v3/hubdb/tables`. In the request body, specify the following required fields: | Field | Type | Description | Example | | --- | --- | --- | --- | | `name` | String | The internal name of the table. This name cannot be changed once the table is created. Names can only include lowercase letters, digits, and underscores and cannot begin with a number. | `"name": "my_table"` | | `label` | String | The label of the table that users see when editing the table in HubSpot. | `"label":"My table"` | In addition, you can specify the following optional fields: | Field | Type | Description | Example | | --- | --- | --- | --- | | `useForPages` | Boolean | Whether the table can be used for [creating dynamic pages](#configuring-hubdb-tables-for-dynamic-pages). | `"useForPages": false` | | `allowPublicAPIAccess` | Boolean | Whether the table can be read without authorization. | `"allowPublicApiAccess": false` | | `allowChildTables` | Boolean | Whether child tables can be created for the table. | `"allowChildTables": false` | | `enableChildTablePages` | Boolean | Whether [multilevel dynamic pages](/guides/cms/content/data-driven-content/dynamic-pages/multilevel) should be created using child tables. | `"enableChildTablePages": false` | | `columns` | Object | The columns of the table. Learn more about column properties in the [Add table columns](#add-table-columns) sections. | `See "Add table columns" section below` | Without any columns added yet, your create request might look like the following: ```json // Example request { "name": "test_table", "label": "Test Table", "useForPages": false, "allowPublicApiAccess": false, "allowChildTables": true, "enableChildTablePages": false, "columns": [] } ``` ### Add table columns Each column in a HubDB table can be defined with the following properties: | Field | Type | Description | Example | | --- | --- | --- | --- | | `name` | String | Required. The internal name of the column. Cannot be changed after the column is created. | `"name": "row_name"` | | `label` | String | Optional. The label for the column that users will see when editing the table in HubSpot. | `"label": "Row label"` | | `type` | String | The data type of the column. Must be one of the following:<ul><li>`TEXT`: a text field.</li><li>`RICHTEXT`: a text field that supports basic HTML formatting. Not recommended for raw HTML, as it may impact whether the HTML is editable in HubSpot. Editing the code in HubSpot may also impact the way the code is rendered. </li><li>`NUMBER`: a number field.</li><li>`BOOLEAN`: represented as a checkbox in HubSpot. Use `0` for unchecked and `1` for checked.</li><li>`DATE`: stores a specific date as a millisecond timestamp set to midnight UTC.</li><li>`DATETIME`: stores a date and a time as a millisecond timestamp.</li><li>`SELECT`: the column can only be set to one of a set of options. See the `options` field below for required properties.</li><li>`MULTISELECT`: the column can be set to one or more of a set of options. See the `options` field below for required properties.</li><li>`LOCATION`: stores a latitude and longitude location.</li><li>`IMAGE`: stores the URL of an image.</li><li>`VIDEO`: stores the player ID of the video.</li><li>`FOREIGN_ID`: the column will reference a column from another HubDB table. In addition, you must define the other HubDB table with the following properties:<ul><li>foreignTableId: the ID of the other HubDB table. </li><li>foreignColumnId: the ID of the column in the other HubDB table.</li></ul></li><li>`CURRENCY`: stores the number as a currency value.</li><li>`FILE`: stores a file from the file manager. You'll also need to include a `fileType` field to specify whether the field can store all file types (`FILE`) or only document types such as PDF (`DOCUMENT`).</li></ul> | `"type": "type"` | | `options` | Object | A list of options for select and multiselect columns. Each option is defined with a `name` along with a `type` equal to `option`. | `"option": [{"name":"Option 1", "type":"option"}, {"name": "Option 2", "type": "option"}]` | Using the above fields, your request to create a new HubDB table might look like the following: ```json // Example request { "label": "Test Table", "name": "test_table", "columns": [ { "name": "text_column", "label": "Text Column", "archived": false, "type": "TEXT" }, { "name": "number_column", "label": "Number Column", "archived": false, "type": "NUMBER" }, { "name": "multiselect", "label": "Multi Select Column", "archived": false, "type": "multiselect", "options": [ { "name": "Option 1", "type": "option" }, { "name": "Option 2", "type": "option" } ] } ], "useForPages": true, "allowChildTables": true, "enableChildTablePages": false, "allowPublicApiAccess": false } ``` After creating a table, columns will be assigned IDs in ascending order. When updating existing columns, include the column's `id` field in the input object. ### Add table rows You can add rows either manually through the API, or you can [import rows from a CSV file](#import-rows-from-csv). To add rows to a HubDB table, make a `POST` request to `/cms/v3/hubdb/tables/{tableIdOrName}/rows`. For each table row, you can include the following fields: | Field | Type | Description | Example | | --- | --- | --- | --- | | `values` | Object | A list of key-value pairs with the column name and the value you want to add to it.<br /><br />If you don't want to set a specific value for a column, you can omit the column name from the list of key-value pairs. | `"values": { "text_column": "sample text", "number_column": 76}` | | `path` | String | For tables [enabled for dynamic pages](/guides/cms/content/data-driven-content/dynamic-pages/hubdb), `path` is the path suffix used for the page created for this row. | `"path": "example_url_path"` | | `name` | String | For tables [enabled for dynamic pages](/guides/cms/content/data-driven-content/dynamic-pages/hubdb), `name` is the HTML title used for the page created for this row. | `"name": "Example Title"` | | `childTableId` | Number | When creating [multilevel dynamic pages](/guides/cms/content/data-driven-content/dynamic-pages/multilevel), `childTableId` specifies the child table ID. | `"childTableId": 123456` | Using the above fields, your request might look similar to the following: ```json // Example request { "values": { "text_column": "sample text value", "number_column": 76, "rich_text_column": "<strong>This is a styled paragraph.</strong>", "date_column": 1591228800000, "date_time_column": 1604450520000, "boolean_column": 1, "select_column": { "name": "option 1", "type": "option" }, "multiselect_column": [ { "name": "Option 1", "type": "option" }, { "name": "Option 2", "type": "option" } ], "url_column": "https://www.hubspot.com/marketing", "video_column": 3392210008, "image_column": { "url": "https://f.hubspotusercontentqa00.net/hubfs/99992002/image3%20(1).jpg", "width": 1600, "height": 900, "type": "image" }, "foreign_id_column": [ { "id": "4364402239", "type": "foreignid" }, { "id": "4364402240", "type": "foreignid" } ] }, "path": "test_path", "name": "test_title", "childTableId": "1902373" } ``` ### Import rows from CSV To import data into a HubDB table from a CSV file, make a `POST` request to `/cms/v3/hubdb/tables/{tableIdOrName}/draft/import`. The import endpoint takes a `multipart/form-data` `POST` request: - `config`: a set of JSON-formatted options for the import. - `file`: the CSV file that you want to import. In `config`, include the following fields as a JSON string: | Field | Type | Description | Example | | --- | --- | --- | --- | | `skipRows` | Number | The number of header rows that should be skipped over. This field defaults to `1`, skipping the first row and treating it as a header. Set this to `0` if all of the rows are valid data. | `"skipRows": 0` | | `separator` | String | The column delimiter in the CSV file. Set to `","` by default. | `"separator": ","` | | `idSourceColumn` | Number | The index of the column in the source file containing the row’s ID (`hs_id`).If this column is specified, the import will update the existing rows in the table for the matching row IDs from the CSV file. This is optional and you can ignore this during the first time you import data into a table.See the [Reset options](#reset-options) section below more detailed information. | `"idSourceColumn": 1` | | `resetTable` | Boolean | Defaults to `false`, meaning that the table's rows will be updated without removing any existing rows. If set to `true`, the spreadsheet rows will overwrite table data, meaning that any rows in the table that aren't in the spreadsheet will be deleted.See the [Reset options](#reset-options) section below more detailed information. | `"resetTable": true` | | `nameSourceColumn` | Number | For tables [enabled for dynamic pages](/guides/cms/content/data-driven-content/dynamic-pages/hubdb), `nameSourceColumn` specifies the column in the CSV file that contains the row's `name`. Column numbers are in ascending order, with the first column being `1`. | `"nameSourcecolumn": 5` | | `pathSourceColumn` | Number | For tables [enabled for dynamic pages](/guides/cms/content/data-driven-content/dynamic-pages/hubdb), `pathSourceColumn` specifies the column in the CSV file that contains the row's `path`. Column numbers are in ascending order, with the first column being `1`. | `"pathSourcecolumn": 6` | | `childTableSourceColumn` | Number | Specifies the column in the CSV file that contains the row's `childTableId`. Column numbers are in ascending order, with the first column being `1`. | `"childTableSourcecolumn": 3` | | `columnMappings` | Array | A list of mappings for the columns in the source file to the columns in the destination HubDB table.Each mapping must have the following format: `{"source":1,"target”:"columnIdOrName"}` <ul><li>`source`: the column index in the source file. For example, `2` for the second column.</li><li>`target`: the ID or name of the HubDB table column. You can get the ID or name of a column by [getting the details for the table](#retrieve-hubdb-data).</li></ul>If your file has an `hs_id` column, you shouldn't include it in `columnMappings`. Instead, include it as the `idSourceColumn` to update existing rows. | `"columnMappings": [{"source":1, "target": 2}, {"source": 2, "target": "column_name"}]` | | `primaryKeyColumn` | String | The name of a column in the target HubDB table that will be used for deduplication. The column's ID cannot be used for this field. | `"primaryKeyColumn": "column_name"` | | `encoding` | String | The file's encoding type. For example, `utf-8`, `ascii`, `iso-8859-2`, `iso-8859-5`, `iso-2022-jp`, `windows-1252`. | `"encoding": "utf-8"` | | `format` | String | Only CSV is supported. | `"format": "csv"` | Using the above table, your `config` JSON might look like the following: ```json // Example config JSON { "skipRows": 1, "separator": ",", "idSourceColumn": 1, "resetTable": false, "columnMappings": [ { "target": 1, "source": 2 }, { "target": 2, "source": "zip_code" } ], "primaryKeyColumn": "name", "encoding": "utf-8", "format": "csv" } ``` If using cURL, your command might look like the following: ```shell curl -L -X POST 'https://api.hubspotqa.com/hubdb/api/v2/tables/xxxxxx/import?portalId=xxxxxxx' \ -H 'Content-Type: multipart/form-data' \ -F 'config="{\"skipRows\":1,\"format\":\"csv\",\"separator\":\",\",\"encoding\":\"iso-8859-1\",\"columnMappings\":[{\"target\":1,\"source\":7},{\"target\":3,\"source\":8}],\"idSourceColumn\":1,\"pathSourceColumn\":null,\"nameSourceColumn\":null,\"childTableSourceColumn\":null,\"resetTable\":true}"' \ -F 'file=@"/Users/xxxxxxxxxxxxx/Downloads/filename.csv"' ``` ### Date formatting There are several formats you can use when importing data into a date-type column. **Integers** - `yyyy/mm/dd` - `yyyy/mm/dd` - `mm/dd/yyyy` - `mm/dd/yy` These formats require the month to precede the day (i.e., `dd/mm/yy` is not accepted). Integers can be separated by hyphens (`-`) or slashes (`/`). **Relaxed dates** You can also import date formats that are less standardized than integer-based dates. For example: - `The 1st of March in the year 2022` - `Fri Mar 4 2022` - `March 4th '22` **Relative dates** HubSpot will parse the following date formats relative to the current day: - `next Thursday` - `Today` - `tomorrow` - `3 days from now` ### Reset options When importing data from a CSV file into a HubDB table, you can set the `resetTable` field to `true` or `false` (default) to manage whether HubDB row data is overwritten. - If `resetTable` is set to `true`: - If the rows in the CSV file does not have a row ID column (`hs_id` or row ID is specified as `0`, those rows will be inserted with the new row IDs generated. - If the row IDs in the CSV file already exists in the target table, the existing rows in the table will be updated with the new values from the input file. - If the table has rows but the input CSV file does not have those row IDs, those rows will be deleted from the target table. - If the row IDs in the input CSV file do not exist in the target table, those rows will be inserted with the new row IDs generated and the row IDs given in the input file will be ignored. - If the input CSV file does not contain the row ID column at all, all the rows will be deleted from the target table and the rows from the input file will be inserted with the new row IDs generated. - If `resetTable` is set to `false` (default): - If the row IDs in the CSV file already exists in the target table, the existing rows in the table will be updated with the new values from the input file. - If the table has rows but the input CSV file does not have those row IDs, those rows will _not_ be deleted from the target table and those rows will remain unchanged. - If the row IDs in the input CSV file do not exist in the target table, those rows will be inserted with the new row IDs generated and the row IDs given in the input file will be ignored. - If the rows in the CSV file does not have a row ID column or row ID is specified as `0`, those rows will be inserted with the new row IDs generated. ## Retrieve HubDB data There are multiple ways to retrieve HubDB data, depending on whether you're looking for table details or the rows of a table: - To retrieve table details from all published tables, make a `GET` request to `/cms/v3/hubdb/tables`. - To retrieve table details from a specific published table, make a `GET` request to `/cms/v3/hubdb/tables/{tableIdOrName}`. - To retrieve all rows from a specific table, make a `GET` request to `/cms/v3/hubdb/tables/{tableIdOrName}/rows`. - To retrieve a specific row from a table, make a `GET` request to `/cms/v3/hubdb/tables/{tableIdOrName}/rows/{rowId}`. When retrieving row data, you can further filter and sort the results. If a table is set to be [allowed for public access](https://knowledge.hubspot.com/website-and-landing-pages/create-and-hubdb-tables#manage-table-settings-content-hub-professional-and-enterprise-only), you can access the published version of the table and rows without any authentication by specifying your HubSpot account ID via the query parameter `portalId`. ### Filter returned rows When retrieving HubDB table data, you can apply filters as query parameters to receive specific data. Filter query parameters are constructed as follows: `columnName__operator`. For example, if you have a number column named _bar_, you can filter results to only include rows where _bar_ is greater than 10: `&bar__gt=10`. All filters are ANDed together (OR filters are not currently supported). When filtering, keep the following in mind: - When passing values for `multiselect` columns, the values should be comma-separated (e.g. `multiselect_column__contains=1,2`). - For `datetime` filters, you can use relative dates in place of timestamps in order to specify a value relative to the current time. For example, `-3h` would correspond to the timestamp 3 hours before now, whereas `10s` would correspond to 10 seconds in the future. Supported time units are ms (milliseconds), s (seconds), m (minutes), h (hours), d (days). Current time can be used by specifying a zero value: 0s - For the purposes of these filters, the built in column `hs_id` is a `number` column, the `hs_created_at` column is a `datetime`, and the `hs_path` and `hs_name` columns are `text` columns. Below, learn which operators can be applied to which column types: | Operator | Name | Description | | --- | --- | --- | | `eq (or none)` | Equals | All column types.This filter is applied if no operator is used. When used with multiselect columns, returns rows that exact match supplied values. | | `ne` | Not equal to | All column types. | | `contains` | Contains | Text, richtext, and multiselect.When used with multiselect columns, returns rows that contain all of the supplied values. This filter is <u>case sensitive</u>. | | `lt` | Less than | Number, date, and datetime. | | `lte` | Less than or equal to | Number, date, and datetime. | | `gt` | Greater than | Number, date, and datetime. | | `gte` | Greater than or equal to | Number, date, and datetime. | | `is_null` | Null | All column types except boolean.This filter doesn't require a value (e.g. `&exampleColumn__is_null=`). | | `not_null` | Not null | All column types except boolean.This filter doesn't require a value (e.g. `&exampleColumn__not_null=`). | | `like` | Like | Text and richtext. | | `not_like` | Not like | Text and richtext. | | `icontains` | Contains | Text and richtext.This filter is <u>case insensitive</u>. | | `startswith` | Starts with | Text and richtext. | | `in` | In | Number, select, and multiselect.Returns rows where the column includes at least one of the passed options. When there is no other `sort` in the query parameter, the results will be sorted in the order in which values are specified in the `in` operator. | ### Sort returned rows When retrieving HubDB data, you can apply sorting as a query parameter to determine the order of the returned data. To sort data, add a `sort` query parameter and specify the column name: `&sort=columnName` By default, data will be returned in the natural order of the specified column. You can reverse the sort by adding a `-` to the column name: `&sort=-columnName` You can include this parameter multiple times to sort by multiple columns. In addition to sorting by a column, there are three functions that can be used: - `geo_distance(location_column_name, latitude, longitude)`: takes the name of a location column and coordinates, returns the rows ordered by how far away the values of the specified location column are from the provided coordinates. - `length(column_name)`: takes the name of a column, returns the rows ordered by the length of the column value (calculated as a string) - `random()`: returns the rows in random order. These functions also support reverse ordering. For example, the following `geo_distance` sort returns items that are the farthest away first: `sort=-geo_distance(location_column,42.37,-71.07)` ## Configuring HubDB tables for dynamic pages Using HubSpot's CMS, you can use a HubDB table as a data source to [generate dynamic pages](/guides/cms/content/data-driven-content/dynamic-pages/hubdb). For example, you can create a table that contains a row for each member of your executive team, with columns containing information that you want to display on a page. After selecting that table as the dynamic data source for a page, that page will generate a listing page that displays all rows as summary items, along with separate pages for each row, similar to a blog listing page and blog post pages. To enable a table to be selected as a data source in the content editor, you'll need to set `useForPage` to `true`. You can optionally include `dynamicMetaTags` to specify which columns to use for each page's metadata. When specifying `dynamicMetaTags`, you'll need to ensure that the page is using `page_meta` HubL tags instead of `content`. Learn more in the [dynamic pages guide](/guides/cms/content/data-driven-content/dynamic-pages/hubdb#metadata). For example, the code below would create a table that can be used for dynamic pages, and specifies the three columns to use for page metadata. ```json // Example POST request to create table { "name": "dynamic_page_table", "label": "Dynamic page table", "useForPages": true, "columns": [ { "name": "meta_description", "label": "Meta description", "archived": false, "type": "TEXT" }, { "name": "featured_image", "label": "Featured image", "archived": false, "type": "IMAGE" }, { "name": "canonical_url", "label": "Canonical URL", "archived": false, "type": "URL" } ], "dynamicMetaTags": { "DESCRIPTION": 1, "FEATURED_IMAGE_URL": 2, "LINK_REL_CANONICAL_URL": 3 } } ``` | Parameter | Type | Description | | --- | --- | --- | | `useForPages` | Boolean | Set to `true` to enable the table to be used as a data source for dynamic pages. | | `dynamicMetaTags` | Object | Specifies the columns by ID to use for metadata on each dynamic page. Can contain:<ul><li>`DESCRIPTION`</li><li>`FEATURED_IMAGE_URL`</li><li>`LINK_REL_CANONICAL_URL`</li></ul>For any metadata fields not specified, pages will inherit the respective values from its parent page. | | `DESCRIPTION` | Number | The numeric ID of the column to use for each page's meta description. | | `FEATURED_IMAGE_URL` | Number | The numeric ID of the column to use for each page's featured image URL. | | `LINK_REL_CANONICAL_URL` | Number | The numeric ID of the column to use for each page's canonical URL. | ## Changes in v3 - Tables should have both `name` and `label`. This name cannot be changed once the table is created. Names can only include lowercase letters, digits, and underscores and cannot begin with a number. Both `name` and `label` should be unique in the account. - The API supports both table `id` and `name` in the URL paths. - The [`GET` row endpoints](/reference/api/cms/hubdb#get-%2Fcms%2Fv3%2Fhubdb%2Ftables%2F%7Btableidorname%7D%2Frows%2F%7Browid%7D) return column `name` instead of `id` in `values` field. Also, `POST` / `PUT` / `PATCH` row endpoints require column `name` instead of `id` in `values` field. - The [row update `PATCH` endpoints](/reference/api/cms/hubdb#patch-%2Fcms%2Fv3%2Fhubdb%2Ftables%2F%7Btableidorname%7D%2Frows%2F%7Browid%7D%2Fdraft) now accept sparse updates, which means you can specify only the column values that you need to update (whereas you had to specify all the column values in the previous versions). When you update a column with a list of values such as multiselect, you need to specify the list of all the values. In order to delete the value for a column, you need to specify the column with the value as `null` in the request. - Removed the endpoints to `get` / `update` / `delete` a row cell in favor of the [row update `PATCH` endpoints](/reference/api/cms/hubdb#patch-%2Fcms%2Fv3%2Fhubdb%2Ftables%2F%7Btableidorname%7D%2Frows%2F%7Browid%7D%2Fdraft). - The [import endpoint](/reference/api/cms/hubdb#post-%2Fcms%2Fv3%2Fhubdb%2Ftables%2F%7Btableidorname%7D%2Fdraft%2Fimport) now supports an optional field `idSourceColumn` along with existing fields in the JSON-formatted options. You can use this field to specify the column in the CSV file which contains row IDs. To import new rows along with the new values for existing rows, specify `0` as the row ID for the new rows and the valid row IDs for the existing columns. Learn more about [importing rows from CSV](/guides/api/cms/hubdb#import-rows-from-csv). Also you can use column names or IDs in the target field of the column mappings in the JSON-formatted options. - The [clone endpoint](/reference/api/cms/hubdb#post-%2Fcms%2Fv3%2Fhubdb%2Ftables%2F%7Btableidorname%7D%2Fdraft%2Fclone) requires a new name and new label to be set for the cloned table. # CMS API | Media Bridge @ $ guides & api/cms --- # Media Bridge API The media bridge API allows integrators to push media objects such as video and audio files, and media consumption data into HubSpot. It also creates the following features in the user’s HubSpot account: - Modules to embed media objects in HubSpot’s drag and drop editors for pages and emails. - CRM timeline events that show when prospects or customers have engaged with videos, audio, and other media types. - Segmented lists for targeted and personalized experiences. - Workflows to automate interactions based on media consumption events. - Reports to measure the impact of media assets. The media bridge uses both [custom objects](/guides/api/crm/objects/custom-objects) and unified events, HubSpot’s events tracking system. This means you can use both the media bridge API and the custom objects API to build your integration. ## Use the media bridge API You need a [HubSpot developer account](https://developers.hubspot.com/get-started) to register your media bridge app and set up your initial media object definitions before connecting your app to a HubSpot user’s account. ## Create and customize your media object definitions To define a media object, make a `POST` request to `/media-bridge/v1/{appId}/settings/object-definitions`. You will use the **mediaTypes** parameter to define the object: **VIDEO**, **AUDIO**, **DOCUMENT**, **IMAGE** or **OTHER**. After defining your media objects, create and modify the media object properties by making a `PATCH` request to `/media-bridge/v1/{appId}/schemas/{objectType}` and a `POST` request to `/media-bridge/v1/{appId}/properties/{objectType}`. Any API calls made will include your developer account ID as the **portalID** query parameter. ## Connect your media bridge app to a HubSpot user’s account To connect your media bridge app to a HubSpot user’s account, you must create an app definition in your HubSpot developer’s account for it. App definitions include: - Details such as the logo and text to be shown to the HubSpot user when your integration attempts to make an initial connection to their account. - Scopes your integration needs in the user’s HubSpot account. To connect your media bridge app to a HubSpot user's account: - Create an [application definition](/guides/apps/overview) in your developer account for the media bridge app. - Include the following scopes when defining your application: - `media_bridge.read` - `media_bridge.write` - Use [OAuth](/guides/api/app-management/oauth-tokens) authentication when authenticating calls made by your app. Learn more about [authentication methods](/guides/apps/authentication/intro-to-auth). To verify the app is installed correctly in a customer's portal: - Visit `https://app.hubspot.com/media-bridge-demo/{HubID}`, replacing `{HubID}` with the account ID. - In the upper right, click the **App** dropdown menu and select your **media bridge app**. - In the app, you can view the app’s supported media types and create example media. Once the media bridge app has been installed in a customer’s portal, you can: - [Create media objects](#create-your-media-objects) - [Create CMS Modules for embedding your media objects](#create-cms-modules-to-embed-media) - [Send media events](#send-your-media-events) ## Create your media objects After creating your media object definitions and installing your media bridge app in a user’s account, you can use the [OAuth token](/guides/apps/authentication/working-with-oauth) to create and modify media objects in the account. Since media objects are custom objects, use the [custom objects API endpoints](/guides/api/crm/objects/custom-objects#tab-2) to create them: - Make a `GET` request to `/media-bridge/v1/{appId}/settings/object-definitions/{mediaType}` to find the **objectType**. - Make a `POST` request to `/crm/v3/objects/{objectType}` to create the media object in the user’s account. A media object represents a piece of embeddable content in a third-party system. Once a media object is added to the media bridge, it can be embedded in HubSpot’s CMS content and associated with media events. For **VIDEO** and **AUDIO** media objects, the tables below list out all of the default and required properties (\* denotes required): | Parameter | Type | Description | | --- | --- | --- | | `id` | Number | An id used to identify the specific piece of media in HubSpot’s media bridge system. This is autogenerated by HubSpot and cannot be set by developers. | | `hs_duration` | Number | The duration of the media in milliseconds. | | `hs_oembed_url*` | String | A URL that must return a valid oEmbed response that follows the [oEmbed spec.](https://oembed.com/#section2) Requires `video` or `rich` type with an iframe in `html`. | | `hs_file_url` | String | URL of the raw media file. This may be used in the future to help support social embedding. | | `hs_thumbnail_url` | String | URL of an image used as the thumbnail for embedding the media into content. The ideal size for this thumbnail is 640x480 pixels. | | `hs_poster_url` | String | URL of an image representing the media. This image should be the same dimensions as the original media, and may be used in places where an image placeholder is needed (for example, when the media is inserted in an email). | | `hs_external_id` | String | The id of the media in the third party’s system. This gives integrators the ability to fetch media from the media bridge based on the same id that they use in their own system. (This is the API endpoint that leverages this mapping) | | `hs_folder_path` | String | A provider-supplied path to the object, intended to represent the object’s location in the third party’s folder system (if any). HubSpot will attempt to represent this directory structure when displaying these objects to the user, but may nest each provider’s objects and folders within a top-level folder named after the provider. | | `hs_title*` | String | The name of the media. This will be shown inside of the HubSpot UI in places such as the media picker. | | `hs_details_page_link` | String | URL that allows a user to view or interact with the media in the media provider’s system. This is used in the HubSpot UI to give users the ability to identify the media without relying on just the title. | For **IMAGE** media objects, the tables below list out all of the default and required properties (\* denotes required): | Parameter | Type | Description | | --- | --- | --- | | `id` | Number | An id used to identify the specific piece of media in HubSpot’s media bridge system. This is autogenerated by HubSpot, and cannot be set by developers. | | `hs_oembed_url*` | String | A URL that must return a valid oEmbed response that follows the [oEmbed spec.](https://oembed.com/#section2) Requires `video` or `rich` type with an iframe in `html`. | | `hs_file_url*` | String | The URL of the raw media file. This may be used in the future to help support social embedding. | | `hs_thumbnail_url` | String | URL to an image that will be used as the thumbnail for embedding the media into content in places such as the media picker. The ideal size for this thumbnail is 640x480 pixels. | | `hs_poster_url` | String | URL of an image representing the media. This image should be the same dimensions as the original media, and may be used in places where an image placeholder is needed (for example, when the media is inserted in an email). | | `hs_external_id` | String | The id of the media in the third party’s system. This gives integrators the ability to fetch media from the media bridge based on the same id that they use in their own system. (This is the api endpoint that leverages this mapping) | | `hs_folder_path` | String | A provider-supplied path to the object, intended to represent the object’s location in the third party’s folder system (if any). HubSpot will attempt to represent this directory structure when displaying these objects to the user, but may nest each provider’s objects and folders within a top-level folder named after the provider. | | `hs_title*` | String | The name of the media. This will be shown inside of the HubSpot UI in places such as the media picker. | | `hs_details_page_link` | String | A URL that allows a user to view or interact with the media in the media provider’s system. This is used in the HubSpot UI to give users the ability to identify the media without relying on just the title. | ## Create CMS Modules to embed media Each media bridge app provider is responsible for creating their own [module](/guides/cms/content/modules/overview) to render their media in the HubSpot CMS. When a media bridge app is installed in a HubSpot account, the [Embed field](/reference/cms/fields/module-theme-fields#embed) on the module has an additional _Media integration_ source type. This allows the user to select media from the installed app to be embedded on their website page. After the user selects a piece of media to be embedded, the media’s **oembed_url** and **oembed_response** are available in HubL to easily render players. Additionally, the **id** and **media_type** of selected media are stored to enable querying for the underlying CRM object via the **crm_objects** HubL function. This can be used to fetch any or all of the properties that are part of a media object. An example use of the crm_objects HubL function with a media object where the ids are **459** and **922**: `{% set objects = crm_objects("a123_Videos", [459,922]) %} {{ objects }}` To fetch a specific image with the same object: `{% set object = crm_object("a123_Images", 459) %} {{ object }}` Apps can fetch the object type (“a123_Videos” in the example) by making a `GET` request to `/media-bridge/{appId}/settings/object-definitions/{mediaType}`. Developers should use the [CMS Source Code API](/guides/api/cms/source-code) endpoints to push their custom module code into customers' accounts once customers have connected via oAuth. Once the module code is pushed into the customer’s account, they will automatically be able to start using the developer’s module in their content. ### Set up an oEmbed domain To use the [oEmbed](/reference/cms/hubl/functions#oembed) HubL function, the domain being used to fetch the oEmbed response must be registered by making a request to `/media-bridge/v1/{appId}/settings/oembed-domains`. The following parameters must be included: - **Scheme:** the URL pattern for the media URLs. This URL pattern is used to match the URL passed into the oEmbed HubL function to your oEmbed API. Wildcard values are supported using `*` (e.g. `www.domain.com/*`). - **URL:** the URL of your oEmbed API. The media URL is passed to this URL via a `URL` parameter. - **Discovery (Boolean):** determines whether or not your oEmbed API supports oEmbed’s [Discovery](https://oembed.com/#section4) feature. By default, the oEmbed domains registered will be made available to all customers who have installed your app. If you have custom domains that are unique to each customer, you can specify which account an oEmbed domain should be allowed to be used in by passing a portalId value into the API request when setting up the oEmbed domain. This will ensure that only the specified HubSpot account can use that oEmbed domain. ## Create a custom module To create a custom module: - Navigate to **Marketing** > **Files and Templates** > **Design Tools**. - On the upper left, click **File** \> **New file**. - In the dialog box, click the **What would you like to build today?** dropdown menu and select **Module**. - Click **Next**. - Select the checkbox next to each type of content where your module will be used: _pages_, _blog posts_, _blog listings_, _emails_, or _quotes_. Modules used in email templates cannot include CSS or JavaScript. - Select whether this module will be a **local module** or **global module**. If you create a [global module](https://knowledge.hubspot.com/design-manager-user-guide-v2/how-to-use-global-content-across-multiple-templates), editing this module's content will update every location where the module is used. - Enter a **file name** for your module, then click **Create**. - In the _Fields_ section on the right, click the **Add field** dropdown menu and select **Embed**. - In the _Supported source types_ section, select **Media integration**. - In the _Default embed content_ section, click **Select from \[media bridge app\]**. - In the right panel, select the **media** you want to embed in the module. - Set any of the editor options, field display conditions, and field repeater options. - Under _HubL variable name_, click **Copy** \> **Copy snippet**. - Paste the snippet into the _module.html_ section. - To preview how the module will look on your page, click **Preview**. - In the left section, click **Select from \[media bridge app\]**, then select the **media** you want to preview. ## Send your media events A media event is an event that happens in relation to a media object, like a play event. Once a media event is sent to the media bridge app, it can be used in reports and in timeline CRM cards. There are three types of media events: - **Played event:** represents when a user begins playing a piece of media. - **Quartile event:** represents when a user has reached quarterly milestones (0%, 25%, 50%, 75%, 100%) in the piece of media they’re viewing. - **Attention span event**: represents when a user has fully consumed a piece of media, or once the user has completed their session. Events can be sent by making a `POST` request to `/media-bridge/v2/events/media-played`, `/media-bridge/v2/events/media-played-percent` and `/media-bridge/v2/events/attention-span respectively`. For media events to be displayed on the user’s contact timeline in HubSpot, a <u>played</u> event must be sent to the media bridge app for every session. Events from a single session will be shown in one card on the contact activity timeline. When events are sent using the v2 event endpoints, they are processed asynchronously, unlike those sent via the v1 endpoints. As such, we recommend the following: - The v1 version of the endpoints should be used for any testing as an erroneous request will immediately error out. - The v2 version of the endpoints should be used in production as its asynchronous nature will help prevent delays in the client while the event is being written to the media bridge. Events are also retained and retried in case of a temporary failure in the media bridge’s server. ### Connect an event to a contact record To connect a media event to a contact record, you must provide either the **contactId** or a **contactUtk**. If only a **contactUtk** is provided, it will be converted into a **contactId**. If both are provided in the request, the **contactId** will be used as the source of truth. This parameter allows the media bridge app to create an association between the contact record and the event. Once a media event has been connected to a contact record, the event can be used in [cross-object reports](https://knowledge.hubspot.com/reports/create-reports-with-the-custom-report-builder). This allows customers to tie their media events to contact records, as well as associated companies and deals. ### Connecting an event to a piece of media To associate a media event to a piece of media, either the **mediaID** or **externalID** parameters must be included in the request. If both are provided, the **mediaID** will be used as the source of truth. ### Connect an event to a page To associate a media event to a HubSpot page, the following parameters must be included in the request: - If the page is hosted on the HubSpot CMS, the `pageId` must be provided. - If the page is <u>not</u> hosted on the HubSpot CMS, the `pageName` and `pageUrl` must be included. The table below outlines supported properties for the three media events: | Property | Event Type | Description | | --- | --- | --- | | `mediaBridgeObjectId` | All Events | The id of the media that this event is related to. | | `externalId` | String | The id of the media in the third party’s system. This gives developers the ability to refer to media in the media bridge based on the same id that they use in their own system. This can be used instead of the `mediaBridgeObjectId` in events. If both an `externalId` and `mediaBridgeObjectId` are provided, the `mediaBridgeObjectId` will be used and the externalId will be ignored.<br /><br /> | | `sessionId` | All Events | A unique identifier to represent a viewing session. This can mean different things to different providers, and HubSpot is letting providers decide what a session means to them. This will be used to group events that happened in the same session together. This is expected to be generated by the third party’s system. | | `contactId` | All Events | The ID of the contact in HubSpot’s system that consumed the media. This can be fetched using [HubSpot's Get contact by usertoken (utk) API](/reference/api/crm/objects/contacts/v1#get-a-contact-by-its-user-token). The API also supports supplying a usertoken, and will handle converting this into a contact ID automatically. | | `contactUtk` | All Events | The usertoken (utk) that identifies which contact consumed the media. | | `pageId` | All Events | The content Id of the page that an event happened on. | | `pageName` | All Events | The name or title of the page that an event happened on. | | `pageUrl` | All Events | The URL of the page that an event happened on. | | `occurredTimestamp` | All Events | The timestamp at which this event occurred, in milliseconds since the epoch. | | `attentionSpanMapString / attentionSpanMap` | Attention Span | This is the raw data which provides the most granular data about spans of the media, and how many times each span was consumed by the user. Example: consider a 10 second video where each second is a span. If a visitor watches the first 5 seconds of the video, then restarts the video and watches the first 2 seconds again, the resulting `attentionSpanMapString` would be `“0=2;1=2;2=1;3=1;4=1;5=0;6=0;7=0;8=0;9=0;”`. | | `totalPercentPlayed` | Attention Span | The percent of the media that the user consumed. Providers may calculate this differently depending on how they consider repeated views of the same portion of media. For this reason, the API will not attempt to validate totalPercentWatched against the attention span information for the event. If it is missing, HubSpot will calculate this from the attention span map as follows: (number of spans with a value of 1 or more)/(Total number of spans). | | `totalSecondsPlayed` | Attention Span | The seconds that a user spent consuming the media. The media bridge calculates this as `totalPercentPlayed`\*`mediaDuration`. If a provider would like this to be calculated differently, they can provide the pre-calculated value when they create the event | | `playedPercent` | Quartile Event | A quartile percent value (0, 25, 50, 75, 100) for how much of the media has been consumed so far. | | `iframeUrl` | Played Event | A URL that can be used to display data from an external system using an iFrame. When included, the event on the contact timeline will display a link that will open a modal window displaying the iFrame contents when clicked. | | `mediaType` | String | The media type that the event belongs to (for example, VIDEO or AUDIO) This allows us to properly assign the event to the correct objects when a single provider has support for multiple media types.<br /><br /> | # CMS API | Pages @ The pages endpoint is used to create and manage HubSpot pages $ guides & api/cms --- # Pages Use this API to create, manage, and publish website and landing pages. Learn more about [creating and customizing pages within HubSpot](https://knowledge.hubspot.com/website-and-landing-pages/create-and-customize-pages). For example, fetch a currently drafted page, then publish it so that it's live on your website. ## Changes in v3 The following properties are deprecated and no longer included within the response: - `campaign_name` - `is_draft` - `style_override_id` - `meta_keywords` ## Retrieve pages To retrieve an account's pages, make a `GET` request to `cms/v3/pages/{landing-pages|site-pages}`. You can filter and sort the returned pages by adding query parameters to the request URL, which are described below. For example, to retrieve all website pages that are currently published or scheduled to publish, you would make a `GET` request to the following URL:`cms/v3/pages/site-pages?state__in=PUBLISHED_OR_SCHEDULED`You'll receive a response containing information about the page, along with the page's content. ```json // Example response { "total": 1, "results": [ { "archivedAt": "1970-01-01T00:00:00Z", "archivedInDashboard": false, "attachedStylesheets": [], "authorName": "Eric D.", "categoryId": 1, "contentTypeCategory": 4, "createdAt": "2024-02-06T19:02:33.991Z", "createdById": "2931299", "currentState": "PUBLISHED", "domain": "", "featuredImage": "", "featuredImageAltText": "", "htmlTitle": "My test page", "id": "155890290211", "layoutSections": { "dnd_area": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "label": "Main section", "name": "dnd_area", "params": {}, "rowMetaData": [ { "cssClass": "dnd-section", "styles": { "backgroundColor": { "a": 1, "b": 250, "g": 248, "r": 245 }, "forceFullWidthSection": false } }, { "cssClass": "dnd-section", "styles": { "backgroundColor": { "a": 1, "b": 255, "g": 255, "r": 255 }, "forceFullWidthSection": false } } ], "rows": [ { "0": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-column-2", "params": { "css_class": "dnd-column" }, "rowMetaData": [ { "cssClass": "dnd-row" }, { "cssClass": "dnd-row" }, { "cssClass": "dnd-row" } ], "rows": [ { "0": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-module-3", "params": { "child_css": {}, "css": {}, "css_class": "dnd-module", "extra_classes": "widget-type-rich_text", ", "path": "@hubspot/rich_text", "schema_version": 2, "smart_objects": [], "smart_type": "NOT_SMART", "wrap_field_tag": "div" }, "rowMetaData": [], "rows": [], "type": "custom_widget", "w": 12, "x": 0 } }, { "0": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-column-4", "params": { "css_class": "dnd-column" }, "rowMetaData": [ { "cssClass": "dnd-row", "styles": {} } ], "rows": [ { "0": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-module-5", "params": { "child_css": {}, "css": {}, "css_class": "dnd-module", "extra_classes": "widget-type-linked_image", "horizontal_alignment": "CENTER", "img": { "alt": "Growth theme placeholder image", "loading": "lazy", "max_height": 500, "max_width": 500, "size_type": "auto_custom_max", "src": "//7528309.fs1.hubspotusercontent-na1.net/hubfs/7528309/raw_assets/public/mV0_d-cms-growth-theme_hubspot/growth/images/service-one.jpg" }, "path": "@hubspot/linked_image", "schema_version": 2, "smart_objects": [], "smart_type": "NOT_SMART", "style": "margin-bottom: 22px;", "wrap_field_tag": "div" }, "rowMetaData": [], "rows": [], "styles": { "flexboxPositioning": "TOP_CENTER" }, "type": "custom_widget", "w": 12, "x": 0 } } ], "type": "cell", "w": 6, "x": 0 }, "6": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-column-6", "params": { "css_class": "dnd-column" }, "rowMetaData": [ { "cssClass": "dnd-row" } ], "rows": [ { "0": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-module-7", "params": { "child_css": {}, "css": {}, "css_class": "dnd-module", "extra_classes": "widget-type-rich_text", "html": "<p>Add a short description about the work that you do at your company. Try to narrow in on your specialization so that you can capture the attention of your clients. Talk about the value that you can deliver through a paid consultation.</p>", "path": "@hubspot/rich_text", "schema_version": 2, "smart_objects": [], "smart_type": "NOT_SMART", "wrap_field_tag": "div" }, "rowMetaData": [], "rows": [], "type": "custom_widget", "w": 12, "x": 0 } } ], "type": "cell", "w": 6, "x": 6 } }, { "0": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-module-8", "params": { "button_link": { "no_follow": false, "open_in_new_tab": false, "rel": "", "sponsored": false, "url": { "href": "#book" }, "user_generated_content": false }, "button_text": "Book a consultation", "child_css": {}, "css": {}, "css_class": "dnd-module", "path": "../modules/button", "schema_version": 2, "smart_objects": [], "smart_type": "NOT_SMART", "styles": { "alignment": { "alignment": { "css": "", "horizontal_align": "CENTER" } } }, "wrap_field_tag": "div" }, "rowMetaData": [], "rows": [], "type": "custom_widget", "w": 12, "x": 0 } } ], "styles": {}, "type": "cell", "w": 12, "x": 0 } }, { "0": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-column-9", "params": { "css_class": "dnd-column" }, "rowMetaData": [ { "cssClass": "dnd-row", "styles": {} }, { "cssClass": "dnd-row" }, { "cssClass": "dnd-row" } ], "rows": [ { "0": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-module-10", "params": { "child_css": {}, "css": {}, "css_class": "dnd-module", "extra_classes": "widget-type-rich_text", ", "path": "@hubspot/rich_text", "schema_version": 2, "smart_objects": [], "smart_type": "NOT_SMART", "wrap_field_tag": "div" }, "rowMetaData": [], "rows": [], "type": "custom_widget", "w": 12, "x": 0 } }, { "0": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-module-11", "params": { "child_css": {}, "content": { "content": "<h3 style='text-align:center;'>Service</h3><p>Summarize the service you provide. Add a message about the value that you can provide to your customers.</p>" }, "css": {}, "css_class": "dnd-module", "icon": { "icon": { "icon_set": "fontawesome-5.0.10", "name": "balance-scale", "type": "SOLID", "unicode": "f24e" } }, "path": "../modules/service-card", "schema_version": 2, "smart_objects": [], "smart_type": "NOT_SMART", "styles": { "card": { "spacing": { "spacing": { "css": "margin-bottom: 22px;\n", "margin": { "bottom": { "units": "px", "value": 22 } } } } }, "icon": { "background": { "color": { "color": "#494a52", "css": "#494a52", "hex": "#494a52", "opacity": 100, "rgb": "rgb(73, 74, 82)", "rgba": "rgba(73, 74, 82, 1)" } } } }, "wrap_field_tag": "div" }, "rowMetaData": [], "rows": [], "type": "custom_widget", "w": 4, "x": 0 }, "4": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-module-12", "params": { "child_css": {}, "content": { "content": "<h3 style='text-align:center;'>Service</h3><p>Summarize the service you provide. Add a message about the value that you can provide to your customers.</p>" }, "css": {}, "css_class": "dnd-module", "icon": { "icon": { "icon_set": "fontawesome-5.0.10", "name": "industry", "type": "SOLID", "unicode": "f275" } }, "path": "../modules/service-card", "schema_version": 2, "smart_objects": [], "smart_type": "NOT_SMART", "styles": { "card": { "spacing": { "spacing": { "css": "margin-bottom: 22px;\n", "margin": { "bottom": { "units": "px", "value": 22 } } } } }, "icon": { "background": { "color": { "color": "#494a52", "css": "#494a52", "hex": "#494a52", "opacity": 100, "rgb": "rgb(73, 74, 82)", "rgba": "rgba(73, 74, 82, 1)" } } } }, "wrap_field_tag": "div" }, "rowMetaData": [], "rows": [], "type": "custom_widget", "w": 4, "x": 4 }, "8": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-module-13", "params": { "child_css": {}, "content": { "content": "<h3 style='text-align:center;'>Service</h3><p>Summarize the service you provide. Add a message about the value that you can provide to your customers.</p>" }, "css": {}, "css_class": "dnd-module", "icon": { "icon": { "icon_set": "fontawesome-5.0.10", "name": "server", "type": "SOLID", "unicode": "f233" } }, "path": "../modules/service-card", "schema_version": 2, "smart_objects": [], "smart_type": "NOT_SMART", "styles": { "card": { "spacing": { "spacing": { "css": "margin-bottom: 22px;\n", "margin": { "bottom": { "units": "px", "value": 22 } } } } }, "icon": { "background": { "color": { "color": "#494a52", "css": "#494a52", "hex": "#494a52", "opacity": 100, "rgb": "rgb(73, 74, 82)", "rgba": "rgba(73, 74, 82, 1)" } } } }, "wrap_field_tag": "div" }, "rowMetaData": [], "rows": [], "type": "custom_widget", "w": 4, "x": 8 } }, { "0": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-module-14", "params": { "button_link": { "no_follow": false, "open_in_new_tab": false, "rel": "", "sponsored": false, "url": { "href": "#book" }, "user_generated_content": false }, "button_text": "Book a consultation", "child_css": {}, "css": {}, "css_class": "dnd-module", "path": "../modules/button", "schema_version": 2, "smart_objects": [], "smart_type": "NOT_SMART", "styles": { "alignment": { "alignment": { "css": "", "horizontal_align": "CENTER" } } }, "wrap_field_tag": "div" }, "rowMetaData": [], "rows": [], "type": "custom_widget", "w": 12, "x": 0 } } ], "type": "cell", "w": 12, "x": 0 } } ], "type": "cell", "w": 12, "x": 0 } }, "name": "My test page", "pageRedirected": false, "publicAccessRules": [], "publicAccessRulesEnabled": false, "publishDate": "2024-03-28T18:48:55Z", "publishImmediately": true, "published": true, "slug": "my-test-page", "state": "PUBLISHED_OR_SCHEDULED", "subcategory": "site_page", "templatePath": "@hubspot/growth/templates/paid-consultation.html", "translations": {}, "updatedAt": "2024-03-28T18:49:44.134Z", "updatedById": "2931299", "url": "https://www.website.com/my-test-page", "useFeaturedImage": false, "widgetContainers": {}, "widgets": {} } ] } ``` ## Filtering When retrieving pages, you can add query parameters to the request URL to filter the results. Each filter will follow the same general syntax: `propertyName__operator=value`. You can include as many filters as you'd like, and all specified filters will be applied to narrow down the results.For example, you can filter the results to only include pages where the name property contains the word `marketing` using this parameter: `name__icontains=marketing`.The table below lists the page properties that can be used as filters, along with which operators each property can use. | Property | Available operators | | --- | --- | | `id` | `eq`, `in`, `not_in` | | `slug` | `eq`, `in`, `nin`, `icontains` | | `campaign` | `eq`, `in` | | `state` | `eq`, `ne`, `in`, `nin`, `contains`See [available states](#state-values) for filtering options. | | `publishDate` | `eq`, `gt`, `gte`, `lt`, `lte` | | `createdAt` | `eq`, `gt`, `gte`, `lt`, `lte` | | `updatedAt` | `eq`, `gt`, `gte`, `lt`, `lte` | | `templatePath` | `eq`, `contains`, `startswith` | | `name` | `eq`, `in`, `icontains` | | `mabExperimentId` | `eq`, `in` | | `abTestId` | `eq`, `in` | | `archivedAt` | `eq`, `gt`, `gte`, `lt`, `lte` | | `createdById` | `eq` | | `updatedById` | `eq` | | `domain` | `eq`, `not_like`, `contains`This will display as blank for content published on a domain that is primary for that content type. | | `subcategory` | `eq`, `ne`, `in`, `nin` | | `folderId` | `eq`, `in`, `null`, `not_null` | | `language` | `in`, `not_null` | | `translatedFromId` | `null`, `not_null` | | `dynamicPageHubDbTableId` | `eq`, `not_null` | Below are the definitions for each operator, along with any available synonyms. | Operator | Description | | --- | --- | | `eq` | Filters for results that are equal to the specified value.Synonyms: `exact`, `is` | | `ne` | Filters for results that are not equal to the specified value.Synonyms: `neq`, `not` | | `contains` | Filters for results that contain the specified, case-sensitive value. Not accepted for the `name` property.Synonym: `like`. | | `icontains` | Filters for results by the specified value, not case sensitive. Synonym: `ilike`. | | `not_like` | Filters for results that don't contain a specified value.Synonym: `nlike` | | `lt` | Filters for results that are less than a specified number value, such as a unix timestamp in milliseconds. | | `lte` | Filters for results that are less than or equal to a specified value. | | `gt` | Filters for results that are greater than a specified value. | | `gte` | Filters for results that are greater than or equal to a specified value. | | `is_null` | Filters for results that do not have a value for the specified property. | | `not_null` | Filters for results that have any value for the specified property. | | `startswith` | Filters for results that start with a specified value. | | `in` | In | | `nin` | Not in | ### State values To retrieve pages with a given publish state, include the `state__in=` query parameter with the following values: **Draft:** `DRAFT`, `DRAFT_AB`, `DRAFT_AB_VARIANT`, `LOSER_AB_VARIANT` **Scheduled:** `PUBLISHED_OR_SCHEDULED`, `SCHEDULED_AB` **Published:** `PUBLISHED_OR_SCHEDULED`, `PUBLISHED_AB`, `PUBLISHED_AB_VARIANT` Because both scheduled and published pages will reflect a `PUBLISHED_OR_SCHEDULED` state, it's recommended to also include a `publishDate` parameter to better understand the page's current state. For example, to identify the pages that are currently scheduled but not yet published, your request URL should include a `publishDate` parameter that uses the `gt` (greater than) operator with the current datetime specified. This will filter for pages with a publish time in the future: `state__in=PUBLISHED_OR_SCHEDULED&publishDate__gt={currentTime}` To identify the pages that are already published, your request URL would similarly include a publishDate parameter, but you would use the `lt` (less than) operator with the current datetime specified. `state__in=PUBLISHED_OR_SCHEDULED&publishDate__lt={currentTime}` `currentState` is a generated field which cannot be used as a filter. This field will reflect the page's current publish state. ### A/B filters When filtering for A/B test pages, you can use the following filters: | Description | Query Params | | --- | --- | | Active A page or winning variant of a completed A/B test | `abStatus__eq=MASTER` | | Active B page | `abStatus__eq=VARIANT` | | Losing variant of a completed A/B test | `abStatus__eq=LOSER_VARIANT` | - An A/B test is running only when there are published, active A and B pages. - An A/B test is complete when a winning and losing variant have been selected, i.e. there isn’t an active B page. ### Multi-language filters When filtering for multi-language pages, you can use the following filters: | Description | Query Params | | --- | --- | | Primary page in a multi-language group | `translatedFromId__is_null` | | Variation page in a multi-language group | `translatedFromId__not_null` | | Page with specific Language\* (German) | `language__in=de`Does not support language locale values (e.g., `en-us`) | ## Sorting To sort results, you can add a `sort` query parameter to your request URL, followed by the property name. You can reverse the sort by adding a `-` to the property name. For example: `sort=-publishDate`.By combining query parameters for filtering, sorting and paging, you can retrieve the pages that match your search criteria. For example, the request below fetches landing pages that do not have a language assigned, ordered by most recently updated. The limit and offset parameters below return the second page of results. ```shell curl https://api.hubapi.com/cms/v3/pages/landing-pages?sort=-updatedAt&&language__not_null&limit=10&offset=10 \ --request POST \ --header "Content-Type: application/json" ``` ## Create pages To create a website page, make a `POST` request to `/cms/v3/pages/site-pages`. In the request body, you'll include a JSON payload that sets the page's details along with the page content contained in the `layoutSections` object. The required fields when creating a page are `name` and `templatePath`.For `templatePath`, the path should not include a slash (`/`) at the start. When using the _Copy path_ function in the design manager, this slash will be included automatically but should be removed after pasting it into your request body.To set the URL of a page, set the `domain` and `slug` fields. Note that the `url` field is generated and cannot be updated. Learn more about [creating and customizing pages within HubSpot](https://knowledge.hubspot.com/website-and-landing-pages/create-and-customize-pages). You can create the page as a draft so that it's not yet published by setting the `state` to `DRAFT`. See [available states](#state-values) for more information. ```json // Example request body { "domain": "", "htmlTitle": "My draft", "name": "My test page", "slug": "my-test-page", "state": "DRAFT", "templatePath": "@hubspot/growth/templates/paid-consultation.html", "url": "https://www.bird.estate/my-draft", "useFeaturedImage": false, "layoutSections": { "dnd_area": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "label": "Main section", "name": "dnd_area", "params": {}, "rowMetaData": [ { "cssClass": "dnd-section", "styles": { "backgroundColor": { "a": 1, "b": 250, "g": 248, "r": 245 }, "forceFullWidthSection": false } }, { "cssClass": "dnd-section", "styles": { "backgroundColor": { "a": 1, "b": 255, "g": 255, "r": 255 }, "forceFullWidthSection": false } } ], "rows": [ { "0": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-column-2", "params": { "css_class": "dnd-column" }, "rowMetaData": [ { "cssClass": "dnd-row" }, { "cssClass": "dnd-row" }, { "cssClass": "dnd-row" } ], "rows": [ { "0": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-module-3", "params": { "child_css": {}, "css": {}, "css_class": "dnd-module", "extra_classes": "widget-type-rich_text", ", "path": "@hubspot/rich_text", "schema_version": 2, "smart_objects": [], "smart_type": "NOT_SMART", "wrap_field_tag": "div" }, "rowMetaData": [], "rows": [], "type": "custom_widget", "w": 12, "x": 0 } }, { "0": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-column-4", "params": { "css_class": "dnd-column" }, "rowMetaData": [ { "cssClass": "dnd-row", "styles": {} } ], "rows": [ { "0": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-module-5", "params": { "child_css": {}, "css": {}, "css_class": "dnd-module", "extra_classes": "widget-type-linked_image", "horizontal_alignment": "CENTER", "img": { "alt": "Growth theme placeholder image", "loading": "lazy", "max_height": 500, "max_width": 500, "size_type": "auto_custom_max", "src": "//7528309.fs1.hubspotusercontent-na1.net/hubfs/7528309/raw_assets/public/mV0_d-cms-growth-theme_hubspot/growth/images/service-one.jpg" }, "path": "@hubspot/linked_image", "schema_version": 2, "smart_objects": [], "smart_type": "NOT_SMART", "style": "margin-bottom: 22px;", "wrap_field_tag": "div" }, "rowMetaData": [], "rows": [], "styles": { "flexboxPositioning": "TOP_CENTER" }, "type": "custom_widget", "w": 12, "x": 0 } } ], "type": "cell", "w": 6, "x": 0 }, "6": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-column-6", "params": { "css_class": "dnd-column" }, "rowMetaData": [ { "cssClass": "dnd-row" } ], "rows": [ { "0": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-module-7", "params": { "child_css": {}, "css": {}, "css_class": "dnd-module", "extra_classes": "widget-type-rich_text", "html": "<p>Add a short description about the work that you do at your company. Try to narrow in on your specialization so that you can capture the attention of your clients. Talk about the value that you can deliver through a paid consultation.</p>", "path": "@hubspot/rich_text", "schema_version": 2, "smart_objects": [], "smart_type": "NOT_SMART", "wrap_field_tag": "div" }, "rowMetaData": [], "rows": [], "type": "custom_widget", "w": 12, "x": 0 } } ], "type": "cell", "w": 6, "x": 6 } }, { "0": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-module-8", "params": { "button_link": { "no_follow": false, "open_in_new_tab": false, "rel": "", "sponsored": false, "url": { "href": "#book" }, "user_generated_content": false }, "button_text": "Book a consultation", "child_css": {}, "css": {}, "css_class": "dnd-module", "path": "../modules/button", "schema_version": 2, "smart_objects": [], "smart_type": "NOT_SMART", "styles": { "alignment": { "alignment": { "css": "", "horizontal_align": "CENTER" } } }, "wrap_field_tag": "div" }, "rowMetaData": [], "rows": [], "type": "custom_widget", "w": 12, "x": 0 } } ], "styles": {}, "type": "cell", "w": 12, "x": 0 } }, { "0": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-column-9", "params": { "css_class": "dnd-column" }, "rowMetaData": [ { "cssClass": "dnd-row", "styles": {} }, { "cssClass": "dnd-row" }, { "cssClass": "dnd-row" } ], "rows": [ { "0": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-module-10", "params": { "child_css": {}, "css": {}, "css_class": "dnd-module", "extra_classes": "widget-type-rich_text", ", "path": "@hubspot/rich_text", "schema_version": 2, "smart_objects": [], "smart_type": "NOT_SMART", "wrap_field_tag": "div" }, "rowMetaData": [], "rows": [], "type": "custom_widget", "w": 12, "x": 0 } }, { "0": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-module-11", "params": { "child_css": {}, "content": { "content": "<h3 style='text-align:center;'>Service</h3><p>Summarize the service you provide. Add a message about the value that you can provide to your customers.</p>" }, "css": {}, "css_class": "dnd-module", "icon": { "icon": { "icon_set": "fontawesome-5.0.10", "name": "balance-scale", "type": "SOLID", "unicode": "f24e" } }, "path": "../modules/service-card", "schema_version": 2, "smart_objects": [], "smart_type": "NOT_SMART", "styles": { "card": { "spacing": { "spacing": { "css": "margin-bottom: 22px;\n", "margin": { "bottom": { "units": "px", "value": 22 } } } } }, "icon": { "background": { "color": { "color": "#494a52", "css": "#494a52", "hex": "#494a52", "opacity": 100, "rgb": "rgb(73, 74, 82)", "rgba": "rgba(73, 74, 82, 1)" } } } }, "wrap_field_tag": "div" }, "rowMetaData": [], "rows": [], "type": "custom_widget", "w": 4, "x": 0 }, "4": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-module-12", "params": { "child_css": {}, "content": { "content": "<h3 style='text-align:center;'>Service</h3><p>Summarize the service you provide. Add a message about the value that you can provide to your customers.</p>" }, "css": {}, "css_class": "dnd-module", "icon": { "icon": { "icon_set": "fontawesome-5.0.10", "name": "industry", "type": "SOLID", "unicode": "f275" } }, "path": "../modules/service-card", "schema_version": 2, "smart_objects": [], "smart_type": "NOT_SMART", "styles": { "card": { "spacing": { "spacing": { "css": "margin-bottom: 22px;\n", "margin": { "bottom": { "units": "px", "value": 22 } } } } }, "icon": { "background": { "color": { "color": "#494a52", "css": "#494a52", "hex": "#494a52", "opacity": 100, "rgb": "rgb(73, 74, 82)", "rgba": "rgba(73, 74, 82, 1)" } } } }, "wrap_field_tag": "div" }, "rowMetaData": [], "rows": [], "type": "custom_widget", "w": 4, "x": 4 }, "8": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-module-13", "params": { "child_css": {}, "content": { "content": "<h3 style='text-align:center;'>Service</h3><p>Summarize the service you provide. Add a message about the value that you can provide to your customers.</p>" }, "css": {}, "css_class": "dnd-module", "icon": { "icon": { "icon_set": "fontawesome-5.0.10", "name": "server", "type": "SOLID", "unicode": "f233" } }, "path": "../modules/service-card", "schema_version": 2, "smart_objects": [], "smart_type": "NOT_SMART", "styles": { "card": { "spacing": { "spacing": { "css": "margin-bottom: 22px;\n", "margin": { "bottom": { "units": "px", "value": 22 } } } } }, "icon": { "background": { "color": { "color": "#494a52", "css": "#494a52", "hex": "#494a52", "opacity": 100, "rgb": "rgb(73, 74, 82)", "rgba": "rgba(73, 74, 82, 1)" } } } }, "wrap_field_tag": "div" }, "rowMetaData": [], "rows": [], "type": "custom_widget", "w": 4, "x": 8 } }, { "0": { "cells": [], "cssClass": "", "cssId": "", "cssStyle": "", "name": "dnd_area-module-14", "params": { "button_link": { "no_follow": false, "open_in_new_tab": false, "rel": "", "sponsored": false, "url": { "href": "#book" }, "user_generated_content": false }, "button_text": "Book a consultation", "child_css": {}, "css": {}, "css_class": "dnd-module", "path": "../modules/button", "schema_version": 2, "smart_objects": [], "smart_type": "NOT_SMART", "styles": { "alignment": { "alignment": { "css": "", "horizontal_align": "CENTER" } } }, "wrap_field_tag": "div" }, "rowMetaData": [], "rows": [], "type": "custom_widget", "w": 12, "x": 0 } } ], "type": "cell", "w": 12, "x": 0 } } ], "type": "cell", "w": 12, "x": 0 } } } ``` ## Editing pages Pages in HubSpot have both draft and live versions. The draft version may be updated without affecting the live page content. Drafts can be reviewed and then published by a user working in HubSpot. They may also be scheduled for publication at a future time via the `/schedule` endpoint. Draft changes can be discarded via the `/reset` endpoint, allowing users to go back to the current live version of the page without disruption. ### Edit Draft The draft version of a page can be updated via a `PATCH` request to the `{objectid}/draft` endpoint. This endpoint accepts a JSON payload representing the page model.Modifying the content of a page via these APIs is _not_ recommended -- the content editor in HubSpot is the simplest way to modify website content.You can modify the `widgets`, `widgetContainers`, and `layoutSections` properties via this endpoint. They store page-level module data, contained directly in the template (`widgets`), within flex columns (`widgetContainers`) and in drag-and-drop areas (`layoutSections`).The properties provided in the supplied payload will override the existing draft properties without any complex merging logic. Consequently, when updating nested properties such as those within the `widgets`, `widgetContainers`, or `layoutSections` of the page, you must include the full definition of the object. Partial updates are not supported. ### Reset Draft The draft version of a page can be reset to the current live version via a `POST` request to the `{objectId}/draft/reset` endpoint. This endpoint accepts no payload and simply reverts the draft version of the target page to match the current live version. ## Publishing pages Unpublished changes to a published page can be pushed live via a `POST` request to the `{objectId}/draft/push-live` endpoint. This endpoint accepts no payload and will only update an already published page, not publish a drafted page. ### Publish Draft (Scheduled) The draft version of a page can be scheduled for publication at a future time via a `POST` request to the `schedule` endpoint. This endpoint accepts a JSON payload containing the `id` of the target page and a `publishDate`. Learn more about [scheduling page publication.](https://knowledge.hubspot.com/website-pages/use-advanced-publishing-features) ## A/B testing A/B tests can be used to tune and optimize content by splitting traffic to a page across two variants of differing formats. Conversion rate of each page is monitored over time so that a winner can be chosen based on the relative success of each variant. Learn more about [running A/B tests in HubSpot](https://knowledge.hubspot.com/website-pages/run-an-a-b-test-on-your-page). The following properties will be populated on pages with active or completed A/B tests: - `abTestId`: Unique identifier for the page’s associated A/B test - `abStatus`: Indicates whether the page is an A/B primary, variant, or loser variant #### Create Test An A/B test variant can be created via a `POST` request to the `ab-test/create-variation` endpoint. This endpoint accepts a JSON payload containing the `contentId` of the page to test and a `variationName` for the variant to be created.Upon creation, the new test variant can be [updated](#editing-pages) and [published](#publishing-pages) in the same manner as a standard page. #### End Test Once enough traffic has been monitored across each variant and a clear winner has been determined, an A/B test can be terminated via a `POST` request to the `ab-test/end` endpoint. This endpoint accepts a JSON payload containing the `abTestId` of the target A/B test and the `winnerId` of the content deemed the winner. ## Multi-language content In HubSpot, you can group together language variants of the same content. Learn more about [working with multi-language pages in HubSpot](https://knowledge.hubspot.com/website-and-landing-pages/create-pages-in-multiple-languages#create-a-multi-language-variation-of-a-page). The following properties will be populated on pages within a multi-language group: - `translatedFromId`: unique identifier for the primary language page of the multi-language group. This property will be null on the primary page itself. - `language`: the ISO 639 code representing the language of the page. - `translations`: a map of ISO 639 codes to variant page objects within the multi-language group. #### Create a New Language Variant A new language variant of an existing page can be created via a `POST` request to the `multi-language/create-language-variant` endpoint. This endpoint accepts a JSON payload containing the `id` of the page to clone and the `language` identifier of the new variant. #### Attach a Page to an Existing Multi-Language Group A page can be added to an existing multi-language group via a `POST` request to the `multi-language/attach-to-lang-group` endpoint. This endpoint accepts a JSON payload containing the `id` of the target page, the `language` identifier of the page being added, and the `primaryId` of the page designated as the primary page in the target multi-language group. #### Detach a Page from a Multi-Language Group A page can be removed from a multi-languages group via a `POST` request to the `multi-language/detach-from-lang-group` endpoint. This endpoint accepts a JSON payload containing the `id` of the target page. # CMS API | Site search @ This endpoint is used to search for content across multiple domains $ guides & api/cms --- Site search allows you to return content related to a search term for HubSpot-hosted sites. Additionally, you can return all indexed data for a given document ID (page ID, post ID, HubDB row ID, etc.). If Google is used to search the entire web, site search indexes an entire website and produces search results in an easy-to-read list. From ecommerce to news outlets, this functionality is commonplace and useful in many different types of applications. **Example use case:** Set up a results page to use a search field on your HubSpot-hosted site. See [knowledge article](https://knowledge.hubspot.com/cos-general/how-do-i-set-up-a-results-page-for-my-search-field-in-hubspot) for more information. **Note**: The search endpoint is used to search content across multiple domains. # CMS | Source Code API @ Create, fetch, update, and delete, files in the HubSpot developer file system. Additionally you can validate files for different types of errors & warnings $ guides & api/cms --- # CMS Source Code ## Basic Features The CMS Source Code API allows you to interact with the files stored in your HubSpot [Developer File System](/guides/cms/overview#developer-file-system). These files include all of the templates, modules, CSS, JS, and other CMS assets seen in the Design Manager. Through this API, you can: - Upload new files to their HubSpot account, or upload changes to existing files. - Download or delete CMS assets from their account. - Fetch metadata for each file or folder in the Design Manager. - Validate file contents for use in HubSpot CMS, including HubL syntax. - Upload zip files as file packages and extract them inside the account. With this API you can edit all your CMS assets locally as well as build tooling to help you move even faster. ## Environment and path The Source Code API endpoints use the `environment` and `path` parameters to identify files in the CMS Developer File System. These parameters are generally specified in the endpoint path itself, as in `/cms/v3/source-code/{environment}/content/{path}`. The `environment` parameter refers to whether you are interacting with the unpublished or live version of each file. For unpublished changes, use `draft`. For live changes, use `published`. Note that uploading to `published` is equivalent to pressing the “Publish” button in the Design Manager. As such, whatever is currently in `draft` will be cleared. The `path` parameter refers to the location of the file in the CMS Developer File System. Top level assets are not preceded by a `/` character as you would see in a UNIX based operating system. When uploading a new file, this should be the desired location where the file should be created. When retrieving or uploading changes to an existing file, this should match the path of the existing file. We use the local file formats for all asset types, which means that certain types of assets are broken up into multiple files. For instance, a module is actually represented as a directory ending in the `.module` suffix, so to retrieve the HTML for a module, one would need to use the path `foo.module/module.html`. See the [local development documentation](/guides/cms/tools/local-development-cli) for further information. ## Downloading a file To download a file from your HubSpot account, make a `GET` request to `/cms/v3/source-code/{environment}/content/{path}` and set the header to `Accept: application/octet-stream`. File data will be downloaded in binary format. You cannot download the entire contents of a folder. Instead, you must fetch the folder metadata and retrieve each of its children individually. ## Fetching file and folder metadata To fetch file and folder metadata, such as path, filename, and created/updated timestamps, make a `GET` request to `/cms/v3/source-code/{environment}/metadata/{path}` and set the header to `Accept: application/json`. File and folder metadata will be returned in a JSON object: - Folder metadata will be indicated by the `folder: true` property. - The `children` array will show the names of files and subfolders within the folder. These filenames can be used to traverse the folder tree: simply fetch one folder metadata and recursively fetch the children of the folder and all subfolders. ## Uploading a file To upload a local file to your HubSpot account, make a `PUT` request to `/cms/v3/source-code/{environment}/content/{path}`. You must upload the file using the `multipart/form-data` content type, and the binary file data must be included as a field named `file`. For example: - **Uploading a new file:** PUT `/cms/v3/source-code/published/content/my-new-file.html` `Content-Type: multipart/form-data` `Form Data: { file: [_binary file data_] }` - **Updating an existing file draft:** PUT `/cms/v3/source-code/draft/content/path/to/existing-file.html` `Content-Type: multipart/form-data` `Form Data: { file: [_binary file data_] }` HubSpot currently supports the following file types: - `css` - `js` - `json` - `html` - `txt` - `md` - `jpg` - `jpeg` - `png` - `gif` - `map` - `svg` - `ttf` - `woff` - `woff2` - `zip` ## Validating file contents To validate the contents of a local file, make a `POST` request to `/cms/v3/source-code/{environment}/validate/{path}`. You must upload the file using the `multipart/form-data` content type, and the binary file data must be included as a field named `file`. This can be used to validate HubL in a template/module or JSON for a theme or module. If there are validation errors, you will receive a `400` response with the list of relevant errors. These are the same warnings and errors you would see within the design manager. Note that invalid files will be rejected if you try and publish them directly. It is recommended to validate files first before publishing. **For example:** POST `/cms/v3/source-code/published/validate/my-file.html` `Content-Type: multipart/form-data` `Form Data: { file: [_binary file data_] }` ## Deleting a file To delete a file, make a `DELETE` request to `/cms/v3/source-code/{environment}/content/{path}`. Deleting from the `published` environment will remove the file entirely, while deleting from the `draft` environment will simply clear out any unpublished changes. Note that deleting published files will immediately impact live content if used anywhere, so make sure to remove all existing references to the file before deleting. ## Extracting a file package To extract a zip file, make a `POST` request to `/cms/v3/source-code/extract/{path}`. The `path` must be a zip file already uploaded to the account. The extraction process is asynchronous and can take up to a minute depending on how many and how large the compressed files are. The contents of the zip are extracted in place to the same folder that contains the zip file, and the original zip file is not deleted automatically upon successful extraction. # CMS API | URL redirects @ This endpoint can be used to manage URL redirects for a portal $ guides & api/cms --- URL redirects allow you to redirect traffic from a HubSpot-hosted page or blog post to any URL. You can also update [URL redirects in bulk](https://knowledge.hubspot.com/articles/kcs_article/cos-general/how-to-bulk-upload-url-mappings-to-hubspot) and use a [flexible pattern redirect](https://knowledge.hubspot.com/cos-general/how-do-i-set-up-a-flexible-pattern-url-mapping) to dynamically update the structure of URLs. # Custom channels @ Learn how to define custom channels in the conversations inbox and help desk. $ guides & api/conversations --- # Custom channels You can create custom channels to build a bridge between an external message service and HubSpot's [inbox](https://knowledge.hubspot.com/inbox/set-up-the-conversations-inbox) or [help desk](https://knowledge.hubspot.com/help-desk/overview-of-the-help-desk-workspace) features. You can then publish your custom channel app in the HubSpot App Marketplace for other HubSpot admins to install and use in their own accounts. You'll need to create a public app if you want to define and register custom channels. The custom channel endpoints are **not** supported when building a private app. ## Create a public app To get started, you'll need to create a public app that you'll use to register your custom channel with HubSpot, then handle incoming and outgoing messages to an inbox. You can follow the instructions in [this article](/guides/apps/public-apps/overview) to create your public app. When configuring the scopes for your app on the _Auth_ tab, ensure that your app requests the following scopes: - `conversations.custom_channels.read`: this scope allows you to access to read messages and threads that your custom channel has already published to HubSpot. - `conversations.custom_channels.write`: this scope allows you to publish messages to HubSpot. - `conversations.read`: this scope allows you to retrieve a list of conversation inboxes in an account using the [Conversations BETA API](/guides/api/conversations/inbox-and-messages#inboxes), so that you can connect channel accounts to specific inboxes. Once you've created your app, take note of the following app details that you'll use to identify your app when making requests to the Custom Channels API endpoints. - App ID - Client ID - Client secret ## Register your custom channel in HubSpot After creating your app, you can create your custom channel in HubSpot, using your HubSpot Developer API Key, and your App ID of the app you just created. To register your channel, make a `POST` request to `https://api.hubapi.com/conversations/v3/custom-channels?hapikey={YOUR_DEVELOPER_API_KEY}&appId={appId}`. In the body of your request, you'll specify the configuration and capabilities for your channel: - Specify the user-facing channel name as the value for the `name` field. - Provide a channel schema that includes an object that details the channel capabilities as the value for the `capabilities` field. - Certain fields are optional and can be updated later using a `PATCH` request: - You can also specify a `webhookUrl` for handling webhook events (e.g., handling [updates to channel accounts](#handling-updates-to-channel-accounts)). - You can include a `channelDescription` and `channelLogoUrl` that will appear in the setup flow for your channel. - Specify a redirect URL that users will be sent to after completing your setup flow with the `channelAccountConnectionRedirectUrl` field. For example, the following request body provides an example channel schema with a set of basic channel capabilities: ```json // Example request body for registering a custom channel { "name": "My new custom channel", "webhookUrl": "https://example.com/handle-new-outgoing-message-notification", "capabilities": { "deliveryIdentifierTypes": [], "richText": ["HYPERLINK", "TEXT_ALIGNMENT", "BLOCKQUOTE"], "allowInlineImages": false, "allowOutgoingMessages": false, "outgoingAttachmentTypes": ["FILE"], "allowedFileAttachmentMimeTypes": ["image/png"], "maxFileAttachmentCount": 1, "maxFileAttachmentSizeBytes": 1500000, "maxTotalFileAttachmentSizeBytes": 1500000, "threadingModel": "INTEGRATION_THREAD_ID" }, "channelAccountConnectionRedirectUrl": "https://example.com/path-back-to-your-connection-flow", "channelDescription": "Respond to prioritized messages via our custom channel", "channelLogoUrl": "https://example.com/logo.png" } ``` ### Channel schema The table below outlines each of the available parameters for the channel schema in the request body. | Parameter | Type | Description | | --- | --- | --- | | `name` | String | The name of your channel, which will appear to users who install your app. | | `webhookUrl` | String | A valid URL where HubSpot will send webhook notifications of outgoing messages that should be sent using your channel. This field is not required when initially registering the channel. | | `capabilities` | Object | An object that specifies that the messaging content and recipient capabilities of your channel. Consult the table below for all supported fields for this object. | | `channelAccountConnectionRedirectUrl` | String | If you're [creating a connection setup flow for your custom channel](#creating-a-connection-setup-flow-for-your-custom-channel), this is the URL that HubSpot will point to when users click the option to install your app in the inbox or help desk connection setup in HubSpot. This field is not required when initially registering the channel. | | `channelDescription` | String | A string that will provide context under the channel when connecting it to HubSpot. This field is not required when initially registering the channel. See the [section below](#creating-a-connection-setup-flow-for-your-custom-channel) on defining a setup flow for your custom channel. | | `channelLogoUrl` | String | A URL that points to a logo that appear for your channel when connecting it to HubSpot. This field is not required when initially registering the channel. See the [section below](#creating-a-connection-setup-flow-for-your-custom-channel) on defining a setup flow for your custom channel. | ### Channel capabilities The table below outlines all available fields you can specify for the `capabilities` field of your channel. | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `deliveryIdentifierTypes` | Array | A required array that specifies the identifier types your channel supports for its senders and recipients. Consult the delivery identifier types table below for more info. | `[]` | | `richText` | Array | An optional array of supported rich text elements presented as options in HubSpot's message composer. Available values are `"BLOCKQUOTE"`, `"BOLD"`, `"FONT_SIZE"`, `"FONT_STYLE"`, `"HYPERLINK"`, `"ITALIC"`, `"LISTS"`, `"TEXT_ALIGNMENT"`, `"TEXT_HIGHLIGHT_COLOR"`, `"TEXT_COLOR"`, and/or `"UNDERLINE"`. If left unspecified, no rich text elements will be supported. | `[]` | | `allowInlineImages` | Boolean | Whether your channel allows inline images to be included in the message body. If set to `true`, HubSpot's message composer will present an _Insert image_ option. | `False` | | `allowOutgoingMessages` | String | Whether your channel allows outgoing messages. You can set this field to true if you want your channel to be an "intake-only" message source. | `False` | | `outgoingAttachmentTypes` | Array | The types of attachments your channel supports (valid values are `"FILE"` and `"QUICK_REPLIES"`). | `[]` | | `allowedFileAttachmentMimeTypes` | Array | The file MIME types that your channel supports for file attachments, if any. This defaults to a fairly permissive list of common text, image, audio, video, and archive files. | `["image/png", "text/plain", ...]` | | `maxFileAttachmentCount` | Integer | The maximum number of file attachments permitted for a single message. | `0` | | `maxFileAttachmentSizeBytes` | Integer | The maximum individual file size permitted for a single attachment on a message, in bytes. | `0` | | `maxTotalFileAttachmentSizeBytes` | Integer | The maximum cumulative file size permitted for all attachments combined for a single message, in bytes. | `0` | | `threadingModel` | String | An enumeration denoting which threading model to use for messages. Values are: `INTEGRATION_THREAD_ID` and `DELIVERY_IDENTIFIER`. See the section below for more information. | `INTEGRATION_THREAD_ID` | ### Specifying a threading model By default, the threading model of your custom channel is set to `INTEGRATION_THREAD_ID`, where you're required to include an `integrationThreadId` in every `POST` [message request](#incoming-message-schema). This ID will always be returned in the `channelIntegrationThreadIds` property of the `OUTGOING_CHANNEL_MESSAGE_CREATED` [webhook event](#handling-outgoing-messages-sent-from-hubspot). You can optionally set your threading model to `DELIVERY_IDENTIFIER`, which is a good fit when there is only ever one open coversation between parties. If you set the `threadingModel` to this value within the `capabilities` [object](#channel-capabilities) of your channel, you should _not_ include an `integrationThreadId` in any of your `POST` [message requests](#incoming-message-schema), because HubSpot will generate and manage its own `threadIds` based on the following rules: - A set of `deliveryIdentifiers` (e.g., email address, phone numbers, or other identifiers in the sender and/or recipients array of a message) may have a maximum of one active thread between these delivery identifiers at once. - Archived threads between that same set of participants are ignored. - Outgoing messages can only be published on open threads, adn there can only be one thread between a specific set of participants at a time. - Incoming messages can only be published to the same `threadId` if either of the following conditions are met: - The thread is already open. - The thread was closed with the latest message activity occurring less than 24 hours ago, in which case the published incoming message will re-open the thread. - The `channelIntegrationThreadIds` property within the payload of the `OUTGOING_CHANNEL_MESSAGE_CREATED` [webhook event](#handling-outgoing-messages-sent-from-hubspot) will contain the value of the `threadId` that HubSpot created. ### Delivery identifiers The `deliveryIdentifierTypes` field specified within the `channelCapabilities` of your channel specifies an array of delivery identifiers. A delivery identifier is an object that includes a `type` and `value`, that describes sender or recipient delivery details: | Parameter | Type | Description | | --- | --- | --- | | `type` | String | The format for an identifier of the sender or recipient. Much like a postal mail address, this identifier provides a well-defined format for sending or receiving messages to/from the individual contact. The following types are available:<ul><li>`HS_EMAIL_ADDRESS`: identifier value must have a valid email address.</li><li>`HS_PHONE_NUMBER`: identifier value must be a valid phone number, based on the logic in libphonenumber.</li><li>`CHANNEL_SPECIFIC_OPAQUE_ID`: identifier value does not require any format validation to be performed, except to check it isn't blank. This option is intended for use in channels with their own concept of "addresses" (e.g., usernames, UUIDs, or chat handles).</li></ul> | | `value` | String | The actual value of the delivery identifier (e.g., "support@example.com" for an `HS_EMAIL_ADDRESS`). | If you need to review or update your channel's configuration, you can send a `GET` or `PATCH` request to `/conversations/v3/custom-channels?hapikey={YOUR_DEVELOPER_API_KEY}&appId={appId}&channelId={channelId}` using the same channel schema. `PATCH` requests also accept partial-update requests. ## Connect channel accounts to HubSpot After you've created your custom channel, the next step is to allow specific channel accounts to be connected to a user's HubSpot account, so that messages can be sent and received. ### Channel account connection for specific accounts If you're developing a custom channel for your own business, or building a specific integration on behalf of a client, you can connect channel accounts directly to a HubSpot account. You'll need to have direct administrative access to the account or have an app installed in an account that's authorized the `conversations.read` scope. When a HubSpot user installs your custom channel app in their account, a request will be sent to your custom channel's _Redirect URL_, which contains an authorization code that can be used to request an OAuth token for accessing that user's HubSpot account and its associated data. For more information on how OAuth works, check out [this article](/guides/apps/authentication/working-with-oauth). Once you've obtained an OAuth access token using the authorization code, you can use this OAuth access token to send requests to HubSpot to connect specific channel accounts. To connect a specific channel account, make a `POST` request to `/conversations/v3/custom-channels/{channelId}/channel-accounts`, where the `channelId` is the channel ID that you've registered. The request body below provides an example channel account schema for an inbox ID of `123`: ```json // Example request body to connect a specific channel account { "inboxId": "123", "name": "My connected inbox", "deliveryIdentifier": { "type": "HS_EMAIL_ADDRESS", "email": "jdoe@example.com" }, "authorized": true } ``` The table below provides details for each of the possible fields of a channel account: | Parameter | Type | Description | | --- | --- | --- | | `inboxId` | String | The ID of the [conversations inbox](https://knowledge.hubspot.com/inbox/use-the-conversations-inbox) that you've connected your channel account to. You can retrieve a list of conversation inboxes with their associated IDs by using the [conversations beta API](/guides/api/conversations/inbox-and-messages#inboxes). | | `name` | String | The friendly name that will appear to HubSpot users in their account. | | `deliveryIdentifier` | String | A required delivery identifier that should be associated with this channel account (e.g., a username or email address that uniquely identifies this channel account within your integrated messaging service). | | `authorized` | Boolean | By default this field is set to `true`, but if the account is no longer usable or accessible, you can update this to `false` to disable this channel account for your custom channel. | ### Creating a connection setup flow for your custom channel If you're developing a custom channel integration that you want to distribute to multiple customers via the HubSpot marketplace, you can register an account connection flow that will appear within the in-app HubSpot inbox or help desk connection flow. The screenshot below showcases how an example custom channel called _HuddleHub_ would appear in the help desk connection flow in HubSpot. Your app's name, description, and logo from your public app settings will appear as part of the custom channel option that users can click to connect. , then proceed to connect that account to HubSpot. When users click the associated option in HubSpot, a pop-up window will open and load a webpage that allows them to provide their credentials. You'll need to host this webpage and any associated backend services yourself. The screenshot below provides a minimal example of what this dialog box looks like and includes some debugging information based on the query parameters that HubSpot includes in the URL. When designing your own page, you should optimize it for a 600px by 600px window, and provide a user-friendly method (e.g., a form) to provide any account details and [delivery identifier information](#delivery-identifiers) you'll need to connect their account. If you didn't specify a redirect URL for your connection flow when you [initially registered your channel](#register-your-custom-channel-in-hubspot), you can update the behavior of your channel by making a `PATCH` request to `/conversations/v3/custom-channels/{channelId}`. In the body of your request, set the `channelAccountConnectionRedirectUrl` property to the URL that you want HubSpot to navigate to when users click your custom channel in the connection flow. You can also provide a `channelDescription` and `channelLogoUrl` if you didn't specify these fields previously or you want to update them. Once clicked, HubSpot will open a new window with the URL you specified, and append the following query parameters that you can use to authenticate with a third-party service, or for any other custom behavior you might need to wire up based on the user's HubSpot account, inbox, channel, or user information: | Parameter | Type | Description | | --- | --- | --- | | `accountToken` | String | A shared object created in HubSpot's database that you'll use as an interim session token while the end user goes through the setup flow.<br /><br />Once they've provided all the parameters you'll need to connect their account, you'll use this token to make a `PATCH` request to `/conversations/v3/custom-channels/{channelId}/channel-account-staging-tokens/{accountToken}`, detailed in the section below this table. | | `channelId` | Number | The ID of your custom channel. | | `inboxId` | Number | The ID of the help desk or inbox that the HubSpot admin wants to connect your channel to. | | `portalId` | Number | The ID of the HubSpot account attempting to connect their account to your custom channel. | | `redirectUrl` | String | The HubSpot URL that you should send the user to when your flow and/or form submission is complete. | Once you've collected the user's information in your page (e.g., using a form to collect each of the required parameters) and the user confirms their input by clicking a submission button, you can make a `PATCH` request to `/conversations/v3/custom-channels/{channelId}/channel-account-staging-tokens/{accountToken}` and provide the following parameters in your request: | Parameter | Type | Description | | --- | --- | --- | | `accountToken` | String | As detailed above, this is the same `accountToken` provided as a query parameter in the URL of your webpage when the user initiated the connection in their HubSpot account. This should be provided as a path parameter in the URL of your request. | | `channelId` | Number | The ID of your custom channel, provided as a path parameter in the URL of your request. | | `accountName` | String | This is a request body parameter that will act as a friendly name for the channel account that will appear in the inbox or help desk settings in the user's HubSpot account.<br /> | | `deliveryIdentifier` | DeliveryIdentifier | Provided as a request body parameter, the `deliveryIdentifier` that describes the address of the connected account. See the [Delivery Identifiers section above](#delivery-identifiers) for more details on how a `deliveryIdentifier` is structured. | Once you've made the `PATCH` request and gotten a successful response, you can redirect the user back to the `redirectUrl` detailed above and close the pop-up window. The user will then see a success screen in their HubSpot account: ## Publish received messages to HubSpot When a channel account that uses your custom channel receives a message in the external message service (e.g., a user gets a new direct message in their social account's inbox), you'll want to ensure that the message gets published to HubSpot, which will log it into the HubSpot CRM and the associated conversations inbox. To publish a received message to HubSpot, make a `POST` request to `/conversations/v3/custom-channels/{channelId}/messages`, where the `channelId` should be the channel ID that you previously registered. ### Incoming message schema The body of your request can include the following fields: | Field | Type | Description | | --- | --- | --- | | `text` | String | The plaintext content of your message. | | `richText` | String | An optional string that represents the HTML markup for the formatted text content of the message. | | `channelAccountId` | String | The ID of the channel account receiving the message. | | `integrationThreadId` | String | A required string based on the `threadingModel` you [specified](#specifying-a-threading-model). If you specified the `threadingModel` for your channel to be `INTEGRATION_THREAD_ID`, then you'll need to provide the `integrationThreadId` for this field. If your `threadingModel` is set to `DELIVERY_IDENTIFIER`, this field must be set to `null`. | | `integrationIdempotencyId` | String | An optional string you can use as a unique-per-message idempotency ID, to ensure that multiple requests to publish this message don't result in duplicate copies of it being published.<br /><br />The idempotency ID is only unique in the context of a channel account: the same channel account cannot have two messages with the same idempotency ID. However, two different channel accounts can have messages that have the same idempotency ID. | | `inReplyToId` | String | An optional string you can use to reference the message ID of an existing message that this message is a direct reply to. | | `messageDirection` | String | A required string that should be set to `"INCOMING"`. | | `senders` | Array | A list of messages senders. Each sender should be provided as an object with two fields: a `deliverIdentifier`, detailed [above](#delivery-identifiers), and a `name`, which is an optional string that serves as a friendly name for the sender. | | `recipients` | Array | A list of intended recipients. Each recipient should be provided as an object with two fields: a `deliveryIdentifier`, detailed [above](#delivery-identifiers), and a `name`, which is an optional string that serves as a friendly name for the recipient. | | `timestamp` | String | An optional timestamp, in ISO8601 format, that specifies when the message was received. If omitted, this value will default to the time you publish the message to HubSpot. | | `attachments` | Array | An optional list of message attachments. | ### Supported message attachment types Several message attachment types are currently available: files, quick replies, as well as a catch-all type for unsupported file attachments. #### File attachments File attachments can be uploaded using HubSpot's files API, and the resulting file ID can be used to reference the file contents when publishing incoming messages to HubSpot. Consult the table below for details on how to structure a message attachment referencing an uploaded file: | Parameter | Type | Description | | --- | --- | --- | | `type` | String | A required string that must be set to `"FILE"`. | | `fileId` | String | The HubSpot file ID for the uploaded file that the attachment will reference. | | `fileUsageType` | String | An enumeration used to render an appropriate preview of the file attachment. Available values are `"STICKER"`, `"VOICE_RECORDING"`, `"IMAGE"`, `"AUDIO"`, or `"OTHER"`. | #### Quick reply attachments Some chat services, such as Facebook Messenger, present suggested options as "quick replies" to users, allowing them to select from a predefined list of response messages. HubSpot supports this functionality by providing the ability to include quick replies as an attachment to your message. Consult the table below on how to structure a message attachment that includes a list of quick replies: | Parameter | Type | Description | | --- | --- | --- | | `type` | String | A required string that must be set to `"QUICK_REPLIES"`. | | `quickReplies` | Array | A list of one or more predefined responses. Each quick reply is an object that should include the following fields:<ul><li>`value`: a string that provides the text content of the reply, or the URL to visit when the option is clicked.</li><li>`label`: an optional string that will appear instead of the raw `value` when presenting the option to the user.</li><li>`valueType`: whether to treat the option as a hyperlink (with the value as the URL and the label as text) or as plaintext (in which case the `label` is used as the option text).</li></ul> | #### Unsupported content attachments These attachments are used to represent attachment types included in the original message that HubSpot cannot recognize. For example, if a channel account on the messaging platform you're connecting to HubSpot via your channel receives a message that includes an attachment not listed in this article, and you want users viewing the message in their HubSpot account to know there was an attachment they might want to view directly on the messaging platform, you could use this attachment type when publishing the message to HubSpot. To specify an unsupported content attachment, the attachment should be specified as follows: ```json // Example attachment schema for unsupported content { "type": "UNSUPPORTED_CONTENT" } ``` ## Handling outgoing messages sent from HubSpot To handle outgoing messages sent from HubSpot using your custom channel, HubSpot will send a payload as a `POST` request to the `webhookUrl` that you specified when you first registered your custom channel. The payload of the request that HubSpot sends will include a type of `OUTGOING_CHANNEL_MESSAGE_CREATED`. The table below provides a full reference on all fields included in the body of the webhook event: | Parameter | Type | Description | | --- | --- | --- | | `type` | String | A string that specifies the webhook event type, which will be `"OUTGOING_CHANNEL_MESSAGE_CREATED"`. | | `portalId` | String | The ID of the HubSpot account from which the message was sent. | | `channelId` | String | The ID of the custom channel. | | `eventTimestamp` | String | When the event occurred, represented as an ISO8601 timestamp. | | `message` | String | An object that provides details about the outgoing message. Each of the fields of this message are detailed in the message table below. | | `eventId` | String | Unique reference to a specific message over a given channel used for debugging. | | `channelIntegrationThreadIds` | Array | The value for this field is based on the [threading model](#specifying-a-threading-model) you specified for your channel. The array includes an `integrationThreadId` that was sent if you opted for a `threadingModel` of `INTEGRATION_THREAD_ID` for your channel. If you specified a `threadingModel` of `DELIVERY_IDENTIFIER`, this array will include a HubSpot-generated value. | The `message` object in the webhook payload contains the following fields: | Field | Type | Description | | --- | --- | --- | | `id` | String | A unique message ID | | `type` | String | A string that's hard-coded to `"MESSAGE"`. | | `channelId` | String | The ID of your custom channel. | | `channelAccountId` | String | The ID of the channel account receiving the message. | | `conversationsThreadId` | String | The ID of the thread in the conversations inbox that includes this message. | | `createdAt` | String | An ISO8601 timestamp representing the time when the user hit **Send** from within the HubSpot message composer. | | `createdBy` | String | A string representing the [actorId](/guides/api/conversations/inbox-and-messages#get-actors) of the HubSpot user who sent the message. | | `senders` | Array | A list of messages senders. Each sender is provided as an object with three fields: an [actorId](/guides/api/conversations/inbox-and-messages#get-actors) string, a `deliverIdentifier`, detailed [above](#delivery-identifiers), and a `name`, which is a string that serves as a friendly name for the sender. | | `recipients` | Array | A list of intended recipients. Each recipient is provided as an object with three fields: an [actorId](/guides/api/conversations/inbox-and-messages#get-actors) string, a `deliveryIdentifier`, detailed [above](#delivery-identifiers), and a `name`, which is an optional string that serves as a friendly name for the recipient. | | `text` | String | The plaintext content of your message. | | `richText` | String | An optional string that represents the HTML markup for the formatted text content of the message. | | `direction` | channelAccountId | `"OUTGOING"` | | `inReplyToId` | String | An optional string you can use to reference the message ID of an existing message that this message is a direct reply to. | | `truncationStatus` | String | `"NOT_TRUNCATED"` | | `status` | String | `"SENT"` | | `attachments` | Array | An optional list of message attachments. | ## Handling updates to channel accounts Whenever a channel account is connected, updated, or deleted, HubSpot will send a payload as a `POST` request to the `webhookUrl` that you specified when you first registered your custom channel. This can be useful for cases where you want to take an action such as notifying your users that their connection to HubSpot has succeeded, or if the channel is later modified or deleted. ### Channel account webhook event payload The payload of the request that HubSpot sends will includes the fields listed in the table below. The event's type will be provided in the `type` field: - `CHANNEL_ACCOUNT_CREATED`: triggered when an admin successfully connected a custom channel to an inbox or help desk in their HubSpot account. - `CHANNEL_ACCOUNT_UPDATED`: triggered when a HubSpot user updates a channel account that uses one of your connected custom channels. - `CHANNEL_ACCOUNT_PURGED`: triggered when a HubSpot user deletes a channel account that uses one of your connected custom channels. | Parameter | Type | Description | | --- | --- | --- | | `type` | String | A string that specifies the webhook event type. | | `portalId` | String | The HubSpot portal ID associated with the custom channel. | | `channelId` | String | The channel ID of your custom channel. | | `eventTimestamp` | String | When the event occurred, represented as an ISO8601 timestamp. | | `channelAccountId` | String | The ID of the custom channel account. | | `channelAccountDeliveryIdentifier` | String | The delivery account identifier that's associated with the channel account. | ## Archive a custom channel To archive a custom channel, make a `DELETE` call to `/conversations/v3/custom-channels/{channelId}` using the ID of the channel you want to archive as the `channelId`. If successfully archived, you'll receive a response of `204 No content`. # Conversations inbox and messages APIs (BETA) @ The conversations APIs available for inboxes, channels, threads, and messages allow you to retrieve message information, as well as post new messages. $ guides & api/conversations --- The conversations APIs enable you to manage and interact with the conversations inbox, channels, and messages. For example, you can use these APIs to: - Get and sort conversations inboxes, channels, threads, and messages. - Update thread statuses. - Delete and restore threads. - Send outbound messages via existing conversations channels. - Send an internal comment to an agent. - Retrieve conversation data to create advanced reports and analytics in external tools. You could also use these APIs to integrate existing channels with other apps such as [Slack](https://knowledge.hubspot.com/integrations/how-do-i-use-the-slack-integration) or Microsoft Teams to send replies or receive notifications. To get started with the conversations APIs, make sure you have [set up a HubSpot developer account](https://developers.hubspot.com/get-started) and [created an app](/guides/apps/public-apps/overview). You will need a developer account ID and app ID to get access to these APIs. You can also use webhooks with the conversations API. Learn more about the [available webhook events](#webhooks). If you're planning on defining a custom channel that users can connect to their HubSpot account, check out the [custom channels API](/guides/api/conversations/create-custom-channels). To view all available endpoints and their requirements, check out the [reference documentation](/reference/api/conversations/inbox-and-messages). To make a `GET` request to any endpoints, or `POST` batch read request to the get actors endpoint, you must have `conversations.read` access. All other endpoints require `conversations.write` access. ## Filter and sort results When retrieving inboxes, channels, channel accounts, threads, and messages using the endpoints outlined in this article, you can use different query parameters to filter and sort your responses. | Parameter | Type | Description | | --- | --- | --- | | `sort` | String | Set the sort order of the response. You can sort by multiple properties. | | `after` | String | The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. | | `limit` | Integer | The maximum number of results to display per page. | You can also sort your results by any field on the channel, channel accounts, or inbox objects. For example, you could sort inboxes by name, or channel accounts by both channel ID and name. ## Inboxes To retrieve a list of inboxes set up in your account, make a `GET` request to `conversations/v3/conversations/inboxes`. When you make a successful request, the response will include the inbox IDs of the different inboxes set up in your account. To retrieve details about a specific inbox, use the inbox ID to make a `GET` request to `conversations/v3/conversations/inboxes/{inboxId}`. ## Channels You can retrieve a list of the channels connected to your inboxes by making a `GET` request to `conversations/v3/conversations/channels`. When you make a successful request, the response will include the channel IDs for the different channels connected to your inbox. To retrieve a specific channel, use the channel ID to make a `GET` request to `conversations/v3/conversations/channels/{channelId}`. You can also retrieve channel accounts, which are instances of a channel that are used to send and receive messages, like a specific chatflow created for one of your chat channels or a team email address connected as an email channel. To retrieve a list of channel accounts, make a `GET` request to `conversations/v3/conversations/channel-accounts`. You can limit the results by including the following parameters in the request URL: | Parameter | Description | | --- | --- | | `channelId` | The ID of the channel type for which you're retrieving a channel instance. | | `inboxId` | The ID of the inbox that the channel is connected to. | For example, your request may look similar to the following: `https://api.hubspot.com/conversations/v3/conversations/channel-accounts?channelId=1000&inboxId=448`. When you make a successful request, the response will include an ID number, which is the channel account ID. You can use the channel account ID to retrieve a single channel account, such as a specific chatflow. To retrieve a specific channel account, make a `GET` request to `conversations/v3/conversations/channel-accounts/{channelAccountId}`. ## Threads & messages ### Retrieve threads Threads are a group of related messages that make up a conversation in the inbox. To retrieve a list of all threads in your conversations inbox, make a `GET` request to `conversations/v3/conversations/threads`. To filter and search your results, you can use the following query parameters in your request: | Parameter | Type | Description | | --- | --- | --- | | `inboxId` | Integer | Filter threads by a specific inbox ID. Note that you can only filter a single inbox ID (multiple instances of this query parameter are _not_ supported). | | `archived` | Boolean | To retrieve archived threads, use the value `true`. | | `sort` | String | Set the sort order of the response. Valid options include `id` , which is the default, and `latestMessageTimestamp` , which requires the `latestMessageTimestampAfter` field to also be set. Results are always returned in ascending order. | | `after` | String | The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. Use this parameter when sorting by ID. | | `latestMessageTimestampAfter` | String | The minimum `latestMessageTimestamp`. This is required only when sorting by `latestMessageTimestamp`. | | `limit` | Integer | The maximum number of results to display per page. | When you make a successful request, the response will include the thread ID, which you can use to retrieve, update, or create a new message in a thread. To retrieve a specific thread by thread ID, make a `GET` request to `conversations/v3/conversations/threads/{threadId}`. ### Retrieve messages To get the entire message history, make a `GET` request to `conversations/v3/conversations/threads/{threadId}/messages`. When you make a successful request, the response will include the message ID, which you can use to retrieve a specific message. To do so, make a `GET` request to `conversations/v3/conversations/threads/{threadId}/messages/{messageId}`. For email messages, HubSpot cuts off the reply history section of an email. This is similar to how common mail clients handle reply history and longer emails. When you retrieve a message, the response will include a `truncationStatus` field. The value will either be `NOT_TRUNCATED`, `TRUNCATED_TO_MOST_RECENT_REPLY,` or `TRUNCATED`. If a message is truncated, you can make a `GET` request to `conversations/v3/conversations/threads/{threadId}/messages/{messageId}/original-content`. This will retrieve the full original version of the message. ### Retrieve a subset of messages associated with a specific contact You can also fetch messages associated with a single contact, which can be helpful if you're creating a view where a customer can review the conversations they've had with your business. To filter for messages associated with a contact, provide the ID of the contact as the `associatedContactId` as a query parameter in the URL of your request, along with a `threadStatus` parameter, which can be either `OPEN` or `CLOSED`. For example, to retrieve open threads associated with a contact whose ID is `53701`, you'd make a `GET` request to the following URL: `https://api.hubapi.com/conversations/v3/conversations/threads?associatedContactId=53701&threadStatus=OPEN` ### Get actors Actors are entities that interact with conversations, like a HubSpot user responding to a message or a visitor sending a message. When you make a request to retrieve threads or messages, the response will include actor IDs. All actor IDs are a single letter representing the actor type, followed by a hyphen, followed by an identifier for the actor. The identifier could be a number, a string, etc. depending on the actor type. In the table below, learn more about the different actor IDs: | Actor Type | Identifier | Description | | --- | --- | --- | | `A-` | HubSpot user ID (number) | Agent actor, i.e. a user in the account. | | `E-` | Email address (string) | Email actor. This is used for email addresses that HubSpot didn't try to resolve to an agent actor or a visitor actor. For example, HubSpot will generally resolve the from email address to a contact, but if an incoming email has any additional email addresses included in the _To_ field, _CC_ field, etc., HubSpot won't try to resolve those email addresses to contacts. | | `I-` | App ID (number) | Integration actor. This is used for actions taken by an integration. | | `S-` | S-hubspot (string) | System actor. This is used for actions taken by the HubSpot system itself. | | `V-` | Contact ID (number) | Visitor actor. Keep in mind that it's possible the visitor isn't a contact yet. | For example, if you make a `GET` request to `conversations/v3/conversations/threads/{threadId}/messages`, the response will include a `sender` and `recipient` fields. These fields will include the actor IDs for the visitor or agent. For example, your response may look similar to the following: To retrieve a single actor, make a `GET` request to `conversations/v3/conversations/actors/{actorId}`. You will need the `actorId`, which is included in the response to a request to get a message or thread. The response will return details like the actor name, email address, avatar, and actor type. ### Update or restore threads You can update a thread's status by making a `PATCH` request to `conversations/v3/conversations/threads/{threadId}`. In the request body, include the thread properties to update. | Field | Description | | -------- | ---------------------------------------------------------- | | `status` | The status of the thread, which can be `OPEN` or `CLOSED`. | To restore any soft-deleted threads, make a `PATCH` request to `conversations/v3/conversations/threads/{threadId}?archived=true` to retrieve the archived thread, then in the request body set the archived property to `false`. | Field | Description | | ---------- | ----------------------------------- | | `archived` | Set to `false` to restore a thread. | At this time you can only update the thread's status or restore a thread when making a `PATCH` request to this endpoint. ### Archive threads You can archive a thread by making a `DELETE` request to `conversations/v3/conversations/threads/{threadId}`. If you use this endpoint to archive a thread, the thread will be moved to the trash and will be permanently deleted after 30 days. In the section above, learn how to restore a thread before it is permanently deleted. ### Add comments to threads To add a comment to an existing thread, make a `POST` request to `conversations/v3/conversations/threads/{threadId}/messages`. Comments only appear in the inbox and are not sent to visitors. You can include the following fields in the request body when sending a comment: | Field | Description | | --- | --- | | `type` | The type of message you're sending. This value can either be `MESSAGE` or `COMMENT`. | | `text` | The content of the message. | | `richText` | The content of the message in HTML format. This is optional. | | `attachments` | Any attachments to attach to the comment, specified as an object containing a `fileId`. If you want to include a specific attachment, you can upload it using the [Files API](/guides/api/library/files) before making the call to add the comment. | For example, your request body may look similar to the following: ```json // POST request to conversations/v3/conversations/threads/{threadId}/messages { "type": "COMMENT", "text": "Can you follow up?", "richText": "<p>Can you follow up?</p>" } ``` ### Send messages to threads Using this endpoint, an integration can send outgoing messages as a user in a HubSpot account. These messages are sent from the channel, just like if a user typed and sent a response in the inbox. To send a message to an existing thread, make a `POST` request to `conversations/v3/conversations/threads/{threadId}/messages`. When sending a message to a thread, you can include the following fields. You can include both the message details, as well as recipient information, in the request body. | Field | Description | | --- | --- | | `type` | The type of message you're sending. This value can either be `MESSAGE` or `COMMENT`. | | `text` | The content of the message. | | `richText` | The content of the message in HTML format. This is optional. | | `recipients` | An object representing the recipients of the message. This is the most important for email, where you can specify multiple recipients. This object includes the following fields:`actorID`: the ID associated with the message recipient.`name`: the name of the recipient.`recipientField`: for email messages, this is the type of recipient. Available values are `TO`, `CC`, or `BCC`.`deliveryIdentifiers`: for email messages, this indicates the email addresses used for the visitor and agent. | | `senderActorId` | This must be an agent actor ID that is associated to the HubSpot user in the account who is sending the message. | | `channelId` | The ID of a generic channel returned from the channels endpoint, like `1000` for live chat, `1001` for Facebook Messenger, `1002` for email, etc. | | `channelAccountId` | The ID of an account that is part of the `channelId` channel. On an existing thread, it is recommended to copy the `channelId` and `channelAccountId` of the most recent message on the thread. | | `subject` | The subject line of the email. This is ignored when sending a message to a non-email channel. | | `attachments` | Any attachments to attach to the message, which can be specified as an object that contains a URL to a file hosted in your HubSpot account, or a list of quick replies if you're sending a message via Facebook Messenger or LiveChat. Learn more about how to include attachments [in the section below.](#include-attachments-in-messages) | For example, your request body may look similar to the following: ```json // POST request to conversations/v3/conversations/threads/{threadId}/messages { "type": "MESSAGE", "text": "Hey there, following up", "richText": "<p>Hey there, following up</p>", "recipients": [ { "actorId": "E-user@hubspot.com", "name": "Leslie Knope", "recipientField": "TO", "deliveryIdentifiers": [ { "type": "HS_EMAIL_ADDRESS", "value": "lknope@hubspot.com" } ] } ], "senderActorId": "A-3892666", "channelId": "1002", "channelAccountId": "42423411", "subject": "Follow up" } ``` If you make a successful request, the new message will be sent to the visitor. This can appear as a message in the chat widget, an email in the thread, a message in Facebook Messenger, etc. ## Include attachments in messages Attachments can be links to a file hosted in the HubSpot files tool, or a set of quick replies if you're sending a message using Facebook Messenger or LiveChat. To include an attachment from a file in your HubSpot account, provide the absolute URL as the `fileId` in the `attachments` field of the request body. For example, the corresponding part of the request body is shown in lines `10-12` below: ```json // POST request to conversations/v3/conversations/threads/{threadId}/messages { "type": "MESSAGE", "text": "Hey there, following up", "recipients": { "actorID": "E-user@hubspot.com", "name": "Leslie Knope", "recipientField": "TO", "deliveryIdentifiers": [ { "type": "HS_EMAIL_ADDRESS", "value": "lknope@hubspot.com" } ] }, "senderActorId": "A-3892666", "channelId": "1002", "channelAccountId": "424232411", "subject": "Follow up", "attachments": { "fileId": "https://12345678.fs1.hubspotusercontent-na1.net/hubfs/12345678/doggo_video.mp4" } } ``` If you connected Facebook Messenger or LiveChat as a channel, you can also specify a set of quick replies within the `attachments` field, which prompt the recipient with certain options that appear as tappable buttons below a message. Once tapped, the corresponding value of that option will be recorded. Quick replies should be provided as a list of objects that each contain the following fields: - `label`: the visible text that appears to the recipient (e.g., _Red_) - `value`: the associated value of the button that you want to record (e.g., `RED`) - `valueType`: the type of the quick reply option, which can be either `TEXT` or `URL`. The example request body below demonstrates how to specify two quick reply options, _Yes_ and _No_, on lines `10-23`: ```json // POST request to conversations/v3/conversations/threads/{threadId}/messages { "type": "MESSAGE", "text": "Did that answer your question?", "recipients": { "actorID": "E-user@hubspot.com", "name": "Leslie Knope", "recipientField": "TO", "deliveryIdentifiers": [{"type": "HS_EMAIL_ADDRESS", "value": "lknope@hubspot.com"}]}, "senderActorId": "A-3892666", "channelId": "1002", "channelAccountId": "424232411", "subject": "Follow up", "attachments": [ "type": "QUICK_REPLIES", "quickReplies": [ { "label": "Yes", "value": "Yes", "valueType": "URL" }, { "label": "No", "value": "No", "valueType": "TEXT" } ] ] } ``` ## Webhooks Webhooks for conversations are also supported and can be used in tandem with the conversations API. You can use webhooks to subscribe to events about conversations, including thread creation, thread status and assignment changes, and new messages on threads. Learn more about using [webhooks](/guides/api/app-management/webhooks). You must have the `conversations.read` scope to get access to following webhooks. The available conversation webhook events are: | Event | Description | | --- | --- | | `conversation.creation` | A new Conversations thread has been created. | | `conversation.deletion` | A Conversations thread has been archived. | | `conversation.privacyDeletion` | A Conversations thread has been permanently deleted. | | `conversation.propertyChange` | A property of a Conversations thread has been updated. | | `conversation.newMessage` | A new message has been posted on a Conversations thread. | The available properties for each event type are: | Event | Properties | | --- | --- | | `All events` | `objectId`: the ID of the Conversations thread that the webhook corresponds to. | | `conversation.propertyChange` | `assignedTo`: a thread's assignment has changed. The value is the user ID of the new assignee, unless the owner was removed.`status`: a thread's status has changed, either to `OPEN` or `CLOSED`.`isArchived`: when a thread is restored a `conversation.propertyChange` webhook will be sent and the value will be `false`. | | `conversation.newMessage` | `messageId`: the ID of the new message.`messageType`: the type of the new message. One of `MESSAGE`, `COMMENT`, `WELCOME_MESSAGE`. | # @ Learn how to use the HubSpot mobile chat SDK in your Android app. $ guides & api/conversations/mobile-chat-sdk --- # Integrate the HubSpot mobile chat SDK into your Android app You can use the HubSpot mobile chat SDK to integrate your Android mobile application with HubSpot's live chat functionality. The mobile chat SDK allows you to: - Integrate HubSpot chat into your mobile app to deliver real-time, in-app customer support. - Leverage the [bot](https://knowledge.hubspot.com/chatflows/create-a-bot) and [knowledge base](https://knowledge.hubspot.com/knowledge-base/create-and-customize-knowledge-base-articles) tools to help deflect customer inquires when your support agents are unavailable. - Alert users of new messages via push notifications. - Customize the chat experience to align with your business's brand and UI. ## Set up and run the demo app To set up the mobile chat SDK dependencies to your project: - Add your `google-services.json` in the `app/` folder, and ensure that the `package_name` field in the file is set to `com.example.demo`. - Create your `hubspot-info.json` file in the `app/` folder. The full path of the file should be `app/src/main/assets/hubspot-info.json`. - Add the HubSpot package to your build gradle file: ```hubl implementation "com.hubspot.mobilechatsdk:mobile-chat-sdk-android::LATEST_VERSION" ``` - [Sync](https://docs.gradle.org/current/dsl/org.gradle.api.tasks.Sync.html) your Android project with its gradle files. - Build and run the app. ## Install and configure the SDK The SDK needs to be configured once per app launch. The most convenient time to configure the SDK is during app initialization. Start by getting an instance of the `HubSpotManager` class, then call the `configure` method ```hubl val manager = HubspotManager.getInstance(context) manager.configure() try! HubspotManager.configure() return true } ``` Failure to include the config file, or forgetting to include the file as being part of your apps target will cause initialization to throw an errors. The `HubspotConfigError` class represents different errors when missing properties. ## Open HubSpot chat view The chat view can be opened by `HubspotWebActivity` which extends the `Activity` class. You can open it by using any of the following approaches: - Directly calling `startActivity` via intent - Create any button type, then within an `onClick` listener of that button, open the `HubspotWebActivity`. - Add `HubspotFloatingActionButton` in the file which handle the clicks and open the `HubspotWebActivity` automatically. For example, if you wanted to go with the second approach above and specify the behavior within an `onClick` listener of a button: ```hubl binding.anybutton.setOnClickListener { startActivity(Intent(requireContext(), HubspotWebActivity::class.java)) } ``` ## Identify users with the HubSpot visitor identification token You can identify users by using the visitor identification API, detailed in this article. This API is best implemented in your own server project, where you can pass the identity token to your app based on whether suits your specific setup (i.e., passing a token in response to a visitor going through a login page as a result of a dedicated API that you own). Once a token is generated, it can be associated with a contact's email address by calling `HubspotManager.setUserIdentity(email, identityToken)`. This should be called before opening a chat view. ## Add custom chat data properties The SDK supports setting key-value pairs to keep track of data you might need to track while a chat session is open. You can set your own custom values, or declare common permission preferences such as photo or notification permissions. Use the `HubspotManager.setChatProperties(key, value)` method and provide the associated key and value you want to set. This is best called before starting a chat, and will apply to all new chats. You could set an account status, or other identifiers when setting up your user. These will then appear in all chats opened for the remainder of the app launch. For example, the following code would set permissions for camera, photo, notifications, and location data to `"false"`. ```hubl val keyValuePair = mapOf( ChatPropertyKey.CameraPermissions.chatPropertyValue to "false", ChatPropertyKey.PhotoPermissions.chatPropertyValue to "false", ChatPropertyKey.NotificationPermissions.chatPropertyValue to "false", ChatPropertyKey.LocationPermissions.chatPropertyValue to "false" ) hubspotManager.setChatProperties(keyValuePair) ``` ## Clear data on logout The SDK stores in memory identification tokens, email address, and any properties set. The push token for the app is also associated with the current user, if applicable. You may want to clear this data when a user is logging out, or changing users in a multi user app. To clear this data, call `HubspotManager.logout()` at an appropriate time in in your app. Calling the `HubspotManager.logout()` only impacts the data used for future chat sessions. It has no impact on data or chat sessions already stored in HubSpot. ## Reference documentation You can consult the [reference documentation](https://github.hubspot.com/mobile-chat-sdk-android/) for details on how to use each of the components in the HubSpot mobile SDK. The binary file is also hosted [here](https://search.maven.org/artifact/com.hubspot.mobilechatsdk/mobile-chat-sdk-android/1.0.0/jar). # Integrate the HubSpot mobile chat SDK into your iOS app @ Learn how to use the HubSpot mobile chat SDK in your iOS app. $ guides & api/conversations/mobile-chat-sdk --- # Integrate the HubSpot mobile chat SDK into your iOS app You can use the HubSpot mobile chat SDK to integrate your iOS mobile application with HubSpot's live chat functionality. The mobile chat SDK allows you to: - Integrate HubSpot chat into your mobile app to deliver real-time, in-app customer support. - Leverage the [bot](https://knowledge.hubspot.com/chatflows/create-a-bot) and [knowledge base](https://knowledge.hubspot.com/knowledge-base/create-and-customize-knowledge-base-articles) tools to help deflect customer inquires when your support agents are unavailable. - Alert users of new messages via push notifications. - Customize the chat experience to align with your business's brand and UI. ## Installation To get start developing with the mobile chat SDK: - Add the [GitHub repository URL](https://github.com/HubSpot/mobile-chat-sdk-ios) of the mobile chat SDK to your project using Swift Package Manager. From your project settings, select the **Package Dependencies** tab, then search for the [HubSpot mobile chat SDK GitHub URL](https://github.com/HubSpot/mobile-chat-sdk-ios) to add it to your project. - While this feature is in beta, you may need to configure Xcode with your GitHub account since this repository is private: - To add your GitHub account directly in Xcode, navigate to **Xcode** > **Settings** in the top menu bar, then click **Account**. Click the **\+ icon** to add your GitHub account. - If you're developing your app using a CLI tool like `xcodebuild`, you may need to specify the `-scmProvider system` or `-scmProvider xcode` arguments to choose whether your system git credentials or xcode credentials are used. ## Configuration After you've added the mobile SDK to your project using the Swift Package Manager, include your `HubSpot-Info.plist config` file in your project, then mark it as included in the app target. During app startup, or in another suitable location in your app's code where you initialize your app components, call the configure method on the SDK. ```hubl func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. // This will configure the SDK using the `Hubspot-Info.plist` file that is bundled in app try! HubspotManager.configure() return true } ``` ## Open HubSpot Chat View You can present the chat view modally as a [sheet](https://developer.apple.com/design/human-interface-guidelines/sheets), as a fullscreen view, or pushed into a navigation stack. The simplest way to get started is to present the chat as a sheet in response to a button press in the UI of your app. The chat view can be initialized using the `HubSpotChatView.init(manager:pushData:chatFlow:)` method, using either default values or with customized chat settings. ### Show the chat view as a sheet in SwiftUI The chat view is a SwiftUI View, meaning it can be the contents of a [sheet](https://developer.apple.com/design/human-interface-guidelines/sheets), or embedded in your existing UI like any other view. You can present it as a sheet using the `.sheet` modifier, in response to a user action such as tapping a button. ```hubl Button(action: { showChat.toggle() }, label: { Text("Chat Now") }).sheet(isPresented: $showChat, content: { HubspotChatView(chatFlow: "support") }) ``` In the example above, `$showChat` is a state property in the view: ```hubl @State var showChat = false ``` ### Show the chat as a presented view controller in UIKit Although the HubSpot Chat View is a SwiftUI view, it will also work when contained in a `UIHostingController`. For example, you can present the chat view from a UIViewController button action. ```hubl @IBAction func onButtonPress(_ source: Any) { //Init chat view with no arguments , or use alternative initialiser for configuring chat specifics let chatView = HubspotChatView() //Create a hosting controller to hold the chat view let hostingVC = UIHostingController(rootView: chatView) // Present the view controller like any other (or push into a navigation stack) self.present(hostingVC, animated: true) } ``` ## Identify users with the HubSpot visitor identification token You can identify users by using the visitor identification API, detailed in [this article](/guides/api/conversations/visitor-identification). This API is best implemented in your own server project, where you can pass the identity token to your app based on whether suits your specific setup (i.e., passing a token in response to a visitor going through a login page as a result of a dedicated API that you own). Once a token is generated, it can associated with a contact's email address by calling `HubspotManager/setUserIdentity(identityToken:email:)`. This should be called before opening a chat view. ## Add custom chat data properties The SDK supports setting key-value pairs to keep track of data you might need to track while a chat session is open. You can set your own custom values, or declare common permission preferences such as photo or notification permissions. Use the `HubspotManager/setChatProperties(data:)` method and provide the associated key and value you want to set. This is best called before starting a chat, and will apply to all new chats. You could set an account status, or other identifiers when setting up your user. These will then appear in all chats opened for the remainder of the app launch. For example, the following code would set several pre-defined permissions and custom properties: ```hubl var properties: [String: String] = [ ChatPropertyKey.cameraPermissions.rawValue: self.checkCameraPermissions(), "myapp-install-id": appUniqueId, "subscription-tier": "premium" ] HubspotManager.shared.setChatProperties(data: properties) ``` ## Clearing data on logout The SDK stores in memory identification tokens, email address, and any properties set. The push token for the app is also associated with the current user, if applicable. You may want to clear this data when a user is logging out, or changing users in a multi user app. To clear this data, call `HubspotManager/clearUserData()` at an appropriate time in your app. Calling the `HubspotManager/clearUserData()` only impacts the data used for future chat sessions. It has no impact on data or chat sessions already stored in HubSpot. ## Reference documentation You can consult the [reference documentation](https://github.hubspot.com/mobile-chat-sdk-ios/documentation/hubspotmobilesdk/) for details on how to use each of the components in the HubSpot mobile SDK. # Visitor Identification @ The visitor identification API is used to identify visitors to your site that were authenticated using external authentication systems. $ guides & api/conversations --- # Visitor identification Use the visitor identification API to identify visitors to your site that were authenticated using your own external authentication system. An identification token returned from this API can be used to pass information about your already-authenticated visitor to the chat widget, so that it treats the visitor as a known contact. Agents in the inbox can then have confidence about who they are talking to, and visitors can access previous thread history across devices. For example, if you host your own web app behind a login, you may set up the HubSpot chat widget to appear on pages within your web app where you know that visitors have already been authenticated and identified. Currently, any visitor chatting in on those pages would still show up as “Unknown visitor” in the Conversations inbox, despite being an identified, logged-in visitor in your web app. Using the Visitor Identification API, you can pass authenticated visitor information directly to the chat widget, which identifies them in the Conversations inbox. - The visitor identification API is for telling HubSpot who the visitor is. You should not rely on this to authenticate users in your platform. - Access to the visitor identification API requires a _Professional_ or _Enterprise_ level subscription. If the account does not have a qualifying subscription, you will receive a `403` error response from the API. ## Example integration flow To integrate with this feature, you must have an existing web application with an authentication system. Before getting started, make sure you have a [private app](/guides/apps/private-apps/overview)set up and the account that you are trying to integrate has a qualifying _Professional_ or _Enterprise_ subscription. Here’s an example of a possible integration flow: Once your customer is logged in and verified in your system, take the following steps to identify them within live chat: **1.** On your front end, set `loadImmediately` to `false` on the `hsConversationsSettings` object on the window. If you do not do this, the chat widget may load before the identification information is passed through. See the [Chat Widget SDK primer below](#chat-widget-sdk-primer) for more information. - Set the `hsConversationsSettings` properties outside the `isConversationsAPIReady` function. - In addition, the `hsConversationsSettings` needs to be set prior to the call, otherwise you may experience a race condition that interferes with widget load. ```js window.hsConversationsSettings = { loadImmediately: false, }; ``` **2.** Generate a token from the Visitor Identification API by passing in the email address of your authenticated visitor. This should be done on the back end of your web application. Check out the [endpoints reference documentation](/reference/api/conversations/visitor-identification) for an example request. ```shell curl --request POST \ --url 'https://api.hubspot.com/conversations/v3/visitor-identification/tokens/create \ --data '{ "email": "gob@bluth.com", "firstName": "Gob", "lastName": "Bluth" }' ``` The provided first and last name will be set on the contact record in HubSpot after the chat begins if: - It's a new contact created by the Visitor Identification API. - It's an existing contact where the name is not already known. This can be useful when personalizing messages to identified visitors when your external system already has name information, but it does not yet exist in HubSpot. These are optional parameters and not required. **3.** Using the token Step 2, set the following properties on the `hsConversationsSettings` object on the window. ```js window.hsConversationsSettings = { identificationEmail: 'visitor-email@example.com', identificationToken: '<TOKEN FROM STEP 1>', }; ``` **4.** Load the widget. ```js window.HubSpotConversations.widget.load(); ``` The token and email must be set on the `hsConversationsSettings` object on the window every time the page loads for an authenticated visitor. This context will not be carried across page loads automatically if these parameters are no longer set. Tokens are temporary and will expire after 12 hours. Tokens can be cached to avoid re-fetching the token on every page load, as long as they are refreshed at least every 12 hours. ## Verify the integration Once you've completed your integration of the Visitor Identification feature, you can verify that it’s working as expected. This can be done in a couple ways, depending on your implementation, so you may need to tailor the examples below to your specific requirements. - If you've added the chat widget to one or more public pages as well as behind an authentication system: - Navigate to a page where the chat widget should not be identifying visitors and start a conversation. - In HubSpot, open the inbox and verify that the chat that just came in belongs to an _Unknown Visitor_. If this is not the case, try following these steps in a private browsing window: - Navigate to a page where the chat widget should be identifying visitors via the Visitor Identification API and start a conversation. - In HubSpot, open the inbox and verify that the chat is correctly attributed to the contact that you’re logged in as. You should see a badge next to the contact’s name, indicating that this contact was successfully identified through this API. - If you've only added the chat widget to pages behind an authentication system, and you have access to multiple test user accounts: - Log in to HubSpot as the first test user, then navigate to a page where the chat widget loads, and start a conversation. - Log out of HubSpot, then log back in as the second test user. Navigate to a page where the chat widget loads, and start a conversation. - In HubSpot, open the inbox and verify that the chats that came in were from the first and second test accounts, respectively, and that you see the badge next to the contact names for both records. For visitors identified with this API, HubSpot will not drop the `messagesUtk` cookie. HubSpot will also skip any email capture questions since email address is already known. Because the `messagesUtk` cookie and email capture do not apply to these chats, the associated settings in the chatflow will not show for visitors identified through the Visitor Identification API. ## Chat widget SDK primer The API is housed in the `window.HubSpotConversations` object. All available methods can be accessed via this object. The HubSpot script loader on your page will create this object for you, but it may not be available immediately. To defer accessing the API until it's initialized, you may use the `window.hsConversationsOnReady` helper. For example: ```html <script type="text/javascript"> function onConversationsAPIReady() { console.log(`HubSpot Conversations API: ${window.HubSpotConversations}`); } /* configure window.hsConversationsSettings if needed. */ window.hsConversationsSettings = {}; /* If external API methods are already available, use them. */ if (window.HubSpotConversations) { onConversationsAPIReady(); } else { /* Otherwise, callbacks can be added to the hsConversationsOnReady on the window object. These callbacks will be called once the external API has been initialized. */ window.hsConversationsOnReady = [onConversationsAPIReady]; } </script> ``` ### SDK reference `window.hsConversationsOnReady` **array** This is an optional field you can define on the window object that enables you to specify code to be executed as soon as the widget becomes available. Once the API has been initialized, it will check for the existence of this array and execute its functions in series. ```js if (window.HubSpotConversations) { console.log('The api is ready already'); } else { window.hsConversationsOnReady = [ () => { console.log('Now the api is ready'); }, ]; } ``` `hsConversationsSettings` **object** This object enables you to provide some configuration options to the widget before it initializes. In order to use the Visitor Identification feature, you must set the following fields: | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `loadImmediately` | boolean | Whether the widget should implicitly load or wait until the `widget.load` method is called | `true` | | `identificationToken` | string | Used to integrate with the Visitor Identification API. This is the token provided by the token generation endpoint on the Visitor Identification API that is used as proof that this visitor has been identified. | `""` | | `identificationEmail` | string | The email address of the visitor that you’ve identified as loading the widget. | `""` | ```js window.hsConversationsSettings = { loadImmediately: false, identificationEmail: 'visitor-email@example.com', identificationToken: '<TOKEN FROM STEP 1>', }; ``` Learn more about the [conversations SDK](/reference/api/conversations/chat-widget-sdk). # Conversations | Running code snippets in bots @ Instructions and for running your own code snippets in a bot action $ guides & api/conversations/working-with-chatbots --- # Running code snippets in bots When [creating or editing a bot](https://knowledge.hubspot.com/articles/kcs_article/conversations/create-a-bot), you can add a code snippet by clicking the "+" to [add an action](https://knowledge.hubspot.com/articles/kcs_article/conversations/a-guide-to-bot-actions) as you normally would. From the action selection panel, click on "Run a code snippet." Next, give your action a nickname. Within the code editing pane, you'll see our default template for Node.js 10.x. The details of the "event" object and possible response object formats are detailed below. The code will be triggered when the saved action is reached in a conversation. There are three main things to keep in mind when working with code snippets: - The `exports.main()` function is called when the code snippet action is executed. - The `event` argument is an object containing details for the visitor and chat session. - The `callback()` function is used to pass data back to the bot and user. It should be called in the `exports.main` function. The `event` object will contain the following data: ```json //example payload { "userMessage": { // Details for the last message sent to your bot "message": "100-500", // The last message received by your bot, sent by the visitor "quickReply": { // If the visitor selected any quick reply options, this will be a list of the selected options. // Will be 'null' if no options were selected. "quickReplies":[ // A list of quick reply options selected by the visitor { "value":"100-500", "label":"100-500" } ], }, "session": { "vid": 12345, // The contact VID of the visitor, if known. "properties": { // A list of properties collected by the bot in the current session. "CONTACT": { "firstname": { "value": "John", "syncedAt": 1534362540592 }, "email": { "value": "testing@domain.com", "syncedAt": 1534362541764 }, "lastname": { "value": "Smith", "syncedAt": 1534362540592 } } }, "customState":{myCustomCounter: 1, myCustomString:"someString"} // Only present if it customState was passed in from a previous callback payload } } ``` The `callback()` function is used to send data back to the bot. The argument should be an object with the following data: ```json //sample payload { "botMessage": "Thanks for checking out our website!", // This is the message your bot will display to the visitor. "quickReplies": [{ value:'option', // Passed to the bot as the response on click label:'Option' // Gets displayed as the button label }], // the quickReplies object is optional "nextModuleNickname": "SuggestAwesomeProduct", // The nickname of the next module the bot should execute. If undefined, the bot will follow the default configured behavior "responseExpected": false // If true, the bot will display the returned botMessage, wait for a response, then execute this code snippet again with that new response. "customState":{myCustomCounter: 1, myCustomString:"someString"} // Optional field to pass along to the next step. } ``` ## Limitations Code snippets in bots must finish running within 20 seconds and only use 128 MB of memory. Exceeding either of these limits will result in an error. ## Available libraries Several popular Node.js libraries are available for use within the code snippet. - [async](https://www.npmjs.com/package/async) - [lodash](https://www.npmjs.com/package/lodash) - [mongoose](https://www.npmjs.com/package/mongoose) - [mysql](https://www.npmjs.com/package/mysql) - [redis](https://www.npmjs.com/package/redis) - [request](https://www.npmjs.com/package/request) - [aws-sdk](https://www.npmjs.com/package/aws-sdk) The libraries can be loaded using the normal `require()` function at the top of your code. ```js const request = require('request'); exports.main = (event, callback) => { request('http://time.jsontest.com/', function (error, response, body) { const responseJson = { botMessage: 'The current time in GMT is ' + JSON.parse(body).time, responseExpected: false, }; callback(responseJson); }); }; ``` # Conversations | Webhooks in bots @ Details for the requests used by webhook steps for bots. $ guides & api/conversations/working-with-chatbots --- # Working with webhooks from bots When [creating or editing a bot](https://knowledge.hubspot.com/articles/kcs_article/conversations/create-a-bot), you can add a webhook by clicking the "+" to [add an action](https://knowledge.hubspot.com/articles/kcs_article/conversations/a-guide-to-bot-actions) as you normally would. From the action selection panel, click on "Trigger a webhook." Next, give your action a nickname and input the endpoint URL for the webhook. If your webhook will be sending data to HubSpot in response to the request, check the "Wait for webhook feedback" box. (Read more on this below.) Save your action. When this action has been reached in a conversation, HubSpot will send a JSON payload to the **Webhook URL** you’ve defined. The payload will contain information relevant to the chat session, including the visitors' responses to any questions asked, their contact ID, and information about the bot. ##### Example request payload: ```json //sample payload { "userMessage": { // Details for the last message sent to your bot "message": "100-500", // The last message received by your bot, sent by the visitor "quickReply": { // If the visitor selected any quick reply options, this will be a list of the selected options. // Will be 'null' if no options were selected. "quickReplies":[ // A list of quick reply options selected by the visitor { "value":"100-500", "label":"100-500" } ], }, "session": { "vid": 12345, // The contact VID of the visitor, if known. "properties": { // A list of properties collected by the bot in the current session. "CONTACT": { "firstname": { "value": "John", "syncedAt": 1534362540592 }, "email": { "value": "testing@domain.com", "syncedAt": 1534362541764 }, "lastname": { "value": "Smith", "syncedAt": 1534362540592 } } } } } ``` Advanced users also have the option of including JSON in your webhook's response. By doing this, you can impact the flow of conversation or send a custom message. ##### Example response payload: ```json //sample payload { "botMessage": null, // This is the message your bot will display to the visitor. "nextModuleNickname": "PromptForCollectUserInput", // If defined, this will be the next module your bot will go to. If undefined, the default configured behavior will be observed. "responseExpected": false // If true, the webhook will be triggered again with the visitor's next reply. If false, the default configured behavior will be observed. } ``` # CRM API | Associations @ The CRM Associations endpoints are used to manage associations between tickets, products, line items, and their related contacts, companies, and deals. $ guides & api/crm/associations --- The [new version of the Associations API](/reference/api/crm/associations/association-details) has additional functionality, including creating and managing association labels. Refer to the [v4 Associations API article](/guides/api/crm/associations/associations-v4) for more information. Associations represent the relationships between objects and activities in the HubSpot CRM. You can use the v3 associations endpoints to create, retrieve, or remove associations in bulk. Learn more about objects, records, properties, and associations APIs in the [Understanding the CRM](/guides/api/crm/understanding-the-crm) guide. For more general information about objects and records in HubSpot, [learn how to manage your CRM database](https://knowledge.hubspot.com/get-started/manage-your-crm-database)[.](https://knowledge.hubspot.com/contacts-user-guide) ## Association types Associations are defined by object and direction. Association types are unidirectional, which means you'll need to use a different definition depending on the starting object type. Each endpoint requires a `{fromObjectType}` and `{toObjectType}` that tell the direction of the association. For example: - To view all the defined association types from contacts to companies, you'd make a request to `/crm/v3/associations/contacts/companies/types`. - To see all tickets associated with a contact, you'd make a request to `/crm/v3/associations/Contacts/Tickets/batch/read` and identify the contact in the request body by its `objectId`. In this example, Contacts is the _fromObjectType_, and Tickets is the _toObjectType_. Association types can include unlabeled associations (e.g., contact-to-company), default labeled associations (e.g., contact-to-primary company), and custom labeled associations (e.g., _Decision maker_ contact-to-company). ## Retrieve association types To view all the defined association types between objects, including default associations and custom association labels, make a `GET` request to `/crm/v3/associations/{fromObjectType}/{toObjectType}/types`. Each type will have a returned numerical `id` value and `name` that can be used to reference the association type in other requests. For default associations, the numerical ID will be the same for all accounts, but for custom association labels, the ID will be unique to your account. For example, your response would look similar to the following: ```json ///Example response GET /crm/v3/associations/contacts/companies/types { "results": [ { "id": "136", "name": "franchise_owner_franchise_location" }, { "id": "26", "name": "manager" }, { "id": "1", "name": "contact_to_company" }, { "id": "279", "name": "contact_to_company_unlabeled" }, { "id": "32", "name": "contractor" }, { "id": "37", "name": "chef" }, { "id": "142", "name": "toy_tester" }, { "id": "30", "name": "decision_maker" }, { "id": "28", "name": "billing_contact" } ] } ``` While you can reference custom association types (i.e. labels) with the v3 Associations API, you cannot use the API to create or edit new labels. Learn how to create, update, and delete labels in the [v4 Associations API article](/guides/api/crm/associations/associations-v4). ## Create associations To [associate records](https://knowledge.hubspot.com/records/associate-records), make a `POST` request to `/crm/v3/associations/{fromObjectType}/{toObjectType}/batch/create`. In your request, include the `id` values for the records you want to associate, as well as the `type` of the association. For example, to associate contacts to companies, your request URL would be `/crm/v3/associations/Contacts/Companies/batch/create`, and your request would look similar to the following: ```json ///Example request body { "inputs": [ { "from": { "id": "53628" }, "to": { "id": "12726" }, "type": "contact_to_company" } ] } ``` ## Retrieve associations To retrieve associated records, make a `POST` request to `/crm/v3/associations/{fromObjectType}/{toObjectType}/batch/read`. In your request, include the `id` values of the records whose associations you want to view. This will be for the `{fromObjectType}`. For example, to retrieve deals associated with companies, your request URL would be `/crm/v3/associations/Companies/Deals/batch/read` and the request body would look like the following, with `id` values for the companies whose deal associations you want to view: ```json ///Example request POST /crm/v3/associations/Companies/Deals/batch/read { "inputs": [ { "id": "5790939450" }, { "id": "6108662573" } ] } ``` In your response, you'll receive the `id` values of all associated records. For the above example, your response would include the `id` values for all associated deals and the association `type`. The response would look similar to the following: ```json ///Example response POST /crm/v3/associations/Companies/Deals/batch/read { "status": "COMPLETE", "results": [ { "from": { "id": "5790939450" }, "to": [ { "id": "1467822235", "type": "company_to_deal" }, { "id": "7213991219", "type": "company_to_deal" }, { "id": "9993513636", "type": "company_to_deal" }, { "id": "18731599139", "type": "company_to_deal" }, { "id": "21678228008", "type": "company_to_deal" } ] }, { "from": { "id": "6108662573" }, "to": [ { "id": "22901690010", "type": "company_to_deal" } ] } ], "startedAt": "2024-10-21T16:40:47.810Z", "completedAt": "2024-10-21T16:40:47.833Z" } ``` When retrieving records associated with companies (i.e. `crm/v3/associations/{fromObjectType}/companies/batch/read`), only the [primary associated company](https://knowledge.hubspot.com/records/associate-records#primary-company-association) will be returned. To view all associated companies, use the [V4 associations API](/guides/api/crm/associations/associations-v4). ## Remove associations To remove associations between records, make a `POST` request to `/crm/v3/associations/{fromObjectType}/{toObjectType}/batch/archive`. In the request body, include the `id` values for the from record and the to record, as well as their association type. For example, to remove the association between a company and a deal, your request would look like: ```json ///Example request POST crm/v3/associations/companies/deals/batch/archive { "inputs": [ { "from": { "id": "5790939450" }, "to": { "id": "21678228008" }, "type": "company_to_deal" } ] } ``` # CRM API | Associations v4 @ The CRM Associations endpoints are used to manage associations between tickets, products, line items, and their related contacts, companies, and deals. $ guides & api/crm/associations --- For the previous version, please see the documentation for the [v3 Associations API](/guides/api/crm/associations/associations-v3). Associations represent the relationships between objects and activities in the HubSpot CRM. Record associations can exist between records of different objects (e.g., Contact to Company), as well as within the same object (e.g., Company to Company). The v4 Associations API includes _Association_ endpoints and _Association schema_ endpoints: - **Association endpoints**: create, edit, and remove associations between records. - **Association schema endpoints**: view your account's association definitions (also known as types), create and manage custom association labels, and set limits for associations. Association labels are supported between contacts, companies, deals, tickets, and custom objects, and can be [used across HubSpot in tools](https://knowledge.hubspot.com/object-settings/create-and-use-association-labels#use-association-labels-in-hubspot-tools), such as lists and workflows. Learn more about objects, records, properties, and associations APIs in the [Understanding the CRM](/guides/api/crm/understanding-the-crm) guide. The v4 Associations API is supported in Version 9.0.0 or later of the NodeJS HubSpot Client. ## Associate records To associate records with records with one another, use the _Association_ endpoints. The number of associations a record can have depends on [the object and your HubSpot subscription.](https://legal.hubspot.com/hubspot-product-and-services-catalog#TechnicalLimits:~:text=CRM%20Record%20Association%20Limits) ### Associate records without a label You can create a default unlabeled association between two records, or set up unlabeled associations for records in bulk. To set up an individual default association between two records, make a `PUT` request to `/crm/v4/objects/{fromObjectType}/{fromObjectId}/associations/default/{toObjectType}/{toObjectId}` In the request URL, include: - `fromObjectType`**:** the ID of the object you're associating. To find the ID values, refer to this [list of object type IDs,](/guides/api/crm/understanding-the-crm#object-type-id) or for contacts, companies, deals, tickets, and notes, you can use the object name (e.g., `contact`, `company`). - `fromObjectId`**:** the ID of the record to associate. - `toObjectType`**:** the ID of the object you're associating the record to. To find the ID values, refer to this [list of object type IDs,](/guides/api/crm/understanding-the-crm#object-type-id) or for contacts, companies, deals, tickets, and notes, you can use the object name (e.g., `contact`, `company`). - `toObjectId`**:** the ID of the record to associate to. For example, to associate a contact record whose ID is 12345 with a company record whose ID is `67891`, your request URL would be: `/crm/v4/objects/contact/12345/associations/default/company/67891`. To associate records without a label in bulk, make a `POST` request to `crm/v4/associations/{fromObjectType}/{toObjectType}/batch/associate/default`. In the request body, include `objectId` values for the records you want to associate. ### Associate records with a label You can also associate records with labels for individual record pairs or multiple pairs of records in bulk. - To associate two records and set a label to describe the association, make a `PUT` request to `/crm/v4/objects/{objectType}/{objectId}/associations/{toObjectType}/{toObjectId}`. In the request URL, include the `id` values of the two records you're associating. - To bulk create labelled associations between records of the same objects, make a `POST` request to `/crm/v4/associations/{fromObjectType}/{toObjectType}/batch/create`. In the request body, include the `id` values of records to associate in addition to the required parameters below. In the request body, include the following information to indicate the labeled association you want to create: - `associationCategory`: either `HUBSPOT_DEFINED` (default label) or `USER_DEFINED` (custom label). - `associationTypeId`: the numerical ID value for the label. If using a default label (e.g., Primary company), refer to this [list of default type IDs.](#association-type-ids) If you're using a custom label, you'll need to [retrieve the labels](#retrieve-association-labels) between those objects. For each association, depending on your [association limits](#set-and-manage-association-limits), you can include multiple labels. For cross-object and paired label relationships, ensure you use the `typeId` that refers to the correct direction (e.g., Contact to Company vs. Company to Contact, Employee to Manager vs. Manager to Employee). For example, to associate a contact with a deal using a custom label: 1\. Make a `GET` request to `/crm/v4/associations/contact/deal/labels`. 2\. In the response, look at the `typeId` and `category` values for the label. The ID will be a number (e.g., `36`), and the category will always be `USER_DEFINED` for custom labels. 3\. Make a `PUT` request to `/crm/v4/objects/contact/{objectId}/associations/deal/{toObjectId}`with the following request body: ```json /// Example request body [ { "associationCategory": "USER_DEFINED", "associationTypeId": 36 } ] ``` A successful response will include the `id` values of the two associated records along with the `label` for the association. For the example above, the response would look like: ```json /// Example response { "fromObjectTypeId": "0-1", "fromObjectId": 29851, "toObjectTypeId": "0-3", "toObjectId": 21678228008, "labels": ["Point of contact"] } ``` ## Retrieve associated records You can retrieve a record's associations of a specific object. - To retrieve an individual record's associations of a specific object, make a `GET` request to `/crm/v4/objects/{fromObjectType}/{objectId}/associations/{toObjectType}`. In the request URL, include the record's object as the _fromObjectType_ and its record ID as the _objectId_. - To retrieve a record's associated records of a specific object, make a `POST` request to `/crm/v4/associations/{fromObjectType}/{toObjectType}/batch/read`. In the request body, include up to 1,000 `id` values of records whose associated records you want to retrieve. For example, to retrieve all company associations for two contacts, make a `POST` request to `/crm/v4/associations/contacts/companies/batch/read`. Your request would look like the following: ```json /// Example request { "inputs": [ { "id": "33451" }, { "id": "29851" } ] } ``` For both the basic and batch endpoints, the record ID values will be returned for each associated record, along with information to describe the association between the record, including the `label`, `category`, and `typeId`. For the example batch request above, the response would be: ```json /// Example response { "status": "COMPLETE", "results": [ { "from": { "id": "33451" }, "to": [ { "toObjectId": 5790939450, "associationTypes": [ { "category": "HUBSPOT_DEFINED", "typeId": 1, "label": "Primary" }, { "category": "HUBSPOT_DEFINED", "typeId": 279, "label": null }, { "category": "USER_DEFINED", "typeId": 28, "label": "Billing contact" } ] } ] }, { "from": { "id": "29851" }, "to": [ { "toObjectId": 5790939450, "associationTypes": [ { "category": "HUBSPOT_DEFINED", "typeId": 1, "label": "Primary" }, { "category": "USER_DEFINED", "typeId": 37, "label": "Chef" }, { "category": "HUBSPOT_DEFINED", "typeId": 279, "label": null } ] }, { "toObjectId": 6675245424, "associationTypes": [ { "category": "HUBSPOT_DEFINED", "typeId": 279, "label": null } ] }, { "toObjectId": 17705714757, "associationTypes": [ { "category": "HUBSPOT_DEFINED", "typeId": 279, "label": null }, { "category": "USER_DEFINED", "typeId": 30, "label": "Decision maker" } ] } ] } ], "startedAt": "2024-10-21T20:22:42.152Z", "completedAt": "2024-10-21T20:22:42.167Z" } ``` ## Update record association labels For existing associations, to update the association labels, you can use the basic and batch create endpoints. If an existing labeled association exists between two records, to <u>replace</u> the existing label, only include the new label in the request. If you want to append labels (i.e. add a new label and keep the existing label), include both labels in your request. For example, if records are already associated with a label with the `typeId` of `30`, to keep that label while adding another label, your request would look like: ```json /// Example request body [ { "associationCategory": "USER_DEFINED", "associationTypeId": 30 }, { "associationCategory": "USER_DEFINED", "associationTypeId": 37 } ] ``` ## Remove record associations You can delete all associations between records, or delete only associations of specific types (i.e. default or custom labels). When deleting all associations, the records will <u>not</u> be deleted, but they will no longer be associated with one another. If deleting a specific association type, the records will still be associated but the specified labels will be removed, with the exception of deleting the default unlabelled association type which will remove all other associations. To remove all associations: - To remove all associations between two records, make a `DELETE` request to `/crm/v4/objects/{objectType}/{objectId}/associations/{toObjectType}/{toObjectId}`. - To batch remove all associations between records, make a `POST` request to `/crm/v4/associations/{fromObjectType}/{toObjectType}/batch/archive`. In the request body, include the `id` values of records for which you want to remove all of their associations. For example, to remove all associations between sets of contacts and companies, your request would look like: ```json /// Example request body POST crm/v4/associations/contacts/companies/batch/archive [ { "from": { "id": "12345" }, "to": [ { "id": "67891" } ] }, { "from": { "id": "9876" }, "to": [ { "id": "54321" } ] } ] } ``` To remove specific association labels, make a `POST` request to `/crm/v4/associations/{fromObjectType}/{toObjectType}/batch/labels/archive`. In the request body, include an array with `id` values of the associated records and the `associationTypeId` and `category` values of label(s) to remove. For example, to remove a custom label from an association, but keep the unlabeled association, your request would look like: ```json /// Example request body POST crm/v4/associations/contacts/companies/batch/labels/archive { "inputs": [ { "types": [ { "associationCategory": "USER_DEFINED", "associationTypeId": 37 } ], "from": { "id": "29851" }, "to": { "id": "5790939450" } } ] } ``` If you then retrieve that contact's company associations, now only the unlabelled association type will be returned for the above associated company: ```json /// Example response GET crm/v4/objects/contacts/29851/associations/companies { "results": [ { "toObjectId": 5790939450, "associationTypes": [ { "category": "HUBSPOT_DEFINED", "typeId": 279, "label": null } ] }, { "toObjectId": 6675245424, "associationTypes": [ { "category": "HUBSPOT_DEFINED", "typeId": 279, "label": null } ] }, { "toObjectId": 17705714757, "associationTypes": [ { "category": "HUBSPOT_DEFINED", "typeId": 279, "label": null }, { "category": "USER_DEFINED", "typeId": 30, "label": "Decision maker" } ] } ] } ``` ## Report on high association usage There are technical [limits to the number of associations a record can have](https://legal.hubspot.com/hubspot-product-and-services-catalog). You can use the associations API to retrieve a report of records that are either approaching or have hit the maximum limit for associations. To retrieve the report, make a `POST` request to `crm/v4/associations/usage/high-usage-report/{userID}`. The file includes records using 80% or more of their association limit. For example, if a company can be associated with up to 50,000 contacts, the company will be included in the file if it has 40,000 or more associated contacts. The file will be sent to the email of the user whose ID was included in the request URL. Learn how to retrieve user IDs with the [users API](/guides/api/settings/users/user-provisioning). ## Understand association definitions, configurations, and labels To manage association definition (a.k.a association type) configurations and labels, use the _Association schema_ endpoints. These endpoints include configuration endpoints and label endpoints. You should use the endpoints for the following goals: - **Association definitions (labels)**: create and manage association types, including creating, editing, and deleting custom association labels. For example, create a _Billing contact_ label between contacts and deals or a _Manager_ and _Employee_ paired label between contacts. - **Association definition configurations**: set and manage limits for how many associations can exist per association type. For example, allow up to five associated deals per company or only one associated contact with the _Decision maker_ label per company. ### HubSpot defined associations HubSpot provides a set of predefined association types (e.g., unlabeled contact to company), but account admins can [define their own association labels](https://knowledge.hubspot.com/object-settings/create-and-use-association-labels) to provide additional context for record relationships (e.g., manager and employee). There are two HubSpot-defined association types: - **Primary:** the main company that the other record is associated with. Primary associations can be used in HubSpot tools such as [lists and workflows](https://knowledge.hubspot.com/object-settings/create-and-use-association-labels#use-associations-in-hubspot-tools). For records with multiple associated companies, this API supports changing which company is considered the primary. - **Unlabeled:** an association added when any contact, company, deal, ticket, or custom object record is associated. This type denotes that an association exists, and will always [returned](#retrieve-associated-records) in responses with a `label` value of `null`. When a record has a primary association or a custom association label, those types will be [listed alongside the unlabeled association type](#retrieve-associated-records). You can view all of the HubSpot-defined association types in [this section](#association-type-ids). ### Custom association labels You can create association labels to further define record associations. For example, you could create a _Decision maker_ label to indicate which contacts at a company are responsible for making purchasing decisions. Learn more about [creating association labels below](#create-association-types). ## Create and manage association types Use the definitions endpoints to create custom labeled association types and review or manage existing types. ### Create association labels You can create custom labeled association types [in HubSpot](https://knowledge.hubspot.com/object-settings/create-and-use-association-labels) or through the association schema API endpoint. You can create up to 10 association labels between each object pairing (e.g. contacts and companies, contacts and contacts). There are two types of [association labels](https://knowledge.hubspot.com/object-settings/create-and-use-association-labels) you can use to describe the relationships between records: - **Single:** one label that applies to both records in the relationship. For example, _Friend_ or _Colleague_. - **Paired**: a pair of labels for when different words are used to describe each side of the associated records' relationship. For example, _Parent_ and _Child_ or _Employer_ and _Employee_. To create paired labels, you must include the `inverseLabel` field in your request to name the second label in the pair. To create a labeled association type, make a `POST` request to `/crm/v4/associations/{fromObjectType}/{toObjectType}/labels` and include the following in your request: - **name**: the internal name of the association type. This value <u>cannot</u> include hyphens or begin with a numerical character. - **label**: the name of the [association label as shown in HubSpot](https://knowledge.hubspot.com/object-settings/create-and-use-association-labels). - **inverseLabel** (paired labels only): the name of the second label in the pair of labels. For example, your request could look similar to the following: ```json ///Example request body - Single label { "label": "Partner", "name": "partner" } ``` ```json ///Example request body - Paired labels { "label": "Manager", "inverseLabel": "Employee", "name": "manager_employee" } ``` In the response, the new association label's `category` and unique `typeId` will be returned, which you can use to retrieve, update, or delete the label moving forward. For paired labels, there'll be a value for each direction of the association (e.g., `550` for contact to company and `551` for company to contact). For example, for the paired label request above, the response would look like: ```json ///Example response { "results": [ { "category": "USER_DEFINED", "typeId": 145, "label": "Employee" }, { "category": "USER_DEFINED", "typeId": 144, "label": "Manager" } ] } ``` Once created, you can now add the label when [associating records](#associate-records-with-a-label). ### Retrieve association labels To view the association types between specific objects, make a `GET` request to `/crm/v4/associations/{fromObjectType}/{toObjectType}/labels`. You'll receive an array, each item containing: - `category`**:** whether the association type was created by HubSpot (`HUBSPOT_DEFINED`) or by a user (`USER_DEFINED`). - `typeId`**:** the numeric ID for that association type. This is used to [set a label when associating records](#associate-with-label). Refer to [this list](#association-type-ids) for all the HubSpot defined `typeId` values. - `label`**:** the alphanumeric label. This will be `null` for the unlabeled association type. You can also find these values in HubSpot [in your association settings](https://knowledge.hubspot.com/object-settings/create-and-use-association-labels#association-label-api-details). For example, to view all association types from contacts to companies, make a `GET` request to `/crm/v4/associations/contacts/companies/labels`. Your response would look similar to the following: ```json ///Example response { "results": [ { "category": "HUBSPOT_DEFINED", "typeId": 1, "label": "Primary" }, { "category": "USER_DEFINED", "typeId": 28, "label": "Billing contact" }, { "category": "USER_DEFINED", "typeId": 142, "label": "Toy Tester" }, { "category": "USER_DEFINED", "typeId": 26, "label": "Manager" }, { "category": "USER_DEFINED", "typeId": 30, "label": "Decision maker" }, { "category": "USER_DEFINED", "typeId": 37, "label": "Chef" }, { "category": "USER_DEFINED", "typeId": 32, "label": "Contractor" }, { "category": "HUBSPOT_DEFINED", "typeId": 279, "label": null } ] } ] } ``` ### Update association labels You can edit the `label` field for association types, which updates the name as it appears in HubSpot in your settings and on records. You <u>cannot</u> change the internal `name` or `typeId`. To update a label, make a `PUT` request to `/crm/v4/associations/{fromObjectType}/{toObjectType}/labels`. In the request body, including the `associationTypeId` and a new value for `label`. If editing a paired label, you can also include a new value for `inverseLabel`. Using the example in the section above, to update the label _Contractor_ to _Contract worker_, your request would look like: ```json ///Example request { "associationTypeId": 32, "label": "Contract worker" } ``` ### Delete association labels You can delete custom association labels if they're no longer in use. If a label is used to describe associated records, you'll need to [remove the label](#remove-record-associations) from associations before deleting. Default association types, including the _Primary company_ label, <u>cannot</u> be deleted. To delete an association label, make a `DELETE` request to `/crm/v4/associations/{fromObjectType}/{toObjectType}/labels/{associationTypeId}`. You'll no longer be able to use this label when associating records. ## Set and manage association limits Use the definition configuration endpoints to set up [limits](https://knowledge.hubspot.com/object-settings/set-limits-for-record-associations) for the number of associated records between objects, or how often a label can be used to describe associations. There are also [technical limits and limits based on your HubSpot subscription](https://legal.hubspot.com/hubspot-product-and-services-catalog). ### Create or update association limits You can create new or update existing association limits between objects. - To create limits, make a `POST` request to `crm/v4/associations/definitions/configurations/{fromObjectType}/{toObjectType}/batch/create`. - To update existing limits, make a `POST` request to `crm/v4/associations/definitions/configurations/{fromObjectType}/{toObjectType}/batch/update`. In the request body, include `inputs` with the following: | Parameter | Description | | --- | --- | | `category` | The category of the association you're setting a limit for, either `HUBSPOT_DEFINED` or `USER_DEFINED`. | | `typeId` | The numeric ID for the association type you want to set a limit for. Refer to [this list](#association-type-id-values) of default `typeId` values or [retrieve the value](#retrieve-association-types) for custom labels. | | `maxToObjectIds` | The maximum number of associations allowed for the association type. | For example, to set limits that a deal can be associated with a maximum of five contacts with only one contact labelled _Point of contact_ for a deal, your request would look like the following: ```json ///Example request POST crm/v4/associations/definitions/configurations/deal/contact/batch/create { "inputs": [ { "category": "HUBSPOT_DEFINED", "typeId": 3, "maxToObjectIds": 5 }, { "category": "USER_DEFINED", "typeId": 35, "maxToObjectIds": 1 } ] } ``` ### Retrieve association limits - To read all defined association limits, make a `GET` request to `/crm/v4/associations/definitions/configurations/all`. This will return custom association limits defined across all objects. - To read association limits between two specific objects, make a `GET` request to `/crm/v4/associations/definitions/configurations/{fromObjectType}/{toObjectType}`. For both requests, the response will return the associations' values for `category`, `typeId`, `maxToObjectIds`, and `label`. For example, if retrieving limits between deals and contacts, the response would look similar to: ```json ///Example response GET crm/v4/associations/definitions/configurations/deal/contact { "results": [ { "category": "HUBSPOT_DEFINED", "typeId": 3, "userEnforcedMaxToObjectIds": 5, "label": null } ] } ``` ### Delete association limits To delete specific association limits, make a `POST` request to `/crm/v4/associations/definitions/configurations/{fromObjectType}/{toObjectType}/batch/purge`. In the request body, include the `category` and `typeId` values of the association types for which you want to remove limits. For example, to remove the _Point of contact_ limit between deals and contacts, the request would look like: ```json ///Example request POST crm/v4/associations/definitions/configurations/deal/contact/batch/purge { "inputs": [ { "category": "USER_DEFINED", "typeId": 35 } ] } ``` If successful, you'll receive a 204 response and the included limit will return to the system default (i.e. Many contacts can have the label _Point of contact_). ## Limitations The association API endpoints are subject to the following limits based on your account subscription: - Daily limits: - **_Professional_ accounts:** 500,000 requests - **_Enterprise_ accounts:** 500,000 requests - You can purchase an [API limit increase](https://legal.hubspot.com/hubspot-product-and-services-catalog#Addons), you can make a maximum of 1,000,000 requests per day. This maximum will <u>not</u> increase for association API requests if you purchase an additional API limit increase. - Burst limits: - **_Free_** **and _Starter_ accounts:** 100 requests per 10 seconds - _**Professional**_ **and _Enterprise_ accounts:** 150 requests per 10 seconds - If you purchase the [API limit increase](https://legal.hubspot.com/hubspot-product-and-services-catalog#Addons), you can make a maximum of 200 requests per 10 seconds. This maximum will <u>not</u> increase for association API requests if you purchase an additional API limit increase. Learn more about API limits in [this article](/guides/apps/api-usage/usage-details). ## Association type ID values The following tables include the HubSpot-defined `associationTypeId` values that specify the type of association. Association types vary depending on the included objects and the direction of the association (e.g., Contact to Company is different from Company to Contact). If you create custom objects or custom association labels, the related association types will have unique `typeId` values that you'll need to [retrieve](#retrieve-association-labels) or locate in your [association settings in HubSpot](https://knowledge.hubspot.com/object-settings/create-and-use-association-labels#association-label-api-details). Default company association types include an unlabeled association type and a primary association type. If a record has more than one associated company, only one can be the primary company. The other associations can either be unlabelled or have custom association labels. ### Company to object | TYPE ID | Association type | | ------- | ------------------------------------------------------------- | | `450` | Company to company | | `14` | Child to parent company | | `13` | Parent to child company | | `280` | Company to contact | | `2` | Company to contact (Primary) | | `342` | Company to deal | | `6` | Company to deal (Primary) | | `340` | Company to ticket | | `25` | Company to ticket (Primary) | | `181` | Company to call | | `185` | Company to email | | `187` | Company to meeting | | `189` | Company to note | | `191` | Company to task | | `88` | Company to communication (SMS, WhatsApp, or LinkedIn message) | | `460` | Company to postal mail | | `180` | Company to invoice | | `510` | Company to order | | `390` | Company to payment | | `298` | Company to subscription | | `909` | Company to appointment | | `939` | Company to course | | `885` | Company to listing | | `793` | Company to service | ### Contact to object | TYPE ID | Association type | | ------- | ------------------------------------------------------------- | | `449` | Contact to contact | | `279` | Contact to company | | `1` | Contact to company (Primary) | | `4` | Contact to deal | | `15` | Contact to ticket | | `193` | Contact to call | | `197` | Contact to email | | `199` | Contact to meeting | | `201` | Contact to note | | `203` | Contact to task | | `82` | Contact to communication (SMS, WhatsApp, or LinkedIn message) | | `454` | Contact to postal mail | | `587` | Contact to cart | | `508` | Contact to order | | `178` | Contact to invoice | | `388` | Contact to payment | | `296` | Contact to subscription | | `907` | Contact to appointment | | `861` | Contact to course | | `883` | Contact to listing | | `799` | Contact to service | ### Deal to object | TYPE ID | Association type | | ------- | ---------------------------------------------------------- | | `451` | Deal to deal | | `3` | Deal to contact | | `341` | Deal to company | | `5` | Deal to company (Primary) | | `27` | Deal to ticket | | `205` | Deal to call | | `209` | Deal to email | | `211` | Deal to meeting | | `213` | Deal to note | | `215` | Deal to task | | `86` | Deal to communication (SMS, WhatsApp, or LinkedIn message) | | `458` | Deal to postal mail | | `313` | Deal to deal split | | `19` | Deal to line item | | `176` | Deal to invoice | | `511` | Deal to order | | `392` | Deal to payment | | `63` | Deal to quote | | `300` | Deal to subscription | | `945` | Deal to appointment | | `863` | Deal to course | | `887` | Deal to listing | | `795` | Deal to service | ### Ticket to object | TYPE ID | Association type | | ------- | ------------------------------------------------------------ | | `452` | Ticket to ticket | | `16` | Ticket to contact | | `339` | Ticket to company | | `26` | Ticket to company (Primary) | | `28` | Ticket to deal | | `219` | Ticket to call | | `223` | Ticket to email | | `225` | Ticket to meeting | | `227` | Ticket to note | | `229` | Ticket to task | | `84` | Ticket to communication (SMS, WhatsApp, or LinkedIn message) | | `456` | Ticket to postal mail | | `32` | Ticket to thread | | `278` | Ticket to conversation | | `526` | Ticket to order | | `947` | Ticket to appointment | | `941` | Ticket to course | | `943` | Ticket to listing | | `797` | Ticket to service | ### Lead to object | TYPE ID | Association type | | ------- | ---------------------------------------------------------- | | `578` | Lead to primary contact | | `608` | Lead to contact | | `610` | Lead to company | | `596` | Lead to call | | `598` | Lead to email | | `600` | Lead to meeting | | `854` | Lead to note | | `646` | Lead to task | | `602` | Lead to communication (SMS, WhatsApp, or LinkedIn message) | ### Appointment to object | TYPE ID | Association type | | ------- | ----------------------------------------------------------------- | | `966` | Appointment to contact | | `908` | Appointment to company | | `944` | Appointment to deal | | `946` | Appointment to ticket | | `912` | Appointment to call | | `916` | Appointment to email | | `918` | Appointment to meeting | | `920` | Appointment to note | | `922` | Appointment to task | | `924` | Appointment to communication (SMS, WhatsApp, or LinkedIn message) | | `926` | Appointment to postal mail | ### Course to object | TYPE ID | Association type | | ------- | ------------------------------------------------------------ | | `860` | Course to contact | | `938` | Course to company | | `862` | Course to deal | | `940` | Course to ticket | | `866` | Course to call | | `870` | Course to email | | `872` | Course to meeting | | `874` | Course to note | | `876` | Course to task | | `878` | Course to communication (SMS, WhatsApp, or LinkedIn message) | | `880` | Course to postal mail | ### Listing to object | TYPE ID | Association type | | ------- | ------------------------------------------------------------- | | `882` | Listing to contact | | `884` | Listing to company | | `886` | Listing to deal | | `942` | Listing to ticket | | `890` | Listing to call | | `894` | Listing to email | | `896` | Listing to meeting | | `898` | Listing to note | | `900` | Listing to task | | `902` | Listing to communication (SMS, WhatsApp, or LinkedIn message) | | `904` | Listing to postal mail | ### Service to object | TYPE ID | Association type | | ------- | ------------------------------------------------------------- | | `798` | Service to contact | | `792` | Service to company | | `794` | Service to deal | | `796` | Service to ticket | | `840` | Service to call | | `842` | Service to email | | `838` | Service to meeting | | `836` | Service to note | | `852` | Service to task | | `846` | Service to communication (SMS, WhatsApp, or LinkedIn message) | | `848` | Service to postal mail | ### Call to object | TYPE ID | Association type | | ------- | ------------------- | | `194` | Call to contact | | `182` | Call to company | | `206` | Call to deal | | `220` | Call to ticket | | `913` | Call to appointment | | `867` | Call to course | | `891` | Call to listing | | `841` | Call to service | ### Email to object | TYPE ID | Association type | | ------- | -------------------- | | `198` | Email to contact | | `186` | Email to company | | `210` | Email to deal | | `224` | Email to ticket | | `917` | Email to appointment | | `871` | Email to course | | `895` | Email to listing | | `843` | Email to service | ### Meeting to object | TYPE ID | Association type | | ------- | ---------------------- | | `200` | Meeting to contact | | `188` | Meeting to company | | `212` | Meeting to deal | | `226` | Meeting to ticket | | `919` | Meeting to appointment | | `873` | Meeting to course | | `897` | Meeting to listing | | `839` | Meeting to service | ### Note to object | TYPE ID | Association type | | ------- | ------------------- | | `202` | Note to contact | | `190` | Note to company | | `214` | Note to deal | | `228` | Note to ticket | | `921` | Note to appointment | | `875` | Note to course | | `899` | Note to listing | | `837` | Note to service | ### Postal mail to object | TYPE ID | Association type | | ------- | -------------------------- | | `453` | Postal mail to contact | | `459` | Postal mail to company | | `457` | Postal mail to deal | | `455` | Postal mail to ticket | | `927` | Postal mail to appointment | | `881` | Postal mail to course | | `905` | Postal mail to listing | | `849` | Postal mail to service | ### Task to object | TYPE ID | Association type | | ------- | ------------------- | | `204` | Task to contact | | `192` | Task to company | | `216` | Task to deal | | `230` | Task to ticket | | `923` | Task to appointment | | `877` | Task to course | | `901` | Task to listing | | `853` | Task to service | ### Communication (SMS, WhatsApp, or LinkedIn message) to object | TYPE ID | Association type | | ------- | ----------------------------------------------------------------- | | `81` | Communication (SMS, WhatsApp, or LinkedIn Message) to contact | | `87` | Communication (SMS, WhatsApp, or LinkedIn Message) to company | | `85` | Communication (SMS, WhatsApp, or LinkedIn Message) to deal | | `83` | Communication (SMS, WhatsApp, or LinkedIn Message) to ticket | | `925` | Communication (SMS, WhatsApp, or LinkedIn Message) to appointment | | `879` | Communication (SMS, WhatsApp, or LinkedIn Message) to course | | `903` | Communication (SMS, WhatsApp, or LinkedIn Message) to listing | | `847` | Communication (SMS, WhatsApp, or LinkedIn Message) to service | ### Invoice to object | TYPE ID | Association type | | ------- | --------------------------------------- | | `177` | Invoice to contact | | `179` | Invoice to company | | `175` | Invoice to deal | | `407` | Invoice to quote | | `622` | Invoice to subscription | | `815` | Invoice to payment link | | `517` | Invoice to order | | `986` | Invoice to ticket | | `409` | Invoice to line item | | `411` | Invoice to discount | | `413` | Invoice to fee | | `415` | Invoice to tax | | `541` | Invoice to commerce payment | | `691` | Invoice to payment schedule installment | | `679` | Invoice to data sync state | ### Quote to object | TYPE ID | Association type | | ------- | --------------------------------- | | `69` | Quote to contact | | `71` | Quote to company | | `64` | Quote to deal | | `67` | Quote to line item | | `286` | Quote to quote template | | `362` | Quote to discount | | `364` | Quote to fee | | `366` | Quote to tax | | `702` | Contact signer (for e-signatures) | | `733` | Quote to cart | | `408` | Quote to invoice | | `731` | Quote to order | | `398` | Quote to payment | | `304` | Quote to subscription | ### Line item to object | TYPE ID | Association type | | ------- | ---------------------------------- | | `571` | Line item to abandoned cart | | `591` | Line item to cart | | `396` | Line item to commerce payment | | `20` | Line item to deal | | `368` | Line item to discount | | `410` | Line item to invoice | | `514` | Line item to order | | `759` | Line item to payment link | | `68` | Line item to quote | | `302` | Line item to subscription | | `565` | Upcoming line item to subscription | ### Order to object | TYPE ID | Association type | | ------- | ---------------------- | | `593` | Order to cart | | `507` | Order to contact | | `509` | Order to company | | `512` | Order to deal | | `519` | Order to discount | | `521` | Order to discount code | | `518` | Order to invoice | | `513` | Order to line item | | `523` | Order to payment | | `730` | Order to quote | | `516` | Order to subscription | | `726` | Order to task | | `525` | Order to ticket | ### Cart to object | TYPE ID | Association type | | ------- | ----------------- | | `586` | Cart to contact | | `588` | Cart to discount | | `590` | Cart to line item | | `592` | Cart to order | | `732` | Cart to quote | | `728` | Cart to task | | `594` | Cart to ticket | ## v1 associations (legacy) If you're using the v1 associations API, view the table below for information about IDs to use when associating records. | Association type | ID | | ------------------------------- | --- | | Contact to company | 1 | | Company to contact (default) | 2 | | Company to contact (all labels) | 280 | | Deal to contact | 3 | | Contact to deal | 4 | | Deal to company | 5 | | Company to deal | 6 | | Company to engagement | 7 | | Engagement to company | 8 | | Contact to engagement | 9 | | Engagement to contact | 10 | | Deal to engagement | 11 | | Engagement to deal | 12 | | Parent company to child company | 13 | | Child company to parent company | 14 | | Contact to ticket | 15 | | Ticket to contact | 16 | | Ticket to engagement | 17 | | Engagement to ticket | 18 | | Deal to line item | 19 | | Line item to deal | 20 | | Company to ticket | 25 | | Ticket to company | 26 | | Deal to ticket | 27 | | Ticket to deal | 28 | # CRM API | Carts @ Learn about using HubSpot's APIs to create and manage ecommerce cart data. $ guides & api/crm/commerce --- # Carts Use the carts API to create and manage data related to ecommerce purchases in HubSpot. This can be especially useful for keeping HubSpot data synced with external ecommerce platforms, such as Shopify and NetSuite. For example, use the API to sync cart data with Shopify, including associating the cart with an order and contact record. ## Create carts To create a cart, make a `POST` request to `crm/v3/objects/cart`. In the request body, you can include the `properties` and `associations` objects to set property values and associate the cart with other CRM objects (e.g., contacts and line items). Learn more about order properties and associations below. ### Properties Cart details are stored in cart properties. HubSpot provides a set of [default cart properties](#cart-properties), but you can also create your own custom properties using the [properties API](/guides/api/crm/properties). To include properties when creating a cart, add them as fields in a `properties` object in the request body.For example, the request body below would create a cart with some basic product details based on the information provided by the buyer at checkout. ```json { "properties": { "hs_cart_name": "name-of-cart", "hs_external_cart_id": "1234567890", "hs_external_status": "pending", "hs_source_store": "name-of-store", "hs_total_price": "500", "hs_currency_code": "USD", "hs_cart_discount": "12", "hs_tax": "36.25", "hs_shipping_cost": "0", "hs_tags": "frames, lenses" } } ``` The response will include the information you provided during creation along with a few other default properties. ```json { "id": "55262618747", "properties": { "hs_cart_discount": "12", "hs_cart_name": "name-of-cart", "hs_external_cart_id": "1234567890", "hs_created_by_user_id": "959199", "hs_createdate": "2024-04-11T20:42:01.734Z", "hs_currency_code": "USD", "hs_exchange_rate": "1.0", "hs_external_status": "pending", "hs_homecurrency_amount": "500.0", "hs_lastmodifieddate": "2024-04-11T20:42:01.734Z", "hs_object_id": "55262618747", "hs_object_source": "CRM_UI", "hs_object_source_id": "userId:959199", "hs_object_source_label": "CRM_UI", "hs_object_source_user_id": "959199", "hs_shipping_cost": "0", "hs_source_store": "name-of-store", "hs_tags": "frames, lenses", "hs_tax": "36.25", "hs_total_price": "500", "hs_updated_by_user_id": "959199" }, "createdAt": "2024-04-11T20:42:01.734Z", "updatedAt": "2024-04-11T20:42:01.734Z", "archived": false } ``` ### Associations You can associate carts with other HubSpot CRM objects at creation by including an `associations` object. You can also use the [associations API](/guides/api/crm/associations/associations-v4) to update existing carts after creation. In the `associations` array, include the following fields: | Fields | Type | Description | | --- | --- | --- | | `toObjectId` | String | The ID of the record that you want to associate the cart with. | | `associationTypeId` | String | A unique identifier to indicate the association type between the cart and the other object. Below are the CRM objects that you can associate orders with, along with their `associationTypeId`:<ul><li>[Contacts](/guides/api/crm/objects/contacts): `586`</li><li>[Discounts](/guides/api/crm/commerce/discounts): `588`</li><li>[Line items](/guides/api/crm/commerce/line-items): `590`</li><li>[Orders](/guides/api/crm/commerce/orders): `592`</li><li>[Quotes](/guides/api/crm/commerce/quotes): `732`</li><li>[Tasks](/guides/api/crm/engagements/tasks): `728`</li><li>[Tickets](/guides/api/crm/objects/tickets): `594`</li></ul>To see a list of all association types, check out the [associations API documentation](/guides/api/crm/associations/associations-v4#association-type-id-values). Or, you can retrieve each value by making a `GET` request to `/crm/v4/associations/{fromObjectType}/{toObjectType}/labels`. | For example, the `POST` request body below would create a cart that's associated with a specific contact and two line items. ```json { "associations": [ { "to": { "id": 301 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 586 } ] }, { "to": { "id": 1243313490 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 590 } ] }, { "to": { "id": 1243557166 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 590 } ] } ], "properties": { "hs_external_cart_id": "1234567890", "hs_external_status": "pending", "hs_source_store": "name-of-store", "hs_total_price": "500", "hs_currency_code": "USD", "hs_tax": "36.25", "hs_tags": "donuts, bagels" } } ``` ## Retrieve carts Depending on the information you need, there are a few ways to retrieve carts: - To retrieve all carts, make a `GET` request to `/crm/v3/objects/cart`. - To retrieve a specific cart, make a `GET` request to the above URL and specify a cart ID. For example: `/crm/v3/objects/cart/44446244097`. - To retrieve carts that meet a specific set of criteria, you can make a `POST` request to the search endpoint and include filters in the request body. Learn more about [searching the CRM](/guides/api/crm/search#make-a-search-request). The response will include a few default properties, including the create date, last modified date. ```json { "id": "55226265370", "properties": { "hs_createdate": "2024-04-10T18:59:32.441Z", "hs_lastmodifieddate": "2024-04-10T18:59:32.441Z", "hs_object_id": "55226265370" }, "createdAt": "2024-04-10T18:59:32.441Z", "updatedAt": "2024-04-10T18:59:32.441Z", "archived": false } ``` To return specific properties, include a `properties` query parameter in the request URL along with comma-separated property names. For example, making a `GET` request to the following URL would result in the response below: `/crm/v3/objects/cart?properties=hs_external_cart_id&hs_external_status` ```json { "results": [ { "id": "55226265370", "properties": { "hs_createdate": "2024-04-10T18:59:32.441Z", "hs_external_cart_id": "1234567890", "hs_lastmodifieddate": "2024-04-10T18:59:32.441Z", "hs_object_id": "55226265370" }, "createdAt": "2024-04-10T18:59:32.441Z", "updatedAt": "2024-04-10T18:59:32.441Z", "archived": false }, { "id": "55262618747", "properties": { "hs_createdate": "2024-04-11T20:42:01.734Z", "hs_external_cart_id": "8675309", "hs_lastmodifieddate": "2024-04-11T20:42:01.734Z", "hs_object_id": "55262618747" }, "createdAt": "2024-04-11T20:42:01.734Z", "updatedAt": "2024-04-11T20:42:01.734Z", "archived": false } ] } ``` To view all available cart properties, use the [properties API](/guides/api/crm/properties) by making a `GET` request to `crm/v3/properties/cart`. Learn more about [cart properties](#cart-properties). ### Search for carts by properties You can use the search endpoint to retrieve carts that meet a specific set of [filter criteria](/guides/api/crm/search#filter-search-results). This will be a `POST` request that includes your filter criteria in the request body. For example, to search for all carts placed at a specific store, you would make a `POST` request to `crm/v3/objects/cart/search` with the following request body: ```json { "filterGroups": [ { "filters": [ { "propertyName": "hs_source_store", "value": "Cat Cafe - Portland", "operator": "EQ" } ] } ], "properties": ["hs_external_cart_id", "hs_source_store"] } ``` ### Retrieve a cart with associations To retrieve a cart and the contact associated with it, make a `GET` request to: `crm/v3/objects/cart/{cartId}/associations/contact` This will return the IDs of the currently associated contact, along with meta information about the association type. ```json { "results": [ { "id": "301", "type": "cart_to_contact" } ] } ``` You can then use the returned IDs to request more information about the contact through the [contacts API](/guides/api/crm/objects/contacts). Using the above example response, you would make a `GET` request to `crm/v3/objects/contacts/301`. ```json { "id": "301", "properties": { "createdate": "2022-09-27T13:13:31.004Z", "email": "tom.bombadil@oldforest.com", "firstname": "Tom", "hs_object_id": "301", "lastmodifieddate": "2023-11- 07T17:14:00.841Z", "lastname": "Bombadil" }, "createdAt": "2022-09-27T13:13:31.004Z", "updatedAt": "2023-11-07T17:14:00.841Z", "archived": false } ``` Note that the `filters` array specifies the search criteria, while the `properties` array specifies which properties to return. ## Update carts To update a cart, make a `PATCH` request to `/crm/v3/objects/cart/{cartId}`. In the request body, include a `properties` object containing the properties that you want to update. For example, if you wanted to update a cart after it's been fulfilled, you could send the following request body: ```json { "properties": { "hs_external_status": "fulfilled" } } ``` The response will include a set of default properties along with the property that you just set. ```json { "id": "55263075218", "properties": { "hs_created_by_user_id": "959199", "hs_createdate": "2024-04-11T20:52:47.212Z", "hs_external_status": "fulfilled", "hs_lastmodifieddate": "2024-04-11T20:56:00.234Z", "hs_object_id": "55263075218", "hs_updated_by_user_id": "959199" }, "createdAt": "2024-04-11T20:52:47.212Z", "updatedAt": "2024-04-11T20:56:00.234Z", "archived": false } ``` To update the associations for an existing cart, make a `PUT` request to `/crm/v3/objects/cart/{cartId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}`. You can also use the [associations API](/guides/api/crm/associations/associations-v4). See the [associations section](#associations) for `associationTypeId` values for cart-to-object associations. You can also make a `GET` request to `/crm/v4/associations/{fromObjectType}/{toObjectType}/labels`. To see all a list of all values, check out the [associations API documentation](/guides/api/crm/associations/associations-v4#association-type-id-values). For example, to associate a cart with an existing order, you would make a `PUT` request to the following URL: `/crm/v3/objects/cart/{cartId}/associations/order/{orderId}/592` The response will return a set of default properties along with an `associations` object containing information about the association that you set. ```json { "id": "55262618747", "properties": { "hs_createdate": "2024-04-11T20:42:01.734Z", "hs_lastmodifieddate": "2024-04-11T20:42:01.734Z", "hs_object_id": "55262618747" }, "createdAt": "2024-04-11T20:42:01.734Z", "updatedAt": "2024-04-11T20:42:01.734Z", "archived": false, "associations": { "orders": { "results": [ { "id": "54807577390", "type": "cart_to_order" } ] } } } ``` To remove an association from an existing cart, make a `DELETE` request to the following URL: `/crm/v3/objects/cart/{cartId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}` For example, if you wanted to remove an associated line item from a cart, your request URL would be the following: `/crm/v3/objects/cart/{cartId}/associations/line_items/{lineItemId}/590` ## Cart properties When managing your cart data, you may want to use some of the common properties in the table below. To get all cart properties, make a `GET` request to `/crm/v3/properties/cart`. Learn more about using the [properties API](/guides/api/crm/properties). | Property name | Label in UI | Description | | --- | --- | --- | | `hs_cart_name` | Name | The name in an external system. | | `hs_external_cart_id` | Cart ID | Unique identifier from an external system. | | `hs_source_store` | Source Store | Data used to identify which store the cart came from. | | `hs_external_status` | Status | The current status of the cart. | | `hs_cart_url` | Cart Url | The recovery URL that's sent to a customer so they can revive an abandoned cart. | | `hs_total_price` | Total Price | The sum total amount associated with the cart. | | `hs_currency_code` | Currency Code | The currency code used in the cart. | | `hs_cart_discount` | Cart Discount | Amount of discount in the cart. | | `hs_tags` | Tags | A collection of tag strings for the cart. | # Create and manage subscriptions (BETA) @ Learn about using HubSpot APIs to create and manage subscriptions. $ guides & api/crm/commerce --- # Create and manage subscriptions (BETA) You can use the API endpoints listed below to create, update, and delete [commerce subscriptions](https://knowledge.hubspot.com/subscriptions/manage-subscriptions-for-recurring-payments) in an account. If you need to fetch information about existing subscriptions, check out [this article](/guides/api/crm/commerce/subscriptions). If you're looking to manage marketing email subscriptions instead, check out the [subscription preferences API](/guides/api/marketing/subscriptions-preferences). : This API is currently in beta and is subject to change based on testing and feedback. By using these endpoints you agree to adhere to HubSpot's [Developer Terms](https://legal.hubspot.com/hs-developer-terms)& [Developer Beta Terms](https://legal.hubspot.com/hubspot-beta-terms). You also acknowledge and understand the risk associated with testing an unstable API. ## Requirements To use this API, the account must be set up to collect payments through either [HubSpot payments](https://knowledge.hubspot.com/payment-processing/set-up-payments) or [Stripe payment processing](https://knowledge.hubspot.com/payment-processing/connect-your-stripe-account-as-a-payment-processor-in-hubspot). Based on your use-case, you'll also need to ensure that your app has authorized the following scopes: - `crm.objects.subscriptions.read`: grants access to view details about existing subscriptions. - `crm.objects.subscriptions.write`: grants access to create, update, and delete subscriptions. - `crm.schemas.subscriptions.read`: view details about property settings for subscriptions. - `crm.objects.line_items.read`: grants access to view details about existing line items. - `crm.objects.line_items.write`: grants access to create, update, and delete line items. - `crm.schemas.line_items.read`: view details about property settings for line items. ## Create a subscription with a line item To create a subscription, you'll first need to create a line item, which you'll then associate with the subscription. Once associated, you can associate additional CRM records such as contacts or companies with your subscription. ### Create a line item To create a line item, make a `POST` request to `/crm/v3/objects/line_items`. In the body of your request, include the line item's details, such as its name, quantity, and price. ```json { "properties": { "price": 10, "quantity": 1, "name": "New standalone line item" } } ``` Once you've successfully created a line item, the response will include its ID, which you can then use when you associate it with your subscription. Learn more about the [line items API](/guides/api/crm/commerce/line-items). ### Create a subscription To create a subscription, make a `POST` request to `/crm/v3/objects/subscription`. In the body of your request, include any details about your subscription, such as its name, collection process, and currency code. A list of common properties is provided in [the table below](#properties). By default, a subscription is non-billable, which means its `hs_invoice_creation` property is set to `"off"`. Once your subscription has been created, you can set it to billable by following the instructions in the sections below. An example request body for a billable subscription is provided below: ```json { "properties": { "hs_name": "New billable subscription", "hs_collection_process": "manual_payments", "hs_currency_code": "USD", "hs_net_payment_terms": "30", "hs_recurring_billing_start_date": {{startDate}}, "hs_recurring_billing_frequency": "monthly" } } ``` ### Properties To view all available subscription properties, make a `GET` request to `crm/v3/properties/subscriptions`. Learn more about using the [properties API](/guides/api/crm/properties). Below are some common subscription properties that you may want to specify in your request. Learn more about fetching subscriptions in [this article](/guides/api/crm/commerce/subscriptions). | Property name | Label in UI | Description | | --- | --- | --- | | `hs_status` | [Status](https://knowledge.hubspot.com/payments/hubspots-payments-and-subscriptions-properties#subscription-properties:~:text=the%20status%20of%20the%20subscription) | The current status of the subscription. Values include:<ul><li>`active`</li><li>`past_due`</li><li>`canceled`</li><li>`expired`</li><li>`scheduled`</li></ul> | | `hs_recurring_billing_start_date` | [Start date](https://knowledge.hubspot.com/payments/hubspots-payments-and-subscriptions-properties#subscription-properties:~:text=for%20the%20subscription.-,Start%20date%3A,-the%20date%20the) | The date that the subscription is scheduled to start. | | `hs_last_payment_amount` | [Last payment amount](https://knowledge.hubspot.com/payments/hubspots-payments-and-subscriptions-properties#subscription-properties:~:text=is%20updated%20automatically.-,Last%20payment%20amount,-%3A%C2%A0the%20amount) | The amount that was charged during the most recent billing period. | | `hs_next_payment_amount` | [Next payment amount](https://knowledge.hubspot.com/payments/hubspots-payments-and-subscriptions-properties#subscription-properties:~:text=of%20the%20subscription.-,Next%20payment%20amount,-%3A%20the%20amount) | The amount that will be charged at the start of the next billing period. | | `hs_next_payment_due_date` | [Next payment due date](https://knowledge.hubspot.com/payments/hubspots-payments-and-subscriptions-properties#subscription-properties:~:text=Next%20payment%20due%20date) | The date that the next payment is due. | ### Associate a subscription with your line item With your subscription created, you can associate it with the line item you created. To create this association, make a `PUT` request to `/crm/v4/objects/subscription/{subscriptionId}/associations/line_item/{lineItemId}`, using the ID of your subscription as the `subscriptionId` and the ID of your line item as the `lineItemId`. In the request body, include the `associationCategory` and `associationTypeId` shown in the code block below that specifies the _Subscription to Line Item_ association: ```json [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 301 } ] ``` ### Associate a subscription with a CRM record Now that your subscription is associated with a line item, you can also associate it with other CRM records, such as a contact or company. Learn more about the associations API in [this article](https://developers.hubspot.com/docs/guides/api/crm/associations/associations-v4), which provides a full list of association type ID values. For example, if you wanted to associate your subscription with a contact, you would make a `PUT` request to `/crm/v4/objects/subscription/{subscriptionId}/associations/contact/{contactId}`, using the ID of your subscription as the `subscriptionId` and the ID of the contact as the `contactId`. In the request body, include the `associationCategory` and `associationTypeId` shown below that specifies the _Subscription to Contact_ association: ```json [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 295 } ] ``` ## Make a subscription billable To set the subscription as billable, make a `PATCH` request to `/crm/v3/objects/subscription/{subscriptionId}` and set the `hs_invoice_creation` property to `"on"` in the request body. The following properties can updated on a billable subscription once it's been created: - `hs_name` - `hs_owner` - `hs_automatically_email_invoices` - `hs_allowed_payment_methods` - `hs_collect_address_types` - `hs_invoice_comments` - `hs_language` - `hs_locale` - `hs_name` - `hs_net_payment_terms` - `hs_recipient_billing_address` - `hs_recipient_billing_address2` - `hs_recipient_billing_city` - `hs_recipient_billing_country` - `hs_recipient_billing_state` - `hs_recipient_billing_zip` - `hs_payment_enabled` - `hs_tax_ids` - `hubspot_owner_id` Billable subscriptions can only be deleted if they've expired or they've been cancelled. ## Update a subscription To update other properties of a subscription after you've created it, you can make a `PATCH` request to `/crm/v3/objects/subscription/{subscriptionId}`, using the ID of the subscription as the `subscriptionId`, and including any fields you want to change in the properties field of the request body. For example, the following request body would change several address fields for the subscription: ```json { "properties": { "hs_recipient_billing_address_source": "from_subscription", "hs_recipient_address": "2 Canal Park", "hs_recipient_city": "Cambridge", "hs_recipient_country": "US", "hs_recipient_state": "MA", "hs_recipient_zip": "02141" } } ``` ## Delete a subscription To delete a subscription, make a `DELETE` request to `/crm/v3/objects/subscription/{subscriptionId}` using the ID of the subscription you want to delete as the subscriptionId: - If you're attempting to delete a billable subscription, keep in mind that you can only delete subscriptions that have expired or that've been cancelled. - All non-billable subscriptions can be deleted, regardless of their status. # CRM API | Create invoices (BETA) @ Invoices are used to bill customers. The invoices endpoints allow you to to create, update and retrieve invoices. $ guides & api/crm/commerce --- # Invoices Use the invoices API to create, update and retrieve invoices ([refer to this documentation to fetch invoice information](/guides/api/crm/commerce/invoices)). Once configured, an invoice can be shared with a buyer at a specified URL. Users can also [manage invoices in HubSpot](https://knowledge.hubspot.com/invoices/create-invoices#manage-invoices) to add details, update associations, and more. If you’ve set up either [HubSpot payments](https://knowledge.hubspot.com/payment-processing/set-up-payments) or [Stripe payment processing](https://knowledge.hubspot.com/payment-processing/connect-your-stripe-account-as-a-payment-processor-in-hubspot), you can configure an invoice to be payable through this API. **Example use case:** You need to automatically create an invoice when a contract is signed by a customer. Below, learn how to create an invoice through the API and configure its various properties, associations, states, and more. ## Overview The invoice creation process can be broken up into the following steps: 1. **Create an invoice:** create a draft invoice and set the currency. 2. **Configure the invoice:** add associations to CRM records, such as a contact and line items to the invoice, properties, and configure payment settings. 3. **Move the invoice status to open:** move the invoice status from `draft` to `open`. 4. **Share the invoice:** share the invoice with the buyer. ## Create an invoice To create an invoice, you'll first configure its basic details by making a `POST` request to `/crm/v3/objects/invoices`. In the post body, include a `properties` object to store basic invoice information. The only property required for creating a draft invoice is `hs_currency`. You can modify a drafted invoice’s properties any time by [updating the invoice](#configure-an-invoice). Additionally, the invoice will inherit other property values when you later associate a contact with it, such as contact and company details, which will be required before you can set the invoice to the _Open_ state. [Learn more about required properties and associations](#configure-an-invoice). Depending on your preferred workflow, you can instead create a draft invoice with associations through one POST request, rather than making two separate requests. [Learn more about creating a draft invoice with one request](#create-a-draft-with-associations-single-request-). ```json { "properties": { "hs_currency": "USD" } } ``` The response will include an `id`, which you'll use to continue configuring the invoice. You can update invoice properties at any time by making a `PATCH` request to `/crm/v3/objects/invoices/{invoiceId}`. ## Configure an invoice To move the invoice to the `open` status so that it can be shared and paid, you’ll need to set a few required properties and associate it with other CRM records, such as line items and a contact. If you’ve set up [HubSpot payments](https://knowledge.hubspot.com/payment-processing/set-up-payments) or [Stripe payment processing](https://knowledge.hubspot.com/payment-processing/connect-your-stripe-account-as-a-payment-processor-in-hubspot), you can also configure the invoice’s payment settings. Below, learn more about: - Updating invoice properties. - Adding associations. - Configuring payment settings. ### Update invoice properties To update an invoice’s properties, make a `PATCH` request to `/crm/v3/objects/invoices/{invoiceId}`. In the request body, include a properties object with fields for the properties you want to update. ```json { "properties": { "hs_currency": "USD", "hs_purchase_order_number": "12345" } } ``` The table below lists which properties are required for an invoice, and others that are autoset. [Click here to see a list of common properties](#common-properties). | Property | Description | | --- | --- | | `hs_currency` | Currency of the invoice. | | `hs_invoice_date` | Date of the invoice. This is autoset if a date isn't provided. | | `hs_due_date` | Due date of the invoice. This is autoset if a date isn't provided. | | `hs_tax_id` | The account tax ID listed on this invoice. The tax ID includes a tax ID type and a tax ID number. This property must match a tax ID in the account settings. Or, it will autoset. [Learn more about tax ID’s](https://knowledge.hubspot.com/invoices/set-up-the-hubspot-invoices-tool#add-a-tax-id).[](https://knowledge.hubspot.com/invoices/set-up-the-hubspot-invoices-tool#add-a-tax-id) | ### Add associations To associate other CRM records with the invoice, make a PUT request to `/crm/v4/objects/invoices/{fromObjectId}/associations/default/{toObjectType}/{toObjectId}`. The table below show which CRM record associations are required to move the invoice to the Open state, and which are optional. [View a full list of the objects can be associated with invoices](/guides/api/crm/associations/associations-v4#invoice-to-object). A _Draft_ invoice can be created without any associations. | Object type | Required | Description | | --- | --- | --- | | [Line items](/guides/api/crm/commerce/line-items) | Yes | The goods and/or services being sold through the invoice. You can create line items from [products in your product library](/guides/api/crm/commerce/products) or create custom standalone line items. | | [Contact](/guides/api/crm/objects/contacts) | Yes | Specific buyers that you're addressing in the invoice. | | [Company](/guides/api/crm/objects/companies) | No | A specific company that you're addressing in the invoice. | | [Discounts](/guides/api/crm/commerce/discounts), [fees](/guides/api/crm/commerce/fees), and [taxes](/guides/api/crm/commerce/taxes) | No | Any discounts, fees, and taxes to be applied at checkout. When determining the total invoice amount, HubSpot first applies discounts, followed by fees, then taxes. You can use the `hs_sort_order` field to reorder objects of the same type. Can be set to fixed values or percentages by setting `hs_type` to either `FIXED` or `PERCENT`. | ### Retrieving IDs for associations To make each association, you'll first need to retrieve the ID of each object you want to associate. To retrieve each ID, you'll make a `GET` request to the relevant object endpoint, which follows the same pattern across each CRM object. When making each request, you can also include a `properties` query parameter to return specific properties when needed. Below are example `GET` requests for each type of object. ```hubl GET request for line items /crm/v3/objects/line_items?properties=name GET request for contacts /crm/v3/objects/contacts?properties=email GET request for companies /crm/v3/objects/companies?properties=name GET request for discounts crm/v3/objects/discounts?properties=hs_type,hs_value GET request for fees crm/v3/objects/fees?properties=hs_type,hs_value GET request for taxes crm/v3/objects/taxes?properties=hs_type,hs_value ``` ```shell #GET request for line items curl --request GET \ --url 'https://api.hubapi.com/crm/v3/properties/line_items?archived=false' \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #GET request for contacts curl --request GET \ --url 'https://api.hubapi.com/crm/v3/properties/contacts?archived=false' \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #GET request for companies curl --request GET \ --url 'https://api.hubapi.com/crm/v3/properties/companies?archived=false' \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #GET request for discounts curl --request GET \ --url 'https://api.hubapi.com/crm/v3/properties/discounts?archived=false' \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #GET request for fees curl --request GET \ --url 'https://api.hubapi.com/crm/v3/properties/fees?archived=false' \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #GET request for taxes curl --request GET \ --url 'https://api.hubapi.com/crm/v3/properties/taxes?archived=false' \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' ``` Each successful call will return a `200` response with details for each fetched object type. You'll use the value in the `id` field to set associations in the next step. ```json { "results": [ { "id": "54486102441", "properties": { "hs_createdate": "2024-03-18T07:34:29.586Z", "hs_lastmodifieddate": "2024-03-18T07:34:29.586Z", "hs_object_id": "54486102441", "hs_type": "PERCENT", "hs_value": "20.0000" }, "createdAt": "2024-03-18T07:34:29.586Z", "updatedAt": "2024-03-18T07:34:29.586Z", "archived": false } ] } ``` ### Creating associations With your IDs retrieved, you can now make calls to the [associations API](/guides/api/crm/associations/associations-v4) to create associations. For each type of object you want to associate with an invoice, you'll need to make a separate call by making a `PUT` request using the URL structure below: `/crm/v4/objects/invoices/{fromObjectId}/associations/default/{toObjectType}/{toObjectId}` | Parameter | Description | | --- | --- | | `fromObjectId` | The ID of the invoice. | | `toObjectType` | The type of object you're associating with. For example, `line_items`, `contact`. | | `toObjectId` | The ID of the object you're associating the invoice with. | Below are example `PUT` requests for each type of object. ```hubl PUT request to associate a line item /crm/v4/objects/invoices/{invoiceID}/associations/default/line_items/{lineItemId} PUT request to associate contacts /crm/v4/objects/invoices/{invoiceID}/associations/default/contacts/{contactId} PUT request to associate companies /crm/v4/objects/invoices/{invoiceID}/associations/default/companies/{companyId} PUT request to associate discounts /crm/v4/objects/invoices/{invoiceID}/associations/default/discounts/{discountId} PUT request to associate fees /crm/v4/objects/invoices/{invoiceID}/associations/default/fees/{feeId} PUT request to associate taxes /crm/v4/objects/invoices/{invoiceID}/associations/default/taxes/{taxId} ``` ```shell #PUT request to associate line items curl --request PUT \ --url https://api.hubapi.com/crm/v4/objects/invoices/{invoiceID}/associations/default/line_items/{lineItemId} \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #PUT request to associate contacts curl --request PUT \ --url https://api.hubapi.com/crm/v4/objects/invoices/{invoiceID}/associations/default/contacts/{contactId} \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #PUT request to associate a company curl --request PUT \ --url https://api.hubapi.com/crm/v4/objects/invoices/{invoiceID}/associations/default/companies/{companyId} \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #PUT request to associate discounts curl --request PUT \ --url https://api.hubapi.com/crm/v4/objects/invoices/{invoiceID}/associations/default/discounts/{discountId} \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #PUT request to associate fees curl --request PUT \ --url https://api.hubapi.com/crm/v4/objects/invoices/{invoiceID}/associations/default/fees/{feeId} \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #PUT request to associate taxes curl --request PUT \ --url https://api.hubapi.com/crm/v4/objects/invoices/{invoiceID}/associations/default/line_items/{taxId} \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' ``` As an example, if your invoice has an ID of `123456`, the requests to associate the invoice might include the following: - **Line items (IDs: `55555`, `66666`):** - `/crm/v4/objects/invoices/123456/associations/default/line_items/55555` - `/crm/v4/objects/invoices/123456/associations/default/line_items/66666` - **Contact (ID:** `34545`**):** `/crm/v4/objects/invoices/123456/associations/default/contacts/34545` Each successful association will return a `200` response with details about the association. The above calls will associate the objects in both directions, with each direction have its own ID. For example, if you associate the invoice with a contact, the response will describe the association from both ends. In the example response below, `178` is the contact-to-invoice association type ID, and `177` is the invoice-to-contact association type ID. ```json { "status": "COMPLETE", "results": [ { "from": { "id": "2171512734" }, "to": { "id": "375894146440" }, "associationSpec": { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 178 } }, { "from": { "id": "375894146440" }, "to": { "id": "2171512734" }, "associationSpec": { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 177 } } ], "startedAt": "2024-10-25T10:51:43.821Z", "completedAt": "2024-10-25T10:51:43.925Z" } ``` ### Make an invoice non-billable A non-billable invoice won't render, meaning an invoice document won't be created, and other invoice functionalities, such as validation, email and notifications will not be included. This could be useful if you want to track invoice data in HubSpot, but manage or bill the invoices outside of HubSpot. To make an invoice non-billable, create an invoice, making a `POST` request to `/crm/v3/objects/invoices` and include the `hs_invoice_billable` property. ```json { "properties": { "hs_currency": "USD", "hs_invoice_billable": "false" } } ``` ## Configure payment settings - Recording payments manually is not possible via the API but is [possible in the platform](https://knowledge.hubspot.com/invoices/create-invoices#manage-finalized-invoices). - The HubSpot account must have either [HubSpot payments](https://knowledge.hubspot.com/payment-processing/set-up-payments) or [Stripe payment processing](https://knowledge.hubspot.com/payment-processing/connect-your-stripe-account-as-a-payment-processor-in-hubspot) set up before using this capability. If your account is set up to collect digital payments using HubSpot payments or Stripe payment processing, and the currency of the invoice is set to a [currency that is valid for transacting](https://knowledge.hubspot.com/payment-processing/configure-currencies-for-commerce-hub#available-currencies), the invoice will automatically be configured to collect payments. In addition, other properties will automatically be set based on the account [payment settings](https://knowledge.hubspot.com/payment-processing/configure-your-buyer-checkout-experience#set-the-default-payment-method-and-shipping-information). The properties below can be amended if required. This is not an exhaustive list. To get a list of all available properties, make a `GET` request to `/crm/v3/objects/invoices`. | Parameter | Type | Description | | --- | --- | --- | | `hs_allowed_payment_methods` | Enumeration | The payment methods to be used. For example, `credit_or_debit_card`, `ach`. <ul><li>[Accepted payment methods when using HubSpot payments](https://knowledge.hubspot.com/payment-processing/set-up-payments).</li><li>[Accepted payment methods when using Stripe as a payment processor](https://knowledge.hubspot.com/payment-processing/connect-your-stripe-account-as-a-payment-processor-in-hubspot#configure-currencies-and-payment-methods).</li></ul>If no value, autoset based on the account [payment settings](https://knowledge.hubspot.com/payment-processing/configure-your-buyer-checkout-experience#set-the-default-payment-method-and-shipping-information).[](https://knowledge.hubspot.com/payment-processing/configure-your-buyer-checkout-experience#set-the-default-payment-method-and-shipping-information) | | `hs_invoice_payment_collection_method` | Enumeration | Set to `manual` if you want the invoice to have digital payment capability. Set to `none` if you don't want digital payment capability. If no value, autoset based on the account [payment settings](https://knowledge.hubspot.com/payment-processing/configure-your-buyer-checkout-experience#set-the-default-payment-method-and-shipping-information).[](https://knowledge.hubspot.com/payment-processing/configure-your-buyer-checkout-experience#set-the-default-payment-method-and-shipping-information) | | `hs_collect_address_types` | Boolean | The types of addresses that are collected while paying the invoice. Value can be `billing_address` and/or `shipping_address`. | | `hs_recipient_shipping_address` | String | The shipping address of the recipient. | | `hs_recipient_shipping_city` | String | The shipping city of the recipient. | | `hs_recipient_shipping_state` | String | The shipping state or region of the recipient. | | `hs_recipient_shipping_zip` | String | The shipping postal or zip code of the recipient. | ## Add taxes to an invoice You can add taxes to an invoice in two different ways depending on your use case: - Add taxes to individual line items by setting the `hs_tax_rate_group_id` property when you create an invoice. You'll first need to [set up tax rates](https://knowledge.hubspot.com/payments/create-and-use-payment-discount-codes#add-a-tax-rate-to-the-tax-library) in your account before associating them with line items. You'll need to authorize the `tax_rates.read` scope for your app to fetch tax rates and the `crm.objects.line_items.write` scope to update or create a line item. - Use the [tax object](/guides/api/crm/commerce/taxes) to add taxes to the entire invoice, applying taxes to the total amount of the invoice. ### Add taxes to line items To associate a tax rate with a line item of an invoice, first make a GET request to `/tax-rates/v1/tax-rates` to fetch all tax rates. The resulting response will resemble the following: ```json { "results": [ { "name": "state-sales-tax", "percentageRate": 1, "label": "State sales tax 2025", "active": true, "id": "15970230", "createdAt": "2025-02-25T21:57:20.703Z", "updatedAt": "2025-02-25T21:57:20.703Z" } ], "paging": { "next": { "after": "MTU5NzAyMzA%3D", "link": "https://api.hubspot.com/tax-rates/v1/tax-rates?after=MTU5NzAyMzA%3D" } } } ``` You can then use the `id` of one of your tax rates as the `hs_tax_rate_group_id` when creating or updating a line item. Learn more about setting up tax rates in the [line items API guide](/guides/api/crm/commerce/line-items#create-a-line-item). ### Add taxes to the entire invoice order Rather than apply an existing tax rate to individual line items, you can instead apply a tax to the [entire invoice order](https://knowledge.hubspot.com/products/edit-products-and-terms-in-the-line-items-editor#add-discounts-fees-and-taxes-to-an-invoice). An invoice-level tax has a 1:1 relationship with the invoice, meaning the tax must be unique to the invoice. However, an invoice can include multiple taxes. To add taxes to an entire invoice order: - [Create a tax](https://developers.hubspot.com/docs/reference/api/crm/commerce/taxes#post-%2Fcrm%2Fv3%2Fobjects%2Ftaxes). - [Create an invoice](#create-an-invoice). - Use the [associations API to associate the invoice to the tax](https://developers.hubspot.com/docs/guides/api/crm/associations/associations-v4#invoice-to-object). ## Move the invoice to the Open status An invoice can be set to one of four statuses, as set by the `hs_invoice_status` property: | Status | Description | | --- | --- | | `draft` | The invoice is being edited and is not yet ready to be sent. | | `open` | The invoice has all of the details needed to be sent to the buyer, and is payable. | | `paid` | The buyer has paid the invoice. | | `voided` | The invoice has been voided. | When creating an invoice, you may want to set its status to `draft` until it's ready to be sent to the buyer. Once all of the [required properties and associations](#configure-an-invoice) are added to the invoice, you can update its status to `open`, meaning the invoice can be shared and is payable. To move the invoice to the `open` status, make a `PATCH` request to `/crm/v3/objects/invoices` and set the `hs_invoice_status` property to `open`. To move an invoice to the _Open_ status (open invoice that is payable), one contact and at least one line item must be associated. ```json { "properties": { "hs_invoice_status": "open" } } ``` ## Retrieve invoices You can retrieve invoices individually or in batches. - To [retrieve an individual invoice](reference/api/crm/commerce/invoices#get-%2Fcrm%2Fv3%2Fobjects%2Finvoices%2F%7Binvoiceid%7D), make a `GET` request to `/crm/v3/objects/invoices/{invoiceID}`. - To [request a list of all invoices](/reference/api/crm/commerce/invoices#get-%2Fcrm%2Fv3%2Fobjects%2Finvoices) , make a `GET` request to `/crm/v3/objects/invoices`. For these endpoints, you can include the following query parameters in the request URL: | Parameter | Description | | --- | --- | | `properties` | A comma separated list of the properties to be returned in the response. If the requested invoice doesn't have a value for a property, it will not appear in the response. | | `propertiesWithHistory` | A comma separated list of the current and historical properties to be returned in the response. If the requested invoice doesn't have a value for a property, it will not appear in the response. | | `associations` | A comma separated list of objects to retrieve associated IDs for. Any specified associations that don't exist will not be returned in the response. Learn more about the [associations API](/guides/api/crm/associations/associations-v4). | To [retrieve a batch of invoices](/reference/api/crm/commerce/invoices#post-%2Fcrm%2Fv3%2Fobjects%2Finvoices%2Fbatch%2Fread) by their IDs, make a `POST` request to `crm/v3/objects/invoices/batch/read` and include the IDs in the request body. You can also include a `properties` array to specify which properties to return. The batch endpoint cannot retrieve associations. Learn how to batch read associations with the [associations API](/guides/api/crm/associations/associations-v4). ```json { "inputs": [{ "id": "59970098434" }, { "id": "375806106904" }], "properties": ["hs_invoice_status", "hs_language", "hs_createdate"] } ``` ## Delete invoices You can delete invoices with a `hs_invoice_status` of `draft` and `open` (if [enabled in your account settings](https://knowledge.hubspot.com/invoices/set-up-the-hubspot-invoices-tool#turn-on-the-ability-to-delete-unpaid-invoices)). - To [delete one invoice](/reference/api/crm/commerce/invoices#delete-%2Fcrm%2Fv3%2Fobjects%2Finvoices%2F%7Binvoiceid%7D) by ID, make a `DELETE` request to `/crm/v3/objects/invoices/{invoiceId}`. - To [delete a batch](/reference/api/crm/commerce/invoices#post-%2Fcrm%2Fv3%2Fobjects%2Finvoices%2Fbatch%2Farchive) of invoices by ID, make a `POST` request to `/crm/v3/objects/invoices/batch/archive` with the invoice IDs specified in the request body: ```json { "inputs": [ { "id": "1234567" }, { "id": "8675309" } ] } ``` ## Reference ### Common properties Below is a list of properties commonly used. This is not an exhaustive list. To get a list of all available properties, make a `GET` request to `/crm/v3/objects/invoices`. | Parameter | Type | Description | | --- | --- | --- | | `hs_currency` | String | Currency of the invoice. | | `hs_invoice_date` | Date/time | Date of the invoice. Autoset if one is not provided. | | `hs_due_date` | Date/time | Due date of the invoice. Autoset if one is not provided. | | `hs_purchase_order_number` | String | Add an associated PO number. | | `hs_comments` | String | Add comments to the invoice for the buyer to see. | | `hs_tax_id` | Enumeration | The account tax ID listed on this invoice. The tax ID includes a tax ID type and a tax ID number. This property must match a tax ID in the account settings. Or, it will autoset. [Learn more about tax ID’s](https://knowledge.hubspot.com/invoices/set-up-the-hubspot-invoices-tool#add-a-tax-id). | | `hs_allowed_payment_methods` | Enumeration | The payment methods to be used. For example, `credit_or_debit_card`, `ach`. <ul><li>[Accepted payment methods when using HubSpot payments](https://knowledge.hubspot.com/payment-processing/set-up-payments).</li><li>[Accepted payment methods when using Stripe as a payment processor](https://knowledge.hubspot.com/payment-processing/connect-your-stripe-account-as-a-payment-processor-in-hubspot#configure-currencies-and-payment-methods).</li></ul>If no value, autoset based on the account [payment settings](https://knowledge.hubspot.com/payment-processing/configure-your-buyer-checkout-experience#set-the-default-payment-method-and-shipping-information).[](https://knowledge.hubspot.com/payment-processing/configure-your-buyer-checkout-experience#set-the-default-payment-method-and-shipping-information) | | `hs_invoice_payment_collection_method` | Enumeration | Set to `manual` if you want the invoice to have digital payment capability. Set to `none` if you don't want digital payment capability. If no value, autoset based on the account [payment settings](https://knowledge.hubspot.com/payment-processing/configure-your-buyer-checkout-experience#set-the-default-payment-method-and-shipping-information).[](https://knowledge.hubspot.com/payment-processing/configure-your-buyer-checkout-experience#set-the-default-payment-method-and-shipping-information) | | `hs_allow_partial_payments` | Boolean | Allow your customer to pay less than the balance due. | | `hs_collect_address_types` | Boolean | The types of addresses that are collected while paying the invoice. Value can be `billing_address` and/or `shipping_address`. | | `hs_recipient_shipping_address` | String | The shipping address of the recipient. This address is used as the billing address. | | `hs_recipient_shipping_address2` | String | The second line of the shipping address of the recipient. This address is used as the billing address. | | `hs_recipient_shipping_city` | String | The shipping city of the recipient. | | `hs_recipient_shipping_state` | String | The shipping state or region of the recipient. | | `hs_recipient_shipping_zip` | String | The shipping postal or zip code of the recipient. This postal or zip code is used as the billing postal or zip code. | | `hs_recipient_shipping_country` | String | The shipping country of the recipient. | | `hs_recipient_company_address` | String | The address of the recipient's company. | | `hs_recipient_company_address2` | String | The second line of the address of the recipient's company. | | `hs_recipient_company_city` | String | The city of the recipient's company. | | `hs_recipient_company_state` | String | The state or region of the recipient's company. | | `hs_recipient_company_zip` | String | The postal or zip code of the recipient's company. | | `hs_custom_fields` | Enumeration | The custom fields that have been added to the invoice. [Learn more about the configuration of this setting](https://knowledge.hubspot.com/invoices/set-up-the-hubspot-invoices-tool#add-or-remove-custom-fields). | | `hs_invoice_link` | String | The URL of the invoice. | | `hs_language` | String | Language of the invoice.<br /><br /> | | `hs_locale` | String | Locale of the invoice. | | `hs_pdf_download_link` | String | PDF download link. | | `hs_invoice_billable` | Boolean | Whether the invoice is billable or not. | ### Create a draft with associations (single request) The following request body will create a new draft invoice with an association to a contact. `POST` `/crm/v3/objects/invoices/batch/create` ```json { "inputs": [ { "properties": { "hs_currency": "USD", "hs_invoice_date": "1729859279", "hs_due_date": "1732537679" }, "associations": [ { "to": { "id": "2171512734" }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 177 } ] } ] } ] } ``` | Parameter | Type | Description | | --- | --- | --- | | `hs_currency` | String | Currency of the invoice. | | `hs_invoice_date` | Date/time | Date of the invoice. Autoset if one is not provided. | | `hs_due_date` | Date/time | Due date of the invoice. Autoset if one is not provided. | | `associations` | Array | The invoice’s associated records.To set each association, include a separate object in the associations array with the following fields:<ul><li>to: the ID of the record to associate with the invoice.</li><li>associationCategory: the [<u>type of association</u>](/guides/api/crm/associations/associations-v4#retrieve-association-types). Can be HUBSPOT_DEFINED or USER_DEFINED.</li><li>associationTypeId: the ID of the type of association being made:<ul><li>177: invoice to contact</li></ul></li></ul>[Learn more about association type IDs](/guides/api/crm/associations/associations-v4#association-type-id-values). | # CRM API | Discounts @ Learn how to create and associate a discount as part of the pricing details of a quote. $ guides & api/crm/commerce --- When you're [creating a quote in HubSpot](/guides/api/crm/commerce/quotes), you can create and associate a discount as part of the pricing details of the quote. ## Create a discount Discounts are used in conjunction with [fees](/guides/api/crm/commerce/fees) and [taxes](/guides/api/crm/commerce/taxes) when determining the pricing details for a quote. Any discounts you associate with your quote will be applied first, followed by associated fees, and then any associated taxes will apply. ```json // POST request to https://api.hubspi.com/crm/v3/objects/discount { "properties": { "hs_label": "A fixed, one-time discount", "hs_duration": "ONCE", "hs_type": "PERCENT", "hs_value": "50", "hs_sort_order": "2" } } ``` After you create a discount, you can use its ID to associate it with a quote. To retrieve a list of discounts you've created, you can make a `GET` request to `/crm/v3/objects/discount`. To view all available endpoints and their required fields, check out the [reference documentation](https://developers.hubspot.com/docs/reference/api/crm/commerce/discounts). # CRM API | Fees @ Learn how you can create and associate a fee as part of the pricing details of a quote (BETA). $ guides & api/crm/commerce --- When you're [creating a quote in HubSpot](/guides/api/crm/commerce/quotes), you can create and associate a fee as part of the pricing details of the quote. ## Create a fee Fees are used in conjunction with [discounts](/guides/api/crm/commerce/discounts) and [taxes](/guides/api/crm/commerce/taxes) when determining the pricing details for a quote. Any discounts you associate with your quote will be applied first, followed by associated fees, and then any associated taxes will apply. To create a percentage fee: ```json // POST request to https://api.hubspi.com/crm/v3/objects/fee { "properties": { "hs_label": "A percentage-based fee of 10%", "hs_type": "PERCENT", "hs_value": "10" } } ``` After you create a fee, you can use its ID to [associate it with a quote](/guides/api/crm/commerce/quotes#adding-associations). To retrieve a list of fees you've created, you can make a `GET` request to `/crm/v3/objects/fee`. To view all available endpoints and their required fields, check out the [reference documentation](https://developers.hubspot.com/docs/reference/api/crm/commerce/fees). # CRM API | Invoices @ Invoices are used to bill customers. The invoices endpoint allows you to retrieve invoices. $ guides & api/crm/commerce --- # Invoices Use the invoices API to fetch information about an account's [invoices](https://knowledge.hubspot.com/invoices/create-invoices). This is a read-only API, so it cannot be used for creating new or managing existing invoices. For example, use this API to [fetch all currently open invoices](#search-for-invoices-by-properties). ## Retrieve invoices Depending on the information you need, there are a few ways to retrieve invoices: - To retrieve all invoices, make a `GET` request to `crm/v3/objects/invoices`. - To retrieve a specific invoice, make a `GET` request to the above URL and specify an invoice ID. For example: `crm/v3/objects/invoices/44446244097`. - To retrieve invoices that meet a specific set of criteria, you can make a `POST` request to the search endpoint and include filters in the request body. See an example of using the [search endpoint below](#search-for-invoices-by-filter-criteria). The response will include a few default properties, including the create date, last modified date. ```json { "id": "44446244097", "properties": { "hs_createdate": "2023-03-08T14:54:17.333Z", "hs_lastmodifieddate": "2024-03-01T22:33:09.011Z", "hs_object_id": "44446244097" }, "createdAt": "2023-03-08T14:54:17.333Z", "updatedAt": "2024-03-01T22:33:09.011Z", "archived": false } ``` ### Properties To return specific properties, include a `properties` query parameter in the request URL along with comma-separated property names. For example, making a `GET` request to the following URL would result in the response below: `crm/v3/objects/invoices?properties=hs_invoice_status,hs_amount_billed` ```json { "id": "44446244097", "properties": { "hs_amount_billed": "20.00", "hs_createdate": "2023-03-08T14:54:17.333Z", "hs_invoice_status": "open", "hs_lastmodifieddate": "2024-03-01T22:33:09.011Z", "hs_object_id": "44446244097" }, "createdAt": "2023-03-08T14:54:17.333Z", "updatedAt": "2024-03-01T22:33:09.011Z", "archived": false } ``` To view all available invoice properties, make a `GET` request to `crm/v3/properties/invoices`. Learn more about using the [properties API](/guides/api/crm/properties). Below are some common invoice properties that you may want to query. | Property name | Label in UI | Description | | --- | --- | --- | | `hs_invoice_status` | [Invoice status](https://knowledge.hubspot.com/invoices/hubspots-default-invoice-properties#:~:text=Legacy%20Quickbooks%20integration.-,Invoice%20Status,-%3A%20the%20current) | The current status of the invoice. Values include:<ul><li>`draft`</li><li>`open`</li><li>`paid`</li><li>`voided`</li></ul> | | `hs_amount_billed` | [Amount billed](https://knowledge.hubspot.com/invoices/hubspots-default-invoice-properties#:~:text=creating%20the%20invoice.-,Amount%20billed,-%3A%20the%20total) | The amount billed on the invoice. | | `hs_balance_due` | [Balance due](https://knowledge.hubspot.com/invoices/hubspots-default-invoice-properties#:~:text=on%20the%20invoice.-,Balance%20due,-%3A%20the%20current) | The balance due on the invoice. | | `hs_due_date` | [Due date](https://knowledge.hubspot.com/invoices/hubspots-default-invoice-properties#:~:text=USD%20is%20supported.-,Due%20date,-%3A%20the%20invoice%E2%80%99s) | The date the invoice is due. | | `hs_number` | [Number](https://knowledge.hubspot.com/invoices/hubspots-default-invoice-properties#:~:text=creating%20the%20invoice.-,Number,-%3A%20the%20unique) | The invoice number (e.g., `INV_1003`) | ### Search for invoices by properties You can use the search [endpoint](/guides/api/crm/search) to retrieve invoices that meet a specific set of [filter criteria](/guides/api/crm/search#filter-search-results). This will be a `POST` request that includes your filter criteria in the request body. For example, to search for all open invoices, you would make a `POST` request to `crm/v3/objects/invoices/search` with the following request body: ```json { "filterGroups": [ { "filters": [ { "propertyName": "hs_invoice_status", "value": "open", "operator": "EQ" } ] } ], "properties": ["hs_invoice_status", "hs_due_date"] } ``` Note that the `filters` array specifies the search criteria, while the `properties` array specifies which properties to return. ## Associations While you cannot set associations using this API, you can retrieve association information by making a GET request to the following URL: ``crm/v3/objects/invoice/{`hs_object_id}`/associations/{associatedObjectName}`` Associated objects can include [contacts](/guides/api/crm/objects/contacts), [companies](/guides/api/crm/objects/companies), [deals](/guides/api/crm/objects/deals), [line items](/guides/api/crm/commerce/line-items), [discounts](/guides/api/crm/commerce/discounts), **[fees](/guides/api/crm/commerce/fees)**, and **[taxes](/guides/api/crm/commerce/taxes)**. To create associations between an invoice and these objects, you can [update the invoice in HubSpot](https://knowledge.hubspot.com/invoices/create-invoices). Below is an example of how you might use this API combined with another API to get a specific set of association information. Line items belong to one single parent object. For example, if retrieving line items from an invoice, the line item ID’s will be different to those on a deal, or a quote. ### Retrieving an invoice with associated line items To retrieve an invoice and the line items associated with it, make a `GET` request to: ``crm/v3/objects/invoice/{`hs_object_id}`/associations/line_items`` This will return the IDs of the currently associated line items, along with meta information about the association type. ```json { "results": [ { "id": "1526712436", "type": "invoice_to_line_item" }, { "id": "1526712437", "type": "invoice_to_line_item" } ] } ``` You can then use the returned IDs to request more information about the line items through the [line items API](/guides/api/crm/commerce/line-items). For example, you could batch retrieve line items by ID by making a `POST` request to the following URL with the request body below: `crm/v3/objects/line_items/batch/read` ```json { "inputs": [{ "id": "1526712436" }, { "id": "1526712437" }], "properties": ["name", "amount"] } ``` The response would be formatted as follows: ```json { "status":"COMPLETE", "results":[ { "id":"1359205183", "properties":{ "amount":"123.00", "createdate":"2023-04-26T14:52:35.885Z" "hs_lastmodifieddate":"2023-04-26T14:52:35.885Z", "hs_object_id":"1359205183", "name":"itemname" }, "createdAt":"2023-04-26T14:52:35.885Z", "updatedAt":"2023-04-26T14:52:35.885Z", "archived":false } ], "startedAt":"2024-03-11T20:09:44.151Z", "completedAt":"2024-03-11T20:09:44.195Z" } ``` # CRM API | Line items @ When a product is attached to a deal, it becomes a line item. The line items endpoints allow you to manage and sync this data. $ guides & api/crm/commerce --- # Line Items In HubSpot, line items are individual instances of [products](/guides/api/crm/commerce/products). When a product is attached to a deal, it becomes a line item. You can create line items that are unique to an individual quote, but they will not be added to your product library. The line items endpoints allow you to manage this data and sync it between HubSpot and other systems. **Example use case:** when creating a set of [quotes](/guides/api/crm/commerce/quotes) for sales reps to send to potential buyers, you can use this API to create standalone line items per quote, as well as line items that are attached to existing products. ## Scope requirements Based on the endpoints you plan on using, you'll need to authorize the following scopes: - `crm.objects.line_items.read`: provides access to retrieve line item data. - `crm.objects.line_items.write`: provides access to create and update line items. - `tax_rates.read`: provides access to retrieve tax rates you've configured in your account. ## Create a line item To create a line item, make a `POST` request to `/crm/v3/objects/line_items`. In the post body, include the line item's details, such as name, quantity, and price. You may also want to include additional data in the request body: - To create a line item based on an existing product (created through the [products API](/guides/api/crm/commerce/products) or [in HubSpot](https://knowledge.hubspot.com/products/how-do-i-use-products)), include `hs_product_id` in the post body. - To include the [tax rate](#retrieve-tax-rates) for your line item, include its ID as the `hs_tax_rate_group_id` within the `properties` field of the request body. - You can also associate the line item with deals, quotes, invoices, payment links or subscriptions, by including an `associations` array in the post body. For example, the post body below would create a line item named "New standalone line item" that's associated with a deal (ID: `12345`). ```js { "properties": { "price": 10, "quantity": 1, "name": "New standalone line item", "hs_tax_rate_group_id": "2148420997" }, "associations": [ { "to": { "id": 12345 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 20 } ] } ] } ``` - Line items belong to one single parent object. If associating objects, line items should be individual to each object. For example, if you're creating a deal and a quote, you should create one set of line items for the deal, and another set for the quote. This will help streamline CRM data across objects and prevent unexpected data loss when needing to modify line items (e.g., deleting a quote will delete the quote's line items, and if those line items are associated with a deal, the deals line items will also be deleted). - The `price` specified within the `properties` field <u>cannot</u> be negative. - The line items _Term_ property (`hs_recurring_billing_period`) accepts [ISO-8601 period formats](https://docs.oracle.com/javase/8/docs/api/java/time/Period.html#:~:text=exceeds%20an%20int-,parse,-public%20static%C2%A0) of PnYnMnD and PnW. ## Retrieve a line item You can retrieve line items individually or in bulk. - To retrieve a specific line item, make a `GET` request to `/crm/v3/objects/line_items/{lineItemId}` where `lineItemId` is the ID of the line item. - To retrieve all line items, make a `GET` request to `/crm/v3/objects/line_items`. In the request URL, you can include the following parameters: | Parameter | Description | | --- | --- | | `properties` | A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. | | `propertiesWithHistory` | A comma separated list of the properties to be returned along with their history of previous values. If any of the specified properties are not present on the requested object(s), they will be ignored. | ## Update a line item To update a line item, make a `PATCH` request to `/crm/v3/objects/line_items/{lineItemId}`, where `lineItemId` is the ID of the line item. In the post body, include the property values that you want to update. You <u>cannot</u> update associations through this method. Instead you'll need to use the [associations API](/guides/api/crm/associations/associations-v4). For example, your request body might look similar to the following: ```js { "properties": { "price": 25, "quantity": 3, "name": "Updated line item" } } ``` ## Delete a line item To delete a line item, make a `DELETE` request to `/crm/v3/objects/line_items/{lineItemId}`, where `lineItemId` is the ID of the line item. ## Line item properties When managing your line item data, you may want to use some of the common properties in the table below. To get all line item properties, make a `GET` request to `/crm/v3/properties/line_item`. Learn more about using the [properties API](/guides/api/crm/properties). | Property name | Label in UI | Description | | --- | --- | --- | | `name` | Name | The name of the line item. | | `description` | Description | Full description of the product | | `hs_sku` | SKU | Unique product identifier | | `hs_recurring_billing_start_date` | Billing start date | Recurring billing start date for a line item | | `hs_recurring_billing_end_date` | Billing end date | Recurring billing end date for a line item | | `recurringbillingfrequency` | Billing frequency | How often a line item with recurring billing is billed. It informs the pricing calculation for deals and quotes. Line items with one-time billing aren't included. | | `quantity` | Quantity | How many units of a product are included in this line item | | `price` | Unit price | The cost of the product | | `amount` | Net price | The total cost of the line item (i.e., the quantity times the unit price). | | `hs_line_item_currency_code` | Currency | Currency code for the line item | ## Retrieve tax rates You can apply a tax rate to individual line items (e.g., a MA Sales tax of 6.26%). Once you [configure your tax rate library](https://knowledge.hubspot.com/payments/create-and-use-payment-discount-codes#add-a-tax-rate-to-the-tax-library) in your HubSpot account, you can then make a `GET` request to `/tax-rates/v1/tax-rates` to fetch all tax rates, or `/tax-rates/v1/tax-rates/{taxRateId}` to fetch a tax rate by its ID. Your app will need to authorize the `tax_rates.read` scope to make this request. The resulting response will resemble the following: ```json { "name": "MA Sales tax 2025", "percentageRate": 6.25 "label": "Sales Tax", "active": true, "id": "2148420997", "createdAt": "2024-12-12T23:20:39.923Z", "updatedAt": "2024-12-12T23:20:39.923Z" } ``` Each tax rate object will include the following properties: | Property name | Description | | --- | --- | | `name` | The internal descriptor for the tax rate. | | `percentageRate` | The value of the tax rate, expressed as a percentage. | | `label` | The buyer-facing descriptor of the tax rate, shown on the quote, invoice, or other parent objects. | | `active` | A boolean that denotes whether the tax rate can be applied to a new quote or invoice. You might set this to `false` for a previous year's tax rate that's no longer applicable. | | `id` | The ID of the tax rate. | | `createdAt` | An ISO 8601 timestamp denoting when the tax rate was created. | | `updatedAt` | An ISO 8601 timestamp denoting when the tax rate was last updated. | Once you have the ID of the tax rate you want to apply, provide that `id` for the `hs_tax_rate_group_id` within the `properties` field when creating a line item. Learn more about creating line items in the [section above](#create-a-line-item). # CRM API | Orders @ Learn about using HubSpot's APIs to create and manage ecommerce order data. $ guides & api/crm/commerce --- # Orders Use the orders API to create and manage data related to ecommerce purchases in HubSpot. This can be especially useful for keeping HubSpot data synced with external ecommerce platforms, such as Shopify and NetSuite. For example, when a buyer adds a set of products to their cart and makes a purchase, store that purchase as an individual order. You can then update that order with tracking information once the shipping label has been printed. Because this information is stored in a property, you can reference it in emails that you send to notify customers that their package is on the way. ## Create orders To create an order, make a `POST` request to `crm/v3/objects/order`. In the request body, you can include the `properties` and `associations` objects to set property values and associate the order with other CRM objects (e.g., contacts and line items). Learn more about order properties and associations below. ### Properties Order details are stored in order properties. HubSpot provides a set of [default order properties](#order-properties), but you can also create your own custom properties using the [properties API](/guides/api/crm/properties). To include properties when creating an order, add them as fields in a `properties` object in the request body. For example, the request body below would create an order with some basic order and shipping details based on the information provided by the buyer at checkout. ```json { "properties": { "hs_order_name": "Camping supplies", "hs_currency_code": "USD", "hs_source_store": "REI - Portland", "hs_fulfillment_status": "Packing", "hs_shipping_address_city": "Portland", "hs_shipping_address_state": "Maine", "hs_shipping_address_street": "123 Fake Street" } } ``` The response will include the information you provided during creation along with a few other default properties. ```json { "id": "54805205097", "properties": { "hs_created_by_user_id": "959199", "hs_createdate": "2024-03-27T18:04:11.823Z", "hs_currency_code": "USD", "hs_exchange_rate": "1.0", "hs_fulfillment_status": "Packing", "hs_lastmodifieddate": "2024-03-27T18:04:11.823Z", "hs_object_id": "54805205097", "hs_object_source": "CRM_UI", "hs_object_source_id": "userId:959199", "hs_object_source_label": "CRM_UI", "hs_object_source_user_id": "959199", "hs_order_name": "Camping supplies", "hs_shipping_address_city": "Portland", "hs_shipping_address_state": "Maine", "hs_shipping_address_street": "123 Fake Street", "hs_source_store": "REI - Portland", "hs_updated_by_user_id": "959199" }, "createdAt": "2024-03-27T18:04:11.823Z", "updatedAt": "2024-03-27T18:04:11.823Z", "archived": false } ``` ### Associations You can associate the order with other HubSpot CRM objects at creation by including an `associations` array. You can also use the [associations API](/guides/api/crm/associations/associations-v4) to update existing orders after creation. In the `associations` array, include an object for each associated record using the following fields: | Fields | Type | Description | | --- | --- | --- | | `toObjectId` | String | The ID of the record that you want to associate the order with. | | `associationTypeId` | String | A unique identifier to indicate the association type between the order and the other object. Below are the CRM objects that you can associate orders with, along with their `associationTypeId`:<ul><li>[Carts](/guides/api/crm/commerce/carts): `593`</li><li>[Contacts](/guides/api/crm/objects/contacts): `507`</li><li>[Companies](/guides/api/crm/objects/companies): `509`</li><li>[Deals](/guides/api/crm/objects/deals): `512`</li><li>[Discounts](/guides/api/crm/commerce/discounts): `519`</li><li>[Discount codes](https://knowledge.hubspot.com/payments/create-and-use-payment-discount-codes): `521`</li><li>[Invoices](/guides/api/crm/commerce/invoices): `518`</li><li>[Line items](/guides/api/crm/commerce/line-items): `513`</li><li>[Payments](/guides/api/crm/commerce/payments): `523`</li><li>[Quotes](/guides/api/crm/commerce/quotes): `730`</li><li>[Subscriptions](https://developers.hubspot.com/docs/api/crm/subscriptions): `516`</li><li>[Tasks](/guides/api/crm/engagements/tasks): `726`</li><li>[Tickets](/guides/api/crm/objects/tickets): `525`</li></ul>To see a list of all association types, check out the [associations API documentation](/guides/api/crm/associations/associations-v4#association-type-id-values). Or, you can retrieve each value by making a `GET` request to `/crm/v4/associations/{fromObjectType}/{toObjectType}/labels`. | For example, the `POST` request body below would create an order that's associated with a specific contact and two line items. Properties are also included below the associations for setting initial order information. ```json { "associations": [ { "to": { "id": 301 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 507 } ] }, { "to": { "id": 1243313490 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 513 } ] }, { "to": { "id": 1243557166 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 513 } ] } ], "properties": { "hs_order_name": "Associated order", "hs_currency_code": "USD", "hs_source_store": "REI - Portland", "hs_fulfillment_status": "Packing", "hs_shipping_address_city": "Portland", "hs_shipping_address_state": "Maine", "hs_shipping_address_street": "123 Fake Street" } } ``` ## Retrieve orders Depending on the information you need, there are a few ways to retrieve orders: - To retrieve all orders, make a `GET` request to `/crm/v3/objects/order`. - To retrieve a specific order, make a `GET` request to the above URL and specify an order ID. For example: `/crm/v3/objects/order/44446244097`. - To retrieve orders that meet a specific set of criteria, you can make a `POST` request to the search endpoint and include filters in the request body. Learn more about [searching the CRM](/guides/api/crm/search#make-a-search-request). The response will include a few default properties, including the create date, last modified date. ```json { "results": [ { "id": "54767113310", "properties": { "hs_createdate": "2024-03-26T20:02:34.935Z", "hs_lastmodifieddate": "2024-03-26T20:02:48.278Z", "hs_object_id": "54767113310" }, "createdAt": "2024-03-26T20:02:34.935Z", "updatedAt": "2024-03-26T20:02:48.278Z", "archived": false }, { "id": "54804869149", "properties": { "hs_createdate": "2024-03-27T17:39:16.122Z", "hs_lastmodifieddate": "2024-03-27T17:39:16.122Z", "hs_object_id": "54804869149" }, "createdAt": "2024-03-27T17:39:16.122Z", "updatedAt": "2024-03-27T17:39:16.122Z", "archived": false } ] } ``` To return specific properties, include a `properties` query parameter in the request URL along with comma-separated property names. For example, making a `GET` request to the following URL would result in the response below: `/crm/v3/objects/order?properties=hs_order_name,hs_source_store` ```json { "id": "54767113310", "properties": { "hs_createdate": "2024-03-26T20:02:34.935Z", "hs_lastmodifieddate": "2024-03-27T18:50:07.678Z", "hs_object_id": "54767113310", "hs_order_name": "Test API order 2", "hs_source_store": "REI - Portland" }, "createdAt": "2024-03-26T20:02:34.935Z", "updatedAt": "2024-03-27T18:50:07.678Z", "archived": false } ``` To view all available order properties, you can query the [properties API](/guides/api/crm/properties) by making a `GET` request to `crm/v3/properties/order`. Learn more about [order properties](#order-properties). ### Search for orders by properties You can use the search endpoint to retrieve orders that meet a specific set of [filter criteria](/guides/api/crm/search#filter-search-results). This will be a `POST` request that includes your filter criteria in the request body. For example, to search for all orders placed at a specific store, you would make a `POST` request to `crm/v3/objects/order/search` with the following request body: ```json { "filterGroups": [ { "filters": [ { "propertyName": "hs_source_store", "value": "REI - Portland", "operator": "EQ" } ] } ], "properties": ["hs_order_name", "hs_source_store"] } ``` ### Retrieve an order with associations To retrieve an order along with its associations, make a `GET` request to `crm/v3/objects/order/{orderId}/associations/{objectName}` For example, to retrieve an order and its associated contacts, you would use the following URL: `crm/v3/objects/order/{orderId}/associations/contact` This will return the IDs of the currently associated contacts, along with meta information about the association type. ```json { "results": [ { "id": "301", "type": "order_to_contact" }, { "id": "1196316844", "type": "order_to_contact" } ] } ``` You could then use the returned IDs to request more information about the contacts through the [contacts API](/guides/api/crm/objects/contacts). For example, you could retrieve the contact using its ID by making a `GET` request to `crm/v3/objects/contacts/{contactId}`. ```json { "id": "301", "properties": { "createdate": "2022-09-27T13:13:31.004Z", "email": "tom.bombadil@oldforest.com", "firstname": "Tom", "hs_object_id": "301", "lastmodifieddate": "2023-11- 07T17:14:00.841Z", "lastname": "Bombadil" }, "createdAt": "2022-09-27T13:13:31.004Z", "updatedAt": "2023-11-07T17:14:00.841Z", "archived": false } ``` Note that the `filters` array specifies the search criteria, while the `properties` array specifies which properties to return. ## Update orders To update an order, make a `PATCH` request to `/crm/v3/objects/order/{orderId}`. In the request body, include a `properties` object containing the properties that you want to update. For example, if you wanted to update an order with the shipping tracking number, you could send the following request body: ```json { "properties": { "hs_shipping_tracking_number": "123098521091" } } ``` The response will include a set of default properties along with the property that you just set. ```json { "id": "54767113310", "properties": { "hs_created_by_user_id": "959199", "hs_createdate": "2024-03-26T20:02:34.935Z", "hs_lastmodifieddate": "2024-03-27T20:03:05.890Z", "hs_object_id": "54767113310", "hs_shipping_tracking_number": "123098521091", "hs_updated_by_user_id": "959199" }, "createdAt": "2024-03-26T20:02:34.935Z", "updatedAt": "2024-03-27T20:03:05.890Z", "archived": false } ``` To update the associations for an existing order, make a `PUT` request to `/crm/v3/objects/order/{orderId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}`. You can also use the [associations API](/guides/api/crm/associations/associations-v4). See the [associations section](#associations) for `associationTypeId` values for order-to-object associations. You can also make a `GET` request to `/crm/v4/associations/{fromObjectType}/{toObjectType}/labels`. To see all a list of all values, check out the [associations API documentation](/guides/api/crm/associations/associations-v4#association-type-id-values). For example, to associate an existing order with an existing payment, you would make a `PUT` request to the following URL: `/crm/v3/objects/order/{orderId}/associations/commerce_payments/{paymentId}/523` The response will return a set of default properties along with an `associations` object containing information about the association that you set. ```json { "id": "54767113310", "properties": { "hs_createdate": "2024-03-26T20:02:34.935Z", "hs_lastmodifieddate": "2024-03-27T20:03:05.890Z", "hs_object_id": "54767113310" }, "createdAt": "2024-03-26T20:02:34.935Z", "updatedAt": "2024-03-27T20:03:05.890Z", "archived": false, "associations": { "payments": { "results": [ { "id": "50927296322", "type": "order_to_commerce_payment" } ] } } } ``` To remove an association from an existing order, make a `DELETE` request to the following URL: `/crm/v3/objects/order/{orderId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}` For example, if you wanted to remove an associated payment from an order, your request URL would be the following: `/crm/v3/objects/order/{orderId}/associations/commerce_payments/{paymentId}/523` ## Order properties When managing your order data, you may want to use some of the common properties in the table below. To get all order properties, make a `GET` request to `/crm/v3/properties/order`. Learn more about using the [properties API](/guides/api/crm/properties). | Property name | Label in UI | Description | | --- | --- | --- | | `hs_order_name` | Name | The name of the order. | | `hs_currency_code` | Currency Code | The currency that the order was placed in. | | `hs_source_store` | Source Store | The store that that the order came from. | | `hs_fulfillment_status` | Fulfillment Status | The current fulfillment / shipping status of the order. | | `hs_shipping_status_url` | Shipping Status URL | A URL for tracking the shipment status. | | `hs_shipping_tracking_number` | Shipping Tracking Number | The tracking number for shipping. | | `hs_shipping_address_street` | Shipping Street | The street address for shipping. | | `hs_shipping_address_city` | Shipping City | The city in the shipping address. | | `hs_shipping_address_postal_code` | Shipping ZIP/Postal Code | The zip code of the shipping address. | | `hs_pipeline` | Pipeline | The pipeline that the order is in. Pipelines contain stages for tracking the order's progress. Learn more about [pipelines and stages](#pipelines-and-stages) below. | | `hs_pipeline_stage` | Stage | The order's progress within its current pipeline. Learn more about [pipelines and stages](#pipelines-and-stages) below. | ### Pipelines and stages To track an order's progress, you can create pipelines with defined stages for each step of the fulfillment process. For example, you could create a pipeline for online orders with stages for when the order has been opened, paid, processed, shipped, cancelled, and refunded. Using the [pipelines API](https://developers.hubspot.com/crm/v3/pipelines/{objectType}), you can create an order pipeline by making a `POST` request to `crm/v3/pipelines/order`. In the request body, you'll include a `label` for the pipeline, `displayOrder` for the display in HubSpot, and a `stages` array with objects for each stage. ```json { "label": "Online orders", "displayOrder": 0, "stages": [ { "label": "Open", "displayOrder": 0, "metadata": { "state": "OPEN" } }, { "label": "Paid", "displayOrder": 1, "metadata": { "state": "OPEN" } }, { "label": "Processed", "displayOrder": 2, "metadata": { "state": "OPEN" } }, { "label": "Shipped", "displayOrder": 3, "metadata": { "state": "CLOSED" } }, { "label": "Cancelled", "displayOrder": 4, "metadata": { "state": "CLOSED" } }, { "label": "Refunded", "displayOrder": 5, "metadata": { "state": "CLOSED" } } ] } ``` | Parameter | Type | Description | | --- | --- | --- | | `label` | String | The pipeline's label as it should appear in HubSpot. | | `displayOrder` | Number | The order for displaying the pipeline in HubSpot. If two pipelines have a matching `displayOrder`, they will be sorted alphabetically by label. | | `stages` | Array | An array containing the pipeline stages. Each stage is an object containing the following fields:<ul><li>`label`: the stage's label as it should appear in HubSpot.</li><li>`displayOrder`: the order in which the stage will appear in HubSpot.</li><li>`metadata`: configures whether the stage is in progress (`OPEN`) or complete (`CLOSED`) using the `state` field.</li></ul> | # Payments API @ Learn about using HubSpot's APIs to retrieve information about payments. $ guides & api/crm/commerce --- # Payments Use the payments API to fetch information about an account's [payments](https://knowledge.hubspot.com/payments/manage-payments). This is a read-only API, so it cannot be used for creating new or managing existing payments. For example, use this API to [fetch all refunded payments](#search-for-payments-by-properties) in an account. ## Requirements To use this API, the account must be set up to collect payments through either [HubSpot payments](https://knowledge.hubspot.com/payment-processing/set-up-payments) or [Stripe payment processing](https://knowledge.hubspot.com/payment-processing/connect-your-stripe-account-as-a-payment-processor-in-hubspot). ## Retrieve payments Depending on the information you need, there are a few ways to retrieve payments: - To retrieve all payments, make a `GET` request to `crm/v3/objects/commerce_payments`. - To retrieve a payment, make a `GET` request to the above URL and specify an payment ID. For example: `crm/v3/objects/commerce_payments/44446244097`. - To retrieve payments that meet a specific set of criteria, you can make a `POST` request to the search endpoint and include filters in the request body. See an example of using the [search endpoint below](#search-for-invoices-by-filter-criteria). The response will include a few default properties, including the create date, last modified date. ```json { "id": "44446244097", "properties": { "hs_createdate": "2023-03-08T14:54:17.333Z", "hs_lastmodifieddate": "2024-03-01T22:33:09.011Z", "hs_object_id": "44446244097" }, "createdAt": "2023-03-08T14:54:17.333Z", "updatedAt": "2024-03-01T22:33:09.011Z", "archived": false } ``` ### Properties To return specific properties, include a `properties` query parameter in the request URL along with comma-separated property names. For example, making a `GET` request to the following URL would result in the response below: `crm/v3/objects/commerce_payments?properties=hs_customer_email,hs_latest_status` ```json { "id": "40744976671", "properties": { "hs_createdate": "2022-09-02T15:03:40.828Z", "hs_customer_email": "name@emailaddress.com", "hs_lastmodifieddate": "2024-02-27T15:03:53.620Z", "hs_object_id": "40744976671", "hs_latest_status": "succeeded" }, "createdAt": "2022-09-02T15:03:40.828Z", "updatedAt": "2024-02-27T15:03:53.620Z", "archived": false } ``` To view all available payment properties, make a `GET` request to `crm/v3/properties/commerce_payments`. Learn more about using the [properties API](/guides/api/crm/properties). Below are some common payment properties that you may want to query. | Property name | Label in UI | Description | | --- | --- | --- | | `hs_latest_status` | [Status](https://knowledge.hubspot.com/payments/hubspots-payments-and-subscriptions-properties#payment-properties:~:text=created%20the%20payment.-,Status,-%3A%C2%A0the%20current) | The current status of the payment. Values include:<ul><li>`succeeded`</li><li>`refunded`</li><li>`processing`</li><li>`failed`</li></ul> | | `hs_initial_amount` | [Gross amount](https://knowledge.hubspot.com/payments/hubspots-payments-and-subscriptions-properties#payment-properties:~:text=made%20the%20payment.-,Gross%20amount,-%3A%C2%A0the%20total) | The total amount that the buyer was charged. | | `hs_customer_email` | [Customer](https://knowledge.hubspot.com/payments/hubspots-payments-and-subscriptions-properties#payment-properties:~:text=be%20in%20USD.-,Customer,-%3A%C2%A0the%20email) | The buyer's email address. | | `hs_initiated_date` | [Payment date](https://knowledge.hubspot.com/payments/hubspots-payments-and-subscriptions-properties#payment-properties:~:text=can%27t%20be%20changed.-,Payment%20date,-%3A%C2%A0the%20date) | The date that the payment was created. | | `hs_payment_id` | [Payment ID](https://knowledge.hubspot.com/payments/hubspots-payments-and-subscriptions-properties#payment-properties:~:text=is%20updated%20automatically.-,Payment%20ID,-%3A%C2%A0the%20unique) | The payment's unique ID. | ### Search for payments by properties You can use the search endpoint to retrieve payments that meet a specific set of [filter criteria](/guides/api/crm/search#filter-search-results). This will be a `POST` request that includes your filter criteria in the request body. For example, to search for all refunded payments, you would make a `POST` request to `crm/v3/objects/commerce_payments/search` with the following request body: ```json { "filterGroups": [ { "filters": [ { "propertyName": "hs_latest_status", "value": "refunded", "operator": "EQ" } ] } ], "properties": ["hs_latest_status", "hs_customer_email"] } ``` Note that the `filters` array specifies the search criteria, while the `properties` array specifies which properties to return. ## Associations While you cannot set associations using this API, you can retrieve association information by making a GET request to the following URL: `crm/v3/objects/commerce_payments/{hs_object_id}/associations/{associatedObjectName}` Associated objects can include [contacts](/guides/api/crm/objects/contacts), [companies](/guides/api/crm/objects/companies), [deals](/guides/api/crm/objects/deals), [invoices](/guides/api/crm/commerce/invoices), [quotes](/guides/api/crm/commerce/quotes), [line items](/guides/api/crm/commerce/line-items), [subscriptions](/guides/api/crm/commerce/subscriptions), [discounts](/guides/api/crm/commerce/discounts), **[fees](/guides/api/crm/commerce/fees)**, and **[taxes](/guides/api/crm/commerce/taxes)**. These associates are based on the associations set on the invoice, payment link, or quote used for transaction. To manage these associations, you can [update the payment in HubSpot](https://knowledge.hubspot.com/payments/manage-payments#view-payment-records). Below is an example of how you might use this API combined with another API to get a specific set of association information. When retrieving line items from different objects created in HubSpot, you should expect to receive different IDs. This is because line items should only be associated with one object, which HubSpot handles automatically by creating copies of line items rather than using the same line item across multiple objects. ### Retrieving a payment with associated contact To retrieve a payment and contact associated with it, make a `GET` request to: `crm/v3/objects/commerce_payments/{hs_object_id}/associations/contact` This will return the IDs of the currently associated contact, along with meta information about the association type. ```json { "results": [ { "id": "301", "type": "commerce_payment_to_contact" } ] } ``` You can then use the returned IDs to request more information about the line items through the [contacts API](/guides/api/crm/objects/contacts). For example, you could retrieve the contact using its ID by making a `GET` request to `crm/v3/objects/contacts/{contactId}` ```json { "id": "301", "properties": { "createdate": "2022-09-27T13:13:31.004Z", "email": "tom.bombadil@oldforest.com", "firstname": "Tom", "hs_object_id": "301", "lastmodifieddate": "2023-11- 07T17:14:00.841Z", "lastname": "Bombadil" }, "createdAt": "2022-09-27T13:13:31.004Z", "updatedAt": "2023-11-07T17:14:00.841Z", "archived": false } ``` # CRM API | Products @ In HubSpot, products represent the goods or services you sell. The products endpoints allow you to manage and sync this data. $ guides & api/crm/commerce --- In HubSpot, products represent the goods or services you sell. Building a [product library](https://knowledge.hubspot.com/products/how-do-i-use-products) allows you to quickly add products to deals, generate quotes, and report on product performance. The products endpoints allow you to manage this data and sync it between HubSpot and other systems. Products, along with companies, contacts, deals, tickets, line items, and quotes, are objects in the HubSpot CRM. Learn more about object properties, associations, relationships, and more in our [Understanding the CRM Objects](/guides/api/crm/understanding-the-crm) guide. **Example use case:** so that sales reps can easily add goods and services to deals, quotes, and more, use the products API to import your product catalog into HubSpot. ## Create a product To create a product make a `POST` request to `crm/v3/objects/products`. In the request body, include a `properties` object containing any product properties that you'd like to set on create. You can later update a product's properties through a `PATCH` request to the same endpoint. To see all available product properties, make a `GET` request to the [properties API](/guides/api/crm/properties). To retrieve product properties, the request URL will be `/crm/v3/properties/products`. ```json // POST request to crm/v3/objects/products { "properties": { "name": "Implementation Service", "price": "6000.00", "hs_sku": "123456", "description": "Onboarding service for data product", "hs_cost_of_goods_sold": "600.00", "hs_recurring_billing_period": "P12M" } } ``` Note that the value for `hs_recurring_billing_period` is formatted as `P#M`, where # is the number of months. ## Associate products Products themselves can't be associated with other CRM objects. However, to associate a product's information with a deal or a quote, you can create a [line item](/guides/api/crm/commerce/line-items) based on that product. Line items are individual instances of products, and are a separate object from products so that you can tailor the goods and services on a deal or quote as needed without needing to update the product itself. For example, if you're putting together a deal where one of your products is being sold, you'd first create a line item from the product, then associate it with the deal. You can either do this with two separate calls, or with one call that creates and associates the line item. Both options are shown below. Line items belong to one single parent object. If associating objects, line items should be individual to each object. For example, if you're creating a deal and a quote, you should create one set of line items for the deal, and another set for the quote. This will help streamline CRM data across objects and prevent unexpected data loss when needing to modify line items. For example, deleting a quote will also delete the quote's line items. If those line items are also associated with a deal, the deal's line items will also be deleted. ### Create and associate a line item (multiple calls) First, you'll create a line item based on a product with the ID of `1234567`. For a full list of available line item properties, make a `GET` request to the [properties API](/guides/api/crm/properties). The URL for line items would be `crm/v3/properties/line_items`. Because you're create the line item from an existing product, it will inherit property values from the product, such as price. ```json // POST request to https://api.hubapi.com/crm/v3/objects/line_item { "properties": { "quantity": 1, "hs_object_id": "1234567", //the object ID of the product "name": "New line item (product-based)" } } ``` The response will return a line item ID which you can use to associate it with a deal using the [associations API](/guides/api/crm/associations/associations-v4). For this example, assume that the returned line item ID is `7791176460`. To associate the line item with an existing deal (ID: `14795354663`), you'll make a `PUT` request to `/crm/v4/objects/line_items/7791176460/associations/default/deals/14795354663`. This request uses the default association type. A `200` response will return information similar to the following: ```json // PUT request to crm/v4/objects/line_items/7791176460/associations/default/deals/14795354663 { "status": "COMPLETE", "results": [ { "from": { "id": "14795354663" }, "to": { "id": "7791176460" }, "associationSpec": { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 19 } }, { "from": { "id": "7791176460" }, "to": { "id": "14795354663" }, "associationSpec": { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 20 } } ], "startedAt": "2023-12-21T20:06:52.083Z", "completedAt": "2023-12-21T20:06:52.192Z" } ``` In HubSpot, the deal record will display the line item in the _Line items_ card. ### Create and associate a line item (single call) To create a line item from an existing product and associate it with a deal using a single call, you can include an `associations` array in the line item create request. To create the line item, make a `POST` request to `crm/v3/objects/line_item`. Your request body will look similar to the following. Note that the `associationTypeId` for the line item-deal association is `20`. Learn more about [association types between different types of CRM records](/guides/api/crm/associations/associations-v4#association-type-id-values). ```json // POST request to https://api.hubapi.com/crm/v3/objects/line_item { "properties": { "quantity": 1, "hs_object_id": "1234567", //the object ID of the product "name": "New line item (product-based)" }, "associations": [ { "to": { "id": "14795354663" }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 20 } ] } ] } ``` A `200` response will return details about the new line item. In HubSpot, the deal record will display the line item in the _Line items_ card. # CRM API | Quotes @ Quotes are used to share pricing info with prospects. The quotes endpoints allow you to retrieve this data and sync it between HubSpot and other systems. $ guides & api/crm/commerce --- # Quotes Use the quotes API to create, manage, and retrieve sales quotes for sharing pricing information with potential buyers. Once configured, a quote can be shared with a buyer either at a specified URL or through PDF. Users can also [manage quotes in HubSpot](https://knowledge.hubspot.com/quotes/use-quotes#manage-quotes) to add details, update associations, and more. If you’ve set up either [HubSpot payments](https://knowledge.hubspot.com/payment-processing/set-up-payments) or [Stripe payment processing](https://knowledge.hubspot.com/payment-processing/connect-your-stripe-account-as-a-payment-processor-in-hubspot), you can configure a quote to be payable through this API. Learn more about [payable quotes](/guides/api/crm/commerce/quotes#enable-payments). **Example use case:** You need to create a contract proposal for a customer who is interested in purchasing one of your annual SEO auditing service packages. Below, learn how to create a quote through the API and configure its various properties, associations, states, and more. ## Overview The quote creation process can be broken up into the following steps: 1. **Create a quote:** create a quote with a few details, such as name and expiration date. You can also configure the quote to [enable e-signatures](/guides/api/crm/commerce/quotes#enable-e-signatures) and [payments](/guides/api/crm/commerce/quotes#enable-payments)[](https://knowledge.hubspot.com/quotes/use-e-signatures-with-quotes). 2. **Set up associations:** associate the quote with other types of CRM objects, such as line items, a quote template, a deal, and more. During the next step, the quote will inherit property values from some of these associated records as well as the account's settings. 3. **Set the quote state:** set the quote's state to reflect its readiness to be shared with buyers. When you set the quote's state, such as making it an editable draft or fully published and publicly accessible quote, it will [inherit certain properties](#properties-set-by-quote-state) from its associated CRM records and account settings. 4. **Share the quote:** once a quote has been published, you can share it with your buyers. ## Create a quote To create a quote, you'll first configure its basic details by making a `POST` request to `/crm/v3/objects/quotes`. Later, you'll make a separate call to associate the quote with other objects, such as the quote template, line items, or a deal. Depending on your preferred workflow, you can instead [create a quote with associations through one POST request](#create-a-quote-with-associations-single-request). In the post body, include the following required properties to configure its basic details. ```json { "properties": { "hs_title": "CustomerName - annual SEO audit", "hs_expiration_date": "2023-12-10" } } ``` | Parameter | Type | Description | | -------------------- | ------ | -------------------------------- | | `hs_title` | String | The name of the quote. | | `hs_expiration_date` | String | The date that the quote expires. | The above are just the minimum required properties to get a quote started, but [other properties](/guides/api/crm/commerce/quotes#adding-associations) are required to publish a quote. To see all available quote properties, make a `GET` request to `crm/v3/properties/quotes`. Learn more about the [properties API](/guides/api/crm/properties). The response will include an `id`, which you'll use to continue configuring the quote. You can update quote properties at any time by making a `PATCH` request to `/crm/v3/objects/quotes/{quoteId}`. ### Quote owner Setting the `hubspot_owner_id` property manually isn't possible due to it being a calculated property, and any values will be overridden. When using quotes, the property works as follows: - If a [deal is associated with the quote](#adding-associations), the `hubspot_owner_id` property will reflect the `hs_associated_deal_owner_id` property (`hs_associated_deal_owner_id` is a calculated property). - If a deal isn't associated with the quote, the `hubspot_owner_id` property will reflect the `hs_quote_owner_id` property. ### Enable e-signatures To enable e-signatures on the quote, include the `hs_esign_enabled` boolean property in your request body with a value of `true`. Note that you will not be able to add countersigners through the API, so those will need to be added in HubSpot before publishing the quote. You also cannot publish a quote with e-sign enabled if you've [exceeded your monthly e-signature limit](https://knowledge.hubspot.com/quotes/use-e-signatures-with-quotes#monitor-e-signature-usage). ```json { "properties": { "hs_title": "CustomerName - annual SEO audit", "hs_expiration_date": "2023-12-10", "hs_esign_enabled": "true" } } ``` Later, you'll need to associate the quote with the quote signers. While the contacts signing the quote exist as contacts in HubSpot, they're stored as a separate association type from contacts. Learn more about [associating quotes with quote signers](#associating-quote-signers). ### Enable payments To turn on payments on a quote, include the `hs_payment_enabled` boolean property in your request body with a value of `true`. Depending on your payment processor and accepted payment methods, you’ll also need to set `hs_payment_type` and `hs_allowed_payment_methods`. The HubSpot account must have either [HubSpot payments](https://knowledge.hubspot.com/payment-processing/set-up-payments) or [Stripe payment processing](https://knowledge.hubspot.com/payment-processing/connect-your-stripe-account-as-a-payment-processor-in-hubspot) set up before using this capability. | Parameter | Type | Description | | --- | --- | --- | | `hs_payment_enabled` | Boolean | When set to `true`, enables the quote to collect payment using either HubSpot payments or Stripe payment processing. Default is `false`. | | `hs_payment_type` | Enumeration | Determines which payment processor to use. Value can be either `HUBSPOT` or `BYO_STRIPE`. | | `hs_allowed_payment_methods` | Enumeration | The payment methods to be used (e.g., credit card). | | `hs_collect_billing_address` | Boolean | When set to `true`, allows the buyer to enter their billing address during checkout. | | `hs_collect_shipping_address` | Boolean | When set to `true`, allows the buyer to enter their shipping address during checkout. | For example, to create a quote and enable HubSpot payments via credit/debit card or ACH, your request would look like: ```json { "properties": { "hs_title": "CustomerName - annual SEO audit", "hs_expiration_date": "2023-12-10", "hs_payment_enabled": "true", "hs_payment_type": "HUBSPOT", "hs_allowed_payment_methods": "CREDIT_OR_DEBIT_CARD;ACH" } } ``` To track payment, HubSpot will automatically update the `hs_payment_status` and `hs_payment_date` properties: - When you publish a quote with payments enabled, HubSpot will automatically set the `hs_payment_status` property to `PENDING`. - If using ACH, when the payment is processed, HubSpot will automatically set the `hs_payment_status` property to `PROCESSING`. - When the payment is confirmed, HubSpot will automatically set the `hs_payment_status` property to `PAID`. - Once the quote is paid, HubSpot will automatically set `hs_payment_date` to the date and time that the payment was confirmed. - Once payment is confirmed, the payment is automatically associated to the quote. If you would like to retrieve more information about the payment, refer to the [Payments API](/guides/api/crm/commerce/payments). ## Adding associations To create a complete quote, you'll need to associate it with other CRM records, such as line items, using the [associations API](/guides/api/crm/associations/associations-v4). The table below shows which CRM record associations are required for a complete quote, and which are optional. Continue reading to learn more about retrieving IDs and using them to create the needed associations. | Object type | Required | Description | | --- | --- | --- | | [Line items](/guides/api/crm/commerce/line-items) | Yes | The goods and/or services being sold through the quote. You can create line items from [products in your product library](/guides/api/crm/commerce/products) or create custom standalone line items. | | [Quote template](https://knowledge.hubspot.com/quotes/create-custom-quote-templates) | Yes | The template that renders the quote, along with providing some default configuration settings for the quote, such as language. Each quote can be associated with one template. | | [Deal](/guides/api/crm/objects/deals) | Yes | The deal record for tracking revenue and sales lifecycle. A quote inherits values from the associated deal, including the owner and currency. Each quote can be associated with one deal. | | [Contact](/guides/api/crm/objects/contacts) | No | Specific buyers that you're addressing in the quote. | | [Company](/guides/api/crm/objects/companies) | No | A specific company that you're addressing in the quote. Each quote can be associated with one company. | | [Discounts](/guides/api/crm/commerce/discounts), [fees](/guides/api/crm/commerce/fees), and [taxes](/guides/api/crm/commerce/taxes) | No | Any discounts, fees, and taxes to be applied at checkout. When determining the total quote amount, HubSpot first applies discounts, followed by fees, then taxes. You can use the `hs_sort_order` field to reorder objects of the same type. Can be set to fixed values or percentages by setting `hs_type` to either `FIXED` or `PERCENT`. | ### Retrieving IDs for associations To make each association, you'll first need to retrieve the ID of each object you want to associate. To retrieve each ID, you'll make a `GET` request to the relevant object endpoint, which follows the same pattern across each CRM object. When making each request, you can also include a `properties` query parameter to return specific properties when needed. Below are example `GET` requests for each type of object. ```hubl GET request for line items /crm/v3/objects/line_items?properties=name GET request for quote templates /crm/v3/objects/quote_template?properties=hs_name GET request for deals /crm/v3/objects/deals?properties=dealname GET request for contacts /crm/v3/objects/contacts?properties=email GET request for companies /crm/v3/objects/companies?properties=name GET request for discounts crm/v3/objects/discounts?properties=hs_type,hs_value GET request for fees crm/v3/objects/fees?properties=hs_type,hs_value GET request for taxes crm/v3/objects/taxes?properties=hs_type,hs_value ``` ```shell #GET request for line items curl --request GET \ --url 'https://api.hubapi.com/crm/v3/properties/line_items?archived=false' \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #GET request for quote templates curl --request GET \ --url 'https://api.hubapi.com/crm/v3/properties/quote_templates?archived=false' \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #GET request for deals curl --request GET \ --url 'https://api.hubapi.com/crm/v3/properties/deals?archived=false' \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #GET request for contacts curl --request GET \ --url 'https://api.hubapi.com/crm/v3/properties/contacts?archived=false' \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #GET request for companies curl --request GET \ --url 'https://api.hubapi.com/crm/v3/properties/companies?archived=false' \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #GET request for discounts curl --request GET \ --url 'https://api.hubapi.com/crm/v3/properties/discounts?archived=false' \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #GET request for fees curl --request GET \ --url 'https://api.hubapi.com/crm/v3/properties/fees?archived=false' \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #GET request for taxes curl --request GET \ --url 'https://api.hubapi.com/crm/v3/properties/taxes?archived=false' \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' ``` Each successful call will return a `200` response with details for each fetched object type. You'll use the value in the `id` field to set associations in the next step. ```json { "results": [ { "id": "235425923863", "properties": { "hs_createdate": "2023-06-12T16:27:32.794Z", "hs_lastmodifieddate": "2023-06-12T16:27:32.794Z", "hs_name": "Default Basic", "hs_object_id": "235425923863" }, "createdAt": "2023-06-12T16:27:32.794Z", "updatedAt": "2023-06-12T16:27:32.794Z", "archived": false }, { "id": "235425923864", "properties": { "hs_createdate": "2023-06-12T16:27:32.794Z", "hs_lastmodifieddate": "2023-06-12T16:27:32.794Z", "hs_name": "Default Modern", "hs_object_id": "235425923864" }, "createdAt": "2023-06-12T16:27:32.794Z", "updatedAt": "2023-06-12T16:27:32.794Z", "archived": false }, { "id": "235425923865", "properties": { "hs_createdate": "2023-06-12T16:27:32.794Z", "hs_lastmodifieddate": "2023-06-12T16:27:32.794Z", "hs_name": "Default Original", "hs_object_id": "235425923865" }, "createdAt": "2023-06-12T16:27:32.794Z", "updatedAt": "2023-06-12T16:27:32.794Z", "archived": false } ] } ``` ### Creating associations With your IDs retrieved, you can now make calls to the [associations API](/guides/api/crm/associations/associations-v4) to create associations. For each type of object you want to associate with a quote, you'll need to make a separate call by making a `PUT` request using the URL structure below: `/crm/v4/objects/quotes/{fromObjectId}/associations/default/{toObjectType}/{toObjectId}` | Parameter | Description | | --- | --- | | `fromObjectId` | The ID of the quote. | | `toObjectType` | The type of object you're associating with. For example, `line_items`, `deals`, and `quote_template`. | | `toObjectId` | The ID of the object you're associating the quote with. | Below are example `PUT` requests for each type of object. ```hubl PUT request to associate a line item /crm/v4/objects/quotes/{quoteId}/associations/default/line_items/{lineItemId} PUT request to associate a quote template /crm/v4/objects/quotes/{quoteId}/associations/default/quote_template/{quoteTemplateId} PUT request to associate a deal /crm/v4/objects/quotes/{quoteId}/associations/default/deals/{dealId} PUT request to associate contacts /crm/v4/objects/quotes/{quoteId}/associations/default/contacts/{contactId} PUT request to associate companies /crm/v4/objects/quotes/{quoteId}/associations/default/companies/{companyId} PUT request to associate discounts /crm/v4/objects/quotes/{quoteId}/associations/default/discounts/{discountId} PUT request to associate fees /crm/v4/objects/quotes/{quoteId}/associations/default/fees/{feeId} PUT request to associate taxes /crm/v4/objects/quotes/{quoteId}/associations/default/taxes/{taxId} ``` ```shell #PUT request to associate line items curl --request PUT \ --url https://api.hubapi.com/crm/v4/objects/quotes/{quoteId}/associations/default/line_items/{lineItemId} \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #PUT request to associate a quote template curl --request PUT \ --url https://api.hubapi.com/crm/v4/objects/quotes/{quote_ID}/associations/default/quote_template/{quoteTemplateId} \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #PUT request to associate a deal curl --request PUT \ --url https://api.hubapi.com/crm/v4/objects/quotes/{quoteId}/associations/default/deals/{dealId} \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #PUT request to associate contacts curl --request PUT \ --url https://api.hubapi.com/crm/v4/objects/quotes/{quoteId}/associations/default/contacts/{contactId} \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #PUT request to associate a company curl --request PUT \ --url https://api.hubapi.com/crm/v4/objects/quotes/{quoteId}/associations/default/companies/{companyId} \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #PUT request to associate discounts curl --request PUT \ --url https://api.hubapi.com/crm/v4/objects/quotes/{quoteId}/associations/default/discounts/{discountId} \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #PUT request to associate fees curl --request PUT \ --url https://api.hubapi.com/crm/v4/objects/quotes/{quoteId}/associations/default/fees/{feeId} \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' #PUT request to associate taxes curl --request PUT \ --url https://api.hubapi.com/crm/v4/objects/quotes/{quoteId}/associations/default/line_items/{taxId} \ --header 'authorization: Bearer YOUR_ACCESS_TOKEN' ``` As an example, if your quote has an ID of `123456`, the requests to associate the quote might include the following: - **Line items (IDs: `55555`, `66666`):** - `/crm/v4/objects/quotes/123456/associations/default/line_items/55555` - `/crm/v4/objects/quotes/123456/associations/default/line_items/66666` - **Quote template (ID:** `987654`**):** `/crm/v4/objects/quotes/123456/associations/default/quote_template/987654` - **Deal (ID: `345345`):** `/crm/v4/objects/quotes/123456/associations/default/deals/345345` Each successful association will return a `200` response with details about the association. The above calls will associate the objects in both directions, with each direction have its own ID. For example, if you associate the quote with a quote template, the response will describe the association from both ends. In the example response below, `286` is the quote-to-quote-template association type ID, and `285` is the quote-template-to-quote association type ID. ```json { "status": "COMPLETE", "results": [ { "from": { "id": "115045534742" }, "to": { "id": "102848290" }, "associationSpec": { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 285 } }, { "from": { "id": "102848290" }, "to": { "id": "115045534742" }, "associationSpec": { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 286 } } ], "startedAt": "2023-10-12T16:48:40.624Z", "completedAt": "2023-10-12T16:48:40.712Z" } ``` - Quote templates must be [created](https://knowledge.hubspot.com/quotes/create-custom-quote-templates) before they can be associated with a quote. - A quote can only be associated with on quote template. - This API does not support legacy or proposal quotes. Only the `CUSTOMIZABLE_QUOTE_TEMPLATE` template type can be used. ### Associating quote signers If you're [enabling the quote for e-signatures](#enable-e-signatures), you'll also need to create an association between the quote and the contacts who are signing by using a specific quote-to-contact [association label](/guides/api/crm/associations/associations-v4#associate-records-with-a-label). Rather than using the default association endpoints shown above, you'll need to make a `PUT` request to the following URL: `/crm/v4/objects/quote/{quoteId}/associations/contact/{contactId}` In the request body, you'll need to specify the `associationCategory` and `associationTypeId`, as shown below: ```json [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 702 } ] ``` ## Create a quote with associations (single request) The following request body will create a new quote with associations to a quote template, a deal, two line items, and a contact. `POST` `/crm/v3/objects/quote` ```json { "properties": { "hs_title": "CustomerName - annual SEO audit", "hs_expiration_date": "2023-09-30" }, "associations": [ { "to": { "id": 115045534742 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 286 } ] }, { "to": { "id": 14795354663 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 64 } ] }, { "to": { "id": 75895447 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 67 } ] }, { "to": { "id": 256143985 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 67 } ] } ] } ``` | Parameter | Type | Description | | --- | --- | --- | | `properties` | Object | Property values to define the quote details. Required properties are `hs_title` and `hs_expiration_date`: <ul><li>`hs_title`: the name of the quote.</li><li>`hs_expiration_date`: the date that the quote expires.</li><li>`hs_status`: the [quote state](#update-quote-states). If not provided at creation, users will not be able to edit the quote in HubSpot.</li></ul> | | `associations` | Array | The other CRM records and objects associated with the quote. For a quote to be publishable, it must have an associated deal and quote template. Line items associated with a quote should be distinct from the line items associated with the quote's deal (i.e., you should create copies of the line items).<br /><br />To set each association, include a separate object in this array with the following fields:<ul><li>`to`: the ID of the record or object to associate with the quote.</li><li>`associationCategory`: the [label category](/guides/api/crm/associations/associations-v4#associate-records-with-a-label) of the association type, either `"HUBSPOT_DEFINED"` (default label) or `"USER_DEFINED"` (custom label).</li><li>`associationTypeId`: the [ID of the type of association](/guides/api/crm/associations/associations-v4#association-type-id-values) being made. For example:<ul><li>`286`: quote to quote template</li><li>`64`: quote to deal</li><li>`67`: quote to line item</li></ul></li></ul> | These line items should be different to line items on other objects, even if they are associated (e.g., associating a quote to a deal). See the [line items API documentation](/guides/api/crm/commerce/line-items) for more information. ## Update quote state A quote's state describes how far along it is in the creation process, from initial set up to being published and publicly accessible. Quote state can also reflect the [quote approval process](https://knowledge.hubspot.com/quotes/use-quotes), if quote approvals are enabled for the account. When setting a quote's state, HubSpot will [automatically fill in certain properties](#properties-set-by-quote-state). You can update a quote's state by making a `PATCH` request to `/crm/v3/objects/quote/{quoteId}`. A quote's state is based on the `hs_status` field. Certain quote states allow users to edit, publish, and use the quote in quote approval workflows. Below are the available quote states. - **No state:** if no value is provided for the `hs_status` field, the quote will be in a _Minimal_ state. The quote will appear on the index page of the quotes tool, but cannot be edited directly. Quotes in this state can still be used in automation, such as the sequences tool, and are also available to analyze within the reporting tool. - `DRAFT`: enables the quote to be edited in HubSpot. This state can be useful for when the quote isn't fully configured or if you'd rather enable sales reps to complete the quote configuration process in HubSpot. - `APPROVAL_NOT_NEEDED`: publishes the quote at a publicly accessible URL (`hs_quote_link`) without needing to be [approved](https://knowledge.hubspot.com/quotes/use-quotes). - `PENDING_APPROVAL`: indicates that the quote is [waiting to be approved](https://knowledge.hubspot.com/quotes/use-quotes) before it can be published. - `APPROVED`: the quote has been [approved](https://knowledge.hubspot.com/quotes/use-quotes) and is now published at a publicly accessible URL (`hs_quote_link`). - `REJECTED`: indicates that the quote has been set up but has not been [approved](https://knowledge.hubspot.com/quotes/use-quotes) for publishing, and must be edited before it can be submitted for approval again. If you're [enabling e-signatures](#enabling-e-signatures) on the quote, you won't be able to publish the quote if you've exceeded your [monthly e-signature limit](https://knowledge.hubspot.com/quotes/use-e-signatures-with-quotes#monitor-e-signature-usage). For example, the following request would publish the quote at a publicly accessible URL. ```json { "properties": { "hs_status": "APPROVAL_NOT_NEEDED" } } ``` By default, HubSpot will set the quote's `hs_template_type` property to `CUSTOMIZABLE_QUOTE_TEMPLATE` after you [update the quote's state](/guides/api/crm/commerce/quotes#update-quote-state). This template type is supported by the v3 API, whereas the following template types are legacy templates that are no longer supported: - `QUOTE` - `PROPOSAL` ### Properties set by quote state Updating the quote's state will update the following properties: - `hs_quote_amount`: calculated based on any associated line items, taxes, discounts, and fees. - `hs_domain`: set from the associated quote template. - `hs_slug`: randomly generated if one is not explicitly provided. - `hs_quote_total_preference`: set based on your account settings. If you haven't configured this setting, it will default to the value of the total_first_payment field. - `hs_timezone`: defaults to your HubSpot account's timezone. - `hs_locale`: set from the associated quote template. - `hs_quote_number`: set based on the current date and time, unless one is provided. - `hs_language`: set from the associated quote template. - `hs_currency`: set based on the associated deal. If you haven't associated a deal with the quote, it will default to your HubSpot account's default currency. Additionally, the properties below will be calculated when the quote is set to a published state: - `hs_pdf_download_link`: populated with a URL of a PDF for the quote. - `hs_locked`: set to `true`. To modify any properties after you've published a quote, you must first update the `hs_status` of the quote back to `DRAFT`, `PENDING_APPROVAL`, or `REJECTED`. - `hs_quote_link`: the quote's publicly accessible URL. This is a read-only property and cannot be set through the API after publishing. - `hs_esign_num_signers_required`: if you've [enabled e-signatures](#enable-e-signatures), displays the number of signatures required. - `hs_payment_status`: the status of payment collection, if you’ve enabled payments. Upon publishing with payments enabled, this property will be set to PENDING. Once the buyer submits payment through the quote, the status will automatically update accordingly. Learn more about [enabling payments](/guides/api/crm/commerce/quotes#enable-payments). ## Retrieve quotes You can retrieve quotes individually or in batches. - To retrieve an individual quote, make a `GET` request to `/crm/v3/objects/quotes/{quoteID}`. - To request a list of all quotes, make a `GET` request to `/crm/v3/objects/quotes`. For these endpoints, you can include the following query parameters in the request URL: | Parameter | Description | | --- | --- | | `properties` | A comma separated list of the properties to be returned in the response. If the requested quotecontact doesn't have a value for a property, it will not appear in the response. | | `propertiesWithHistory` | A comma separated list of the current and historical properties to be returned in the response. If the requested quote doesn't have a value for a property, it will not appear in the response. | | `associations` | A comma separated list of objects to retrieve associated IDs for. Any specified associations that don't exist will not be returned in the response. Learn more about the [associations API.](/guides/api/crm/associations/associations-v4) | To retrieve a batch of specific quotes by their IDs, make a `POST` request to `crm/v3/objects/quotes/batch/read` and include the IDs in the request body. You can also include a `properties` array to specify which properties to return. The batch endpoint cannot retrieve associations. Learn how to batch read associations with the [associations API](/guides/api/crm/associations/associations-v4). ```json { "inputs": [{ "id": "342007287" }, { "id": "81204505203" }], "properties": ["hs_content", "hs_sentiment", "hs_submission_timestamp"] } ``` ## Scopes The following scopes are required for an app to create a valid publishable quote: - `crm.objects.quotes.write`, `crm.objects.quotes.read`, `crm.objects.line_items.write`, `crm.objects.line_items.read`, `crm.objects.owners.read`, `crm.objects.contacts.write`, `crm.objects.contacts.read`, `crm.objects.deals.write`, `crm.objects.deals.read`, `crm.objects.companies.write`, `crm.objects.companies.read` - `crm.schemas.quote.read`, `crm.schemas.line_items.read`, `crm.schemas.contacts.read`, `crm.schemas.deals.read`, `crm.schemas.companies.read` # Subscriptions API @ Learn about using HubSpot APIs to retrieve information about subscriptions. $ guides & api/crm/commerce --- # Subscriptions Use the subscriptions API to fetch information about an account's [commerce subscriptions](https://knowledge.hubspot.com/subscriptions/manage-subscriptions-for-recurring-payments). This is a read-only API, so it cannot be used for creating new or managing existing subscriptions. If you're looking to manage marketing email subscriptions instead, check out the [subscription preferences API](/guides/api/marketing/subscriptions-preferences). For example, use this API to [fetch all currently active subscriptions](#search-for-subscriptions-by-properties). ## Requirements To use this API, the account must be set up to collect payments through either [HubSpot payments](https://knowledge.hubspot.com/payment-processing/set-up-payments) or [Stripe payment processing](https://knowledge.hubspot.com/payment-processing/connect-your-stripe-account-as-a-payment-processor-in-hubspot). ## Retrieve subscriptions Depending on the information you need, there are a few ways to retrieve subscriptions: - To retrieve all subscriptions, make a `GET` request to `crm/v3/objects/subscriptions`. - To retrieve a specific subscription, make a `GET` request to the above URL and specify a subscription ID. For example: `crm/v3/objects/subscriptions/41112146008`. - To retrieve subscriptions that meet a specific set of criteria, you can make a `POST` request to the search endpoint and include filters in the request body. See an example of using the [search endpoint below](#search-for-invoices-by-filter-criteria). The response will include a few default properties, including the create date, last modified date. ```json { "id": "41112146008", "properties": { "hs_createdate": "2023-03-08T14:54:17.333Z", "hs_lastmodifieddate": "2024-03-01T22:33:09.011Z", "hs_object_id": "44446244097" }, "createdAt": "2023-03-08T14:54:17.333Z", "updatedAt": "2024-03-01T22:33:09.011Z", "archived": false } ``` ### Properties To return specific properties, include a `properties` query parameter in the request URL along with comma-separated property names. For example, making a `GET` request to the following URL would result in the response below: `crm/v3/objects/subscriptions?properties=hs_status,hs_last_payment_amount` ```json { "id": "41112146008", "properties": { "hs_createdate": "2022-09-02T15:03:40.828Z", "hs_last_payment_amount": "200.00", "hs_lastmodifieddate": "2024-02-27T15:03:53.620Z", "hs_object_id": "41112146008", "hs_status": "active" }, "createdAt": "2022-09-02T15:03:40.828Z", "updatedAt": "2024-02-27T15:03:53.620Z", "archived": false } ``` To view all available subscription properties, make a `GET` request to `crm/v3/properties/subscriptions`. Learn more about using the [properties API](/guides/api/crm/properties). Below are some common subscription properties that you may want to query. | Property name | Label in UI | Description | | --- | --- | --- | | `hs_status` | [Status](https://knowledge.hubspot.com/payments/hubspots-payments-and-subscriptions-properties#subscription-properties:~:text=the%20status%20of%20the%20subscription) | The current status of the subscription. Values include:<ul><li>`active`</li><li>`past_due`</li><li>`canceled`</li><li>`expired`</li><li>`scheduled`</li></ul> | | `hs_recurring_billing_start_date` | [Start date](https://knowledge.hubspot.com/payments/hubspots-payments-and-subscriptions-properties#subscription-properties:~:text=for%20the%20subscription.-,Start%20date%3A,-the%20date%20the) | The date that the subscription is scheduled to start. | | `hs_last_payment_amount` | [Last payment amount](https://knowledge.hubspot.com/payments/hubspots-payments-and-subscriptions-properties#subscription-properties:~:text=is%20updated%20automatically.-,Last%20payment%20amount,-%3A%C2%A0the%20amount) | The amount that was charged during the most recent billing period. | | `hs_next_payment_amount` | [Next payment amount](https://knowledge.hubspot.com/payments/hubspots-payments-and-subscriptions-properties#subscription-properties:~:text=of%20the%20subscription.-,Next%20payment%20amount,-%3A%20the%20amount) | The amount that will be charged at the start of the next billing period. | | `hs_next_payment_due_date` | [Next payment due date](https://knowledge.hubspot.com/payments/hubspots-payments-and-subscriptions-properties#subscription-properties:~:text=Next%20payment%20due%20date) | The date that the next payment is due. | ### Search for subscriptions by properties You can use the search endpoint to retrieve subscriptions that meet a specific set of [filter criteria](/guides/api/crm/search#filter-search-results). This will be a `POST` request that includes your filter criteria in the request body. For example, to search for all currently active subscriptions, you would make a `POST` request to `crm/v3/objects/subscriptions/search` with the following request body: ```json { "filterGroups": [ { "filters": [ { "propertyName": "hs_status", "value": "active", "operator": "EQ" } ] } ], "properties": ["hs_status", "hs_last_payment_amount"] } ``` Note that the `filters` array specifies the search criteria, while the `properties` array specifies which properties to return. ## Associations While you cannot set associations using this API, you can retrieve association information by making a GET request to the following URL: `crm/v3/objects/subscriptions/{hs_object_id}/associations/{associatedObjectName}` Associated objects can include [contacts](/guides/api/crm/objects/contacts), [companies](/guides/api/crm/objects/companies), [deals](/guides/api/crm/objects/deals), [quotes](/guides/api/crm/commerce/quotes), [line items](/guides/api/crm/commerce/line-items), [payments](/guides/api/crm/commerce/payments), [discounts](/guides/api/crm/commerce/discounts), **[fees](/guides/api/crm/commerce/fees)**, and **[taxes](/guides/api/crm/commerce/taxes)**. These associations are typically set by the payment link or quote associated with the initial subscription payment. To manage these associations, you can [update the subscription in HubSpot](https://knowledge.hubspot.com/subscriptions/manage-subscriptions-for-recurring-payments#edit-a-subscription). Below is an example of how you might use this API combined with another API to get a specific set of association information. Line items belong to one single parent object. For example, if retrieving line items from a subscription, the line item ID’s will be different to those on a deal, or a quote. ### Retrieving a subscription with associated line items To retrieve a subscription and the line items associated with it, make a `GET` request to: `crm/v3/objects/subscription/{hs_object_id}/associations/line_items` This will return the IDs of the currently associated line items, along with meta information about the association type. ```json { "results": [ { "id": "1459694380", "type": "subscription_to_line_item" }, { "id": "1459694381", "type": "subscription_to_line_item" } ] } ``` You can then use the returned IDs to request more information about the line items through the [line items API](/guides/api/crm/commerce/line-items). For example, you could batch retrieve line items by ID with the following `POST` request: `crm/v3/objects/line_items/batch/read` ```json { "inputs": [{ "id": "1459694380" }, { "id": "1459694381" }], "properties": ["name", "amount"] } ``` The response would be formatted as follows: ```json { "status": "COMPLETE", "results": [ { "id": "1459694381", "properties": { "amount": "100.00", "createdate": "2023-11-08T18:23:06.361Z", "hs_lastmodifieddate": "2023-11-08T18:23:06.361Z", "hs_object_id": "1459694381", "name": "Recurring line item 2" }, "createdAt": "2023-11-08T18:23:06.361Z", "updatedAt": "2023-11-08T18:23:06.361Z", "archived": false }, { "id": "1459694380", "properties": { "amount": "100.00", "createdate": "2023-11-08T18:23:06.361Z", "hs_lastmodifieddate": "2023-11-08T18:23:06.361Z", "hs_object_id": "1459694380", "name": "Recurring line item 1" }, "createdAt": "2023-11-08T18:23:06.361Z", "updatedAt": "2023-11-08T18:23:06.361Z", "archived": false } ], "startedAt": "2024-03-14T15:43:53.179Z", "completedAt": "2024-03-14T15:43:53.186Z" } ``` # CRM API | Taxes @ Learn how to create and associate a tax as part of the pricing details of a quote. $ guides & api/crm/commerce --- # Taxes When you're [creating a quote in HubSpot](/guides/api/crm/commerce/quotes), you can create and associate a tax as part of the pricing details of the quote. The functionality described in this article pertains to the order-level tax object you can create and apply to [invoices](/guides/api/crm/commerce/invoices) and [subscriptions](/guides/api/crm/commerce/subscriptions). This object is distinct from the tax rate object that applies separately to line items only. Learn more about tax rates in the [line items API guide](/guides/api/crm/commerce/line-items#retrieve-tax-rates). ## Create a tax Taxes are used in conjunction with [discounts](/guides/api/crm/commerce/discounts) and [fees](/guides/api/crm/commerce/fees) when determining the pricing details for a quote. Any discounts you associate with your quote will be applied first, followed by associated fees, and then any associated taxes will apply. ```json // POST request to https://api.hubspi.com/crm/v3/objects/tax { "properties": { "hs_label": "A percentage-based tax of 6.5%", "hs_type": "PERCENT", "hs_value": "6.5" } } ``` After you create a tax, you can use its ID to associate it with a quote. To retrieve a list of taxes you've created, you can make a `GET` request to `/crm/v3/objects/tax`. To view all available endpoints and their required fields, check out the [reference documentation](https://developers.hubspot.com/docs/reference/api/crm/commerce/taxes). # CRM API | HubSpot CRM Embed @ Learn how to embed a HubSpot view such as a record timeline, property history, or meeting link in a third-party platform. $ guides & api/crm --- # HubSpot CRM Embed The HubSpot CRM Embed feature allows you to display interactive HubSpot interfaces inside your app. You can directly embed views from specific tools from your account, including the object timeline, along with tabs with the record's enrollment history and management for a workflow, property management, and a view to schedule and manage meetings. This functionality is distinct from the [content embed](https://www.hubspot.com/products/content/embedabble-content-blocks) feature and and the [content timeline embed](/reference/api/crm/contacts-timeline-embed). ## Supported HubSpot views and tools The following tools are available to embed within your app: - **Object timeline:** review activities related to an object record, including any upcoming meetings or tasks, along with a fully interactive sidebar. You can apply activity filters and access association cards within the embedded view (e.g., related contacts, companies, deals, etc.). - **Workflows tab:** track workflow enrollment history, enroll records into workflows, or search workflows by name. - **Sequences tab:** if you're embedding the view for a contact, you can manage sequences, enroll contacts, or track progress through a sequence. - **Properties tab:** search for and filter properties, view property history, and hide blank fields you're not actively using. - **Meetings tab:** schedule meetings, or manage and review details of upcoming meetings. - **Company & Deal Functionality:** if you're embedding a view for a company or deal, you can review the _Target Accounts_ tab for metrics such as emails and meetings, along with other aggregate metrics. Only certain object types are available for embedding your app. At this time, only contacts, companies, deals, and tickets are supported. ## Linking to a specific view in your HubSpot account To link to a specific view in your HubSpot account, you'll need to customize an embed URL based on the associated record and tool you want to appear: Each URL will have the following structure: `https://app.hubspot.com/embed/{hubId}/{objectType}/{recordId}/{view}` - **hubId:** the Hub ID of the HubSpot account. - **objectType:** the object type you want to link to (e.g., a contact, company, deal, etc.). - **recordId:** the instance of the object you want to link to. - **view:** the specific tool or view you want to link to. The available view types you can specify here include timeline, workflows, sequences, For example, to embed a view for the contact timeline when the contact has an ID of 251, and the Hub ID was 12345, the resulting embed URL would be: `https://app.hubspot.com/embed/12345/0-1/251/timeline` A user will need to be logged into their HubSpot account to view the embedded view. If a user isn't authenticated, they'll be prompted to log in to their account. You can follow the steps in the section below to use the embed sandbox to explore and test how different views from your account would appear in an embedded window. ## Linking to a specific view in your HubSpot account HubSpot provides an embed sandbox environment you can use to find specific records in your account and preview how they would appear in an embedded window. - Navigate to the [embed sandbox environment](https://app.hubspot.com/login?loginRedirectUrl=https%3A%2F%2Fapp.hubspot.com%2Fshortlink%2Fhubspot-embed-sandbox-ui) for your account. If you're prompted with an account selection page, select the specific HubSpot account with the data you want to embed in your app. - Click the **Object Type ID** dropdown menu to select an object type, then click the **Object ID** dropdown menu and select a specific instance of that object. - In the embedded preview, you can interactive with the object timeline, or click the tabs at the top to use other tools in the context of that record. - You can click **Copy** to copy the associated _Embed URL_ to the clipboard, where you can then embed it within your app. You'll need to append the **view** to the _Embed URL_ to ensure that URL loads correctly (e.g., to link directly to the sequences associated with a specific contact, the resulting URL would be: `https://app.hubspot.com/embed/12345/0-1/251/sequences`). # Engagements | Calls @ Use the calls engagement API to log and manage calls on CRM records. $ guides & api/crm/engagements --- # Calls Use the calls engagement API to log and manage calls on CRM records and on the [calls index page](https://knowledge.hubspot.com/crm-setup/create-customize-and-manage-your-saved-views). You can log calls either [in HubSpot](https://knowledge.hubspot.com/contacts/manually-log-a-call-email-or-meeting-on-a-record) or through the calls API. Below, learn the basic methods of managing calls through the API. To view all available endpoints and their requirements, check out the [reference documentation](/reference/api/crm/engagements/calls). ## Create a call engagement To create a call engagement, make a `POST` request to `/crm/v3/objects/calls`. In the request body, add call details in a **properties** object. You can also add an **associations** object to associate your new call with an existing record (e.g.,contacts, companies). ### Properties Below is a list of HubSpot default calling properties that you can include in the properties object. You can also create custom properties using the [properties API](/guides/api/crm/properties). | Field | Description | | --- | --- | | `hs_timestamp` | Required. This field marks the call's time of creation and determines where the call sits on the record timeline. You can use either a Unix timestamp in milliseconds or UTC format. | | `hs_call_body` | The description of the call, including any notes that you want to add. | | `hs_call_callee_object_id` | The ID of the HubSpot record associated with the call. This will be the recipient of the call for `OUTBOUND` calls, or the dialer of the call for `INBOUND` calls. | | `hs_call_callee_object_type` | The type of the object to which the call's associated record belongs (e.g., specifies if the record is a contact or company). This will be the object of the recipient for `OUTBOUND`calls, or the object of the dialer for `INBOUND` calls. | | `hs_call_direction` | The direction of the call from the perspective of the HubSpot user. If the user is the call recipient, the direction should be set to `INBOUND`. If the user initiated the call, the direction should be set to `OUTBOUND`. | | `hs_call_disposition` | The outcome of the call. To set the call disposition, you need to use the internal GUID value. If your account has set up [custom call outcomes](https://knowledge.hubspot.com/calling/create-custom-call-and-meeting-outcomes), you can find their disposition GUIDs using [this API](/reference/api/crm/engagements/engagement-details#get-call-engagement-dispositions). The default HubSpot outcome labels and their internal values are:<ul><li>Busy: `9d9162e7-6cf3-4944-bf63-4dff82258764`</li><li>Connected: `f240bbac-87c9-4f6e-bf70-924b57d47db7`</li><li>Left live message: `a4c4c377-d246-4b32-a13b-75a56a4cd0ff`</li><li>Left voicemail: `b2cf5968-551e-4856-9783-52b3da59a7d0`</li><li>No answer: `73a0d17f-1163-4015-bdd5-ec830791da20`</li><li>Wrong number: `17b47fee-58de-441e-a44c-c6300d46f273`</li></ul> | | `hs_call_duration` | The duration of the call in milliseconds. | | `hs_call_from_number` | The phone number that the call was made from. | | `hs_call_recording_url` | The URL that stores the call recording. URLS to .mp3 or .wav files can be played back on CRM records. Only HTTPS, secure URLs will be accepted. | | `hs_call_status` | The status of the call. The statuses are `BUSY`, `CALLING_CRM_USER`, `CANCELED`, `COMPLETED`, `CONNECTING`, `FAILED`, `IN_PROGRESS`, `NO_ANSWER`, `QUEUED`, and `RINGING`. | | `hs_call_title` | The title of the call. | | `hs_call_source` | The source of the call. This is not required, but it is required to leverage the [recording and transcriptions pipeline](/guides/apps/extensions/calling-extensions/recordings-and-transcriptions#log-a-call-with-your-app-s-endpoint-using-the-engagements-api). If the property is set, it must be set to `INTEGRATIONS_PLATFORM`. | | `hs_call_to_number` | The phone number that received the call. | | `hubspot_owner_id` | The [ID of the owner](/guides/api/crm/owners) associated with the call. This field determines the user listed as the call creator on the record timeline. | | `hs_activity_type` | The type of call. The options are based on the [call types set in your HubSpot account.](https://knowledge.hubspot.com/meetings-tool/how-do-i-create-and-use-call-and-meeting-types) | | `hs_attachment_ids` | The IDs of the call's attachments. Multiple attachment IDs are separated by a semi-colon. | ### Associations To create and associate a call with existing records, include an associations object in your request. For example, to create a call and associate it with a contact and a ticket, your request body might look similar to the following: ```json // Example request body { "properties": { "hs_timestamp": "2021-03-17T01:32:44.872Z", "hs_call_title": "Support call", "hubspot_owner_id": "11349275740", "hs_call_body": "Resolved issue", "hs_call_duration": "3800", "hs_call_from_number": "(857) 829 5489", "hs_call_to_number": "(509) 999 9999", "hs_call_recording_url": "https://api.twilio.com/2010-04-01/Accounts/AC890b8e6fbe0d989bb9158e26046a8dde/Recordings/RE3079ac919116b2d22", "hs_call_status": "COMPLETED" }, "associations": [ { "to": { "id": 500 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 194 } ] }, { "to": { "id": 1234 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 220 } ] } ] } ``` In the associations object, you should include the following: | Field | Description | | --- | --- | | `to` | The record you want to associate with the call, specified by its unique `id` value. | | `types` | The type of the association between the call and the record. Include the `associationCategory`and `associationTypeId`. Default association type IDs are listed [here](/guides/api/crm/associations/associations-v4#association-type-id-values), or you can retrieve the value for custom association types (i.e. labels) via the [associations API](/guides/api/crm/associations/associations-v4#retrieve-association-types). | Learn more about batch creating calls by checking out the [reference documentation](/reference/api/crm/engagements/calls#post-%2Fcrm%2Fv3%2Fobjects%2Fcalls%2Fbatch%2Fcreate). ## Retrieve calls You can retrieve calls individually or in bulk. Learn more about batch retrieval by checking out the [reference documentation](/reference/api/crm/engagements/calls#post-%2Fcrm%2Fv3%2Fobjects%2Fcalls%2Fbatch%2Fread). To retrieve an individual call by its call ID, make a `GET` request to `/crm/v3/objects/calls/{callId}`. You can include the following parameters in the request URL: | Parameter | Description | | --- | --- | | `properties` | A comma separated list of the [properties](#call-properties-1) to be returned. | | `associations` | A comma separated list of object types to retrieve associated IDs for. Any specified associations that don't exist will not be returned in the response. Learn more about the [associations API.](/guides/api/crm/associations/associations-v4) | To request a list of all of calls, make a `GET` request to `/crm/v3/objects/calls`. You can include the following parameters in the request URL: | Parameter | Description | | --- | --- | | `limit` | The maximum number of results to display per page. | | `properties` | A comma separated list of the [properties](#call-properties-1) to be returned. | When you make a successful request, the response will include the `callId` which you can use to retrieve, update, and delete the call. ## Identify voicemails vs. recorded calls For recorded calls and voicemails, a recording is stored in the `hs_call_recording_url` property. If your account has access to [inbound calling](https://knowledge.hubspot.com/calling/receive-calls-in-hubspot), to differentiate between calls that were completed and recorded vs. inbound calls with a voicemail, include the following properties in your request: `hs_call_status` and `hs_call_has_voicemail`. If a call has a voicemail, the `hs_call_status` value will be `missed`, and the `hs_call_has_voicemail` value will be `true`. The `hs_call_has_voicemail` value will be `false` for an inbound call where no voicemail was left, or `null` if the call has a status other than missed. ## Update calls You can update calls individually or in batches. To update an individual call by its call ID, make a `PATCH` request to `/crm/v3/objects/calls/{callId}`. In the request body, include the call properties that you want to update: ```json //Example PATCH request to https://api.hubspot.com/crm/v3/objects/calls/{callID} { "properties": { "hs_timestamp": "2021-03-17T01:32:44.872Z", "hs_call_title": "Discovery call", "hubspot_owner_id": "11349275740", "hs_call_body": " Decision maker out, will call back tomorrow", "hs_call_duration": "3800", "hs_call_from_number": "(857) 829 5489", "hs_call_to_number": "(509) 999 9999", "hs_call_recording_url": "https://api.twilio.com/2010-04-01/Accounts/AC890b8e6fbe0d989bb9158e26046a8dde/Recordings/RE3079ac919116b2d22", "hs_call_status": "COMPLETED" } }' ``` HubSpot will ignore values for read-only and non-existent properties. To clear a property value, pass an empty string for the property in the request body. Learn more about batch updating by checking out the [reference documentation](/reference/api/crm/engagements/calls#post-%2Fcrm%2Fv3%2Fobjects%2Fcalls%2Fbatch%2Fupdate). ### Associate existing calls with records To associate a call with records, such as a contact and its associated companies, make a `PUT` request to `/crm/v3/objects/calls/{callId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}`. The request URL contains the following fields: | Field | Description | | --- | --- | | `callId` | The ID of the call. | | `toObjectType` | The type of object that you want to associate the call with (e.g., contact or company) | | `toObjectId` | The ID of the record that you want to associate the call with. | | `associationTypeId` | A unique identifier to indicate the association type between the call and the other object. The ID can be represented numerically or in snake case (e.g., `call_to_contact`). You can retrieve the value through the [associations API](/reference/api/crm/associations/association-details). | For example, your request URL might look similar to the following: `https://api.hubspot.com/crm/v3/objects/calls/17591596434/associations/contact/104901/194` ### Remove an association To remove an association between a call and a record, make a `DELETE` request to the same URL as above: `/crm/v3/objects/calls/{callId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}` ## Pin a call on a record You can [pin a call](https://knowledge.hubspot.com/records/pin-an-activity-on-a-record) on a record so it remains on the top of the record's timeline. The call must already be associated with the record prior to pinning, and you an only pin one activity per record. To pin a call, include the call's `id` in the `hs_pinned_engagement_id` field when creating or updating a record via the object APIs. Learn more about using the [companies,](/guides/api/crm/objects/companies#pin-an-activity-on-a-company-record)[contacts](/guides/api/crm/objects/contacts#pin-an-activity-on-a-contact-record), [deals](/guides/api/crm/objects/deals#pin-an-activity-on-a-deal-record), [tickets](/guides/api/crm/objects/tickets#pin-an-activity-on-a-ticket-record), and [custom objects](/guides/api/crm/objects/custom-objects) APIs. ## Delete calls You can delete calls individually or in batches, which will add the call to the recycling bin in HubSpot. You can later [restore the call from the record timeline](https://knowledge.hubspot.com/crm-setup/restore-deleted-activity-in-a-record). To delete an individual call by its call ID, make a `DELETE` request to `/crm/v3/objects/calls/{callId}`. Learn more about deleting calls by checking out the [reference documentation](/reference/api/crm/engagements/calls#delete-%2Fcrm%2Fv3%2Fobjects%2Fcalls%2F%7Bcallid%7D). # Engagements | Communications @ Learn how to use the communication APIs, which allow you to log WhatsApp, LinkedIn, and SMS messages to the CRM record timeline. $ guides & api/crm/engagements --- # Communications You can log external communications via WhatsApp, LinkedIn, or SMS messages on CRM records to add information about the message to the record timeline. You can [log a message directly in your HubSpot account](https://knowledge.hubspot.com/records/manually-log-activities-on-records) or using the API endpoints below. You can review all available endpoints in the [reference documentation](/reference/api/crm/engagements/communications). The Communications API does not apply to marketing SMS messages. Learn how to create and view [marketing SMS messages in HubSpot](https://knowledge.hubspot.com/sms/create-and-send-sms-messages). ## Create a WhatsApp, LinkedIn, or SMS message To create a message, make a `POST` request to `/crm/v3/objects/communications`. In the request body, add message details in a **properties** object. You can also add an **associations** object to associate your new message with an existing record (e.g., contacts, companies). ### Properties In the properties object, you can include the following fields: | Parameter | Description | | --- | --- | | `hs_communication_channel_type` | The channel type of the message that you sent or received from the contact. Supported values are `WHATS_APP`, `LINKEDIN_MESSAGE`, or `SMS`. | | `hs_communication_logged_from` | Enum used to differentiate between conversations objects. This must be set to `CRM` in your request. | | `hs_communication_body` | The text body of the message. | | `hubspot_owner_id` | The [ID of the owner](/guides/api/crm/owners) associated with the message. This field determines the user listed as the message creator on the record timeline. | | `hs_timestamp` | This field marks the message's time of creation and determines where the message appears on the record timeline. You can use either a Unix timestamp in milliseconds or UTC format. | ### Associations To create and associate a postal mail engagement with existing records, include an associations object in your request. For example, if you want to log an SMS message and associate it with a contact and company, your request body might resemble the following: ```json // Example POST request to https://api.hubapi.com/crm/v3/objects/communications { "properties": { "hs_communication_channel_type": "SMS", "hs_communication_logged_from": "CRM", "hs_communication_body": "Texted Linda to confirm that we're ready to move forward with the contract.", "hs_timestamp": "2022-11-12T15:48:22Z", "hubspot_owner_id": 1234567 }, "associations": [ { "to": { "id": 9001 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 87 } ] }, { "to": { "id": 1234 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 81 } ] } ] } ``` In the associations object, you should include the following: | Field | Description | | --- | --- | | `to` | The record you want to associate with the message, specified by its unique `id` value. | | `types` | The type of the association between the message and the record. Include the `associationCategory`and `associationTypeId`. Default association type IDs are listed [here](/guides/api/crm/associations/associations-v4#association-type-id-values), or you can retrieve the value for custom association types (i.e. labels) via the [associations API](/guides/api/crm/associations/associations-v4#retrieve-association-types). | The response to your request will include an ID that you can use to update or associate the message with a record: ```json // Example response from POST request to https://api.hubapi.com/crm/v3/objects/communications { "id": "12021896773", "properties": { "hs_communication_channel_type": "SMS ", "hs_communication_logged_from": "CRM", "hs_communication_body": "Texted John to confirm that we're ready to move forward with the contract.", "hs_timestamp": "2022-11-12T15:48:22Z", "hs_createdate": "2022-11-29T18:35:00.484Z", "hs_lastmodifieddate": "2022-11-29T18:35:00.484Z", "hs_object_id": "12021896773" }, "createdAt": "2022-11-29T18:35:00.484Z", "updatedAt": "2022-11-29T18:35:00.484Z", "archived": false } ``` ## Retrieve messages You can retrieve messages individually or in batches. To retrieve an individual messages, make a `GET` request to `/crm/v3/objects/communication/{communicationId}`. To request a list of all of logged WhatsApp, LinkedIn, and SMS messages, make a `GET` request to `/crm/v3/objects/communications`. For both endpoints, you can include the following query parameters in the request URL: | Parameter | Description | | --- | --- | | `properties` | A comma separated list of the properties to be returned in the response. If the requested message doesn't have a value for a property, it will not appear in the response. | | `associations` | A comma separated list of object types to retrieve associated IDs for. Any specified associations that don't exist will not be returned in the response. Learn more about the [associations API.](/guides/api/crm/associations/associations-v4) | For example, to retrieve messages with their text content and any associated contact IDs, your request URL might look similar to the following: `https://api.hubapi.com/crm/v3/objects/communications?limit=10&properties=hs_communication_body&associations=contact&archived=false`. Learn more about retrieving a batch of messages by internal ID or unique property value in the [reference documentation](/reference/api/crm/engagements/communications#post-%2Fcrm%2Fv3%2Fobjects%2Fcommunications%2Fbatch%2Fread). ## Update messages You can update messages individually or in batches. To update an individual message by its communication ID, make a `PATCH` request to `/crm/v3/objects/communications/{communicationId}`. In the request body, include the message properties that you want to update: ```json // Example PATCH request to https://api.hubapi.com/crm/v3/objects/communications/{communicationId} { "properties": { "hs_communication_body": "Sent a follow-up message to Carla." } } ``` HubSpot will ignore values for read-only and non-existent properties. To clear a property value, pass an empty string for the property in the request body. Learn more about batch updating messages in the [reference documentation](/reference/api/crm/engagements/communications#post-%2Fcrm%2Fv3%2Fobjects%2Fcommunications%2Fbatch%2Fupdate). ### Associate an existing message with a record To associate a message with other CRM records, such as a contact, make a `PUT` request to `/crm/v3/objects/communications/{communicationId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}`. The request URL contains the following fields: | Field | Description | | --- | --- | | `communicationId` | The ID of your WhatsApp, LinkedIn, or SMS message. | | `toObjectType` | The type of object that you want to associate the message with (e.g., contact or company) | | `toObjectId` | The ID of the record that you want to associate the message with. | | `associationTypeId` | A unique identifier to indicate the association type between the message and the other object. The ID can be represented numerically or in snake case (e.g., `communication_to_contact`). You can retrieve the value through the [associations API](/reference/api/crm/associations/association-details). | For example, your request URL might look similar to the following: `https://api.hubapi.com/crm/v3/objects/communications/12021896773/associations/contact/581751/communication_to_contact` ### Remove an association To remove an association between a message and a record, make a `DELETE` request to the same URL as above: `/crm/v3/objects/communications/{communicationId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}` ## Pin a message on a record You can [pin a message](https://knowledge.hubspot.com/records/pin-an-activity-on-a-record) on a record so it remains on the top of the record's timeline. The message must already be associated with the record prior to pinning, and you an only pin one activity per record. To pin a message, include the message's `id` in the `hs_pinned_engagement_id` field when creating or updating a record via the object APIs. Learn more about using the [companies,](/guides/api/crm/objects/companies#pin-an-activity-on-a-company-record)[contacts](/guides/api/crm/objects/contacts#pin-an-activity-on-a-contact-record), [deals](/guides/api/crm/objects/deals#pin-an-activity-on-a-deal-record), [tickets](/guides/api/crm/objects/tickets#pin-an-activity-on-a-ticket-record), and [custom objects](/guides/api/crm/objects/custom-objects) APIs. ## Delete messages You can delete messages individually or in batches, which will add the message to the recycling bin in HubSpot. You can later [restore the message from the record timeline](https://knowledge.hubspot.com/records/restore-deleted-activity-in-a-record). To delete an individual message by its ID, make a `DELETE` request to `/crm/v3/objects/communications/{communicationId}`. Learn more about deleting messages in the [reference documentation](/reference/api/crm/engagements/communications#delete-%2Fcrm%2Fv3%2Fobjects%2Fcommunications%2F%7Bcommunicationid%7D). # Engagements | Email @ Use the email engagement API to log and manage emails on CRM records. $ guides & api/crm/engagements --- # Email Use the email engagement API to log and manage emails on CRM records. You can log email activities either [in HubSpot](https://knowledge.hubspot.com/records/manually-log-activities-on-records) or through the emails API. Below, learn the basic methods of managing emails through the API. To view all available endpoints and their requirements, check out the [reference documentation](/reference/api/crm/engagements/email). ## Create an email To create an email engagement, make a `POST` request to `/crm/v3/objects/emails`. In the request body, add email details in a **properties** object. You can also add an **associations** object to associate your new email with an existing record (e.g., contacts, companies). ### Properties In the properties object, you can include the following fields: | Field | Description | | --- | --- | | `hs_timestamp` | Required. This field marks the email's time of creation and determines where the email sits on the record timeline. You can use either a Unix timestamp in milliseconds or UTC format. | | `hubspot_owner_id` | The [ID of the owner](/guides/api/crm/owners) associated with the email. This field determines the user listed as the email creator on the record timeline. | | `hs_email_direction` | The direction the email was sent in. Possible values include:`EMAIL`: the email was sent from the CRM or sent and logged to the CRM with the [BCC address.](https://knowledge.hubspot.com/connected-email/log-email-in-your-crm-with-the-bcc-or-forwarding-address)`INCOMING_EMAIL`: the email was a reply to a logged outgoing email. `FORWARDED_EMAIL`: the email was [forwarded to the CRM.](https://knowledge.hubspot.com/connected-email/log-email-in-your-crm-with-the-bcc-or-forwarding-address) | | `hs_email_html` | The body of an email if it is sent from a CRM record. | | `hs_email_status` | The send status of the email. The value can be `BOUNCED`, `FAILED`, `SCHEDULED`, `SENDING`, or `SENT`. | | `hs_email_subject` | The subject line of the logged email. | | `hs_email_text` | The body of the email. | | `hs_attachment_ids` | The IDs of the email's attachments. Multiple attachment IDs are separated by a semi-colon. | | `hs_email_headers` | The email's headers. The value for this property will automatically populate certain read only email properties. Learn how to [set email headers.](/guides/api/crm/engagements/email#set-email-headers) | Learn more about batch creating email engagements by checking out the [reference documentation](/reference/api/crm/engagements/email#post-%2Fcrm%2Fv3%2Fobjects%2Femails%2Fbatch%2Fcreate). ### Read only properties There are also some email properties that are read only, which are automatically populated by HubSpot. The properties in the table below are all automatically populated from the `hs_email_headers` value. | Field | Description | | ------------------------- | ---------------------------------------------- | | `hs_email_from_email` | The email address of the email's sender. | | `hs_email_from_firstname` | The email sender's first name. | | `hs_email_from_lastname` | The email sender's last name. | | `hs_email_to_email` | The email addresses of the email's recipients. | | `hs_email_to_firstname` | The first names of the email's recipients. | | `hs_email_to_lastname` | The last names of the email recipient. | When retrieving an email header, you may notice there are values both for `From` and `Sender`. These are often the same, but because `Sender` identifies what actually submitted an email, there are scenarios where the values may differ. For example, if an email is sent from an email alias, the `From` value will refer to the user's actual email address, and the `Sender` value will refer to the email alias. ### Set email headers Since headers automatically populate the read only properties, you may want to manually set the email headers. To set the `hs_email_headers` value, you can use a JSON escaped string with the following data: ```json //Example data { "from": { "email": "from@domain.com", "firstName": "FromFirst", "lastName": "FromLast" }, "to": [ { "email": "ToFirst ToLast<to@test.com>", "firstName": "ToFirst", "lastName": "ToLast" } ], "cc": [], "bcc": [] } ``` For example, your request to create an email may look like: ```json //Example request body { "properties": { "hs_timestamp": "2019-10-30T03:30:17.883Z", "hubspot_owner_id": "47550177", "hs_email_direction": "EMAIL", "hs_email_status": "SENT", "hs_email_subject": "Let's talk", "hs_email_text": "Thanks for youremail", "hs_email_headers": "{\"from\":{\"email\":\"from@domain.com\",\"firstName\":\"FromFirst\",\"lastName\":\"FromLast\"},\"sender\":{\"email\":\"sender@domain.com\",\"firstName\":\"SenderFirst\",\"lastName\":\"SenderLast\"},\"to\":[{\"email\":\"ToFirst+ToLast<to@test.com>\",\"firstName\":\"ToFirst\",\"lastName\":\"ToLast\"}],\"cc\":[],\"bcc\":[]}" } } ``` ### Associations To create and associate an email with existing records, include an associations object in your request. For example, to create an email and associate it with a deal and a contact, your request body might look like the following: ```json // Example request body { "properties": { "hs_timestamp": "2019-10-30T03:30:17.883Z", "hubspot_owner_id": "11349275740", "hs_email_direction": "EMAIL", "hs_email_status": "SENT", "hs_email_subject": "Let's talk", "hs_email_text": "Thanks for your interest let's find a time to connect" }, "associations": [ { "to": { "id": 601 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 210 } ] }, { "to": { "id": 602 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 198 } ] } ] } ``` In the associations object, you should include the following: | Field | Description | | --- | --- | | `to` | The record you want to associate with the email, specified by its unique `id` value. | | `types` | The type of the association between the email and the record. Include the `associationCategory`and `associationTypeId`. Default association type IDs are listed [here](/guides/api/crm/associations/associations-v4#association-type-id-values), or you can retrieve the value for custom association types (i.e. labels) via the [associations API](/guides/api/crm/associations/associations-v4#retrieve-association-types). | ## Retrieve emails You can retrieve emails individually or in bulk. Learn more about batch retrieval by checking out the [reference documentation](/reference/api/crm/engagements/email#post-%2Fcrm%2Fv3%2Fobjects%2Femails%2Fbatch%2Fread). To retrieve an individual email by its email ID, make a `GET` request to `/crm/v3/objects/emails/{emailId}`. You can also include the following parameters in the request URL: | Parameter | Description | | --- | --- | | `properties` | A comma separated list of the properties to be returned. | | `associations` | A comma separated list of object types to retrieve associated IDs for. Any specified associations that don't exist will not be returned in the response. Learn more about the [associations API.](/guides/api/crm/associations/associations-v4) | To request a list of all of emails, make a `GET` request to `crm/v3/objects/emails`. You can include the following parameters in the request URL: | Parameter | Description | | ------------ | -------------------------------------------------------- | | `limit` | The maximum number of results to display per page. | | `properties` | A comma separated list of the properties to be returned. | ## Update emails You can update emails individually or in batches. To update an individual email by its email ID, make a `PATCH` request to `/crm/v3/objects/emails/{emailId}`. In the request body, include the email properties that you want to update. For example, your request body might look similar to the following: ```json // Example request body { "properties": { "hs_timestamp": "2019-10-30T03:30:17.883Z", "hubspot_owner_id": "11349275740", "hs_email_direction": "EMAIL", "hs_email_status": "SENT", "hs_email_subject": "Let's talk tomorrow", "hs_email_text": "Thanks for your interest let's find a time to connect!" } } ``` HubSpot will ignore values for read-only and non-existent properties. To clear a property value, pass an empty string for the property in the request body. Learn more about batch updating by checking out the [reference documentation](/reference/api/crm/engagements/email#post-%2Fcrm%2Fv3%2Fobjects%2Femails%2Fbatch%2Fupdate). ### Associate existing emails with records To associate an email with records, such as a contact and its associated companies, make a `PUT` request to `/crm/v3/objects/emails/{emailId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}`. The request URL contains the following fields: | Field | Description | | --- | --- | | `emailId` | The ID of the email. | | `toObjectType` | The type of object that you want to associate the email with (e.g., contact or company) | | `toObjectId` | The ID of the record that you want to associate the email with. | | `associationTypeId` | A unique identifier to indicate the association type between the email and the other object. The ID can be represented numerically or in snake case (e.g., `email_to_contact`). You can retrieve the value through the [associations API](/reference/api/crm/associations/association-details). | For example, your request URL might look similar to the following: `https://api.hubspot.com/crm/v3/objects/emails/17691787884/associations/contact/104901/198` ### Remove an association To remove an association between an email and a record, make a `DELETE` request to the same URL as above: `/crm/v3/objects/emails/{emailId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}` ## Pin an email on a record You can [pin an email](https://knowledge.hubspot.com/records/pin-an-activity-on-a-record) on a record so it remains on the top of the record's timeline. The email must already be associated with the record prior to pinning, and you an only pin one activity per record. To pin an email, include the email's `id` in the `hs_pinned_engagement_id` field when creating or updating a record via the object APIs. Learn more about using the [companies,](/guides/api/crm/objects/companies#pin-an-activity-on-a-company-record)[contacts](/guides/api/crm/objects/contacts#pin-an-activity-on-a-contact-record), [deals](/guides/api/crm/objects/deals#pin-an-activity-on-a-deal-record), [tickets](/guides/api/crm/objects/tickets#pin-an-activity-on-a-ticket-record), and [custom objects](/guides/api/crm/objects/custom-objects) APIs. ## Delete emails When you delete an email, it is permanently deleted and <u>cannot</u> be restored. You can delete emails individually or in batches. To delete an individual email by its email ID, make a `DELETE` request to `/crm/v3/objects/emails/{emailId}`. Learn more about deleting emails by checking out the [reference documentation](/reference/api/crm/engagements/email#delete-%2Fcrm%2Fv3%2Fobjects%2Femails%2F%7Bemailid%7D). # Engagements API Overview @ The Engagements API is used to manage engagement records, which store data from CRM actions, including notes, tasks, emails, meetings, and calls. $ guides & api/crm/engagements --- # Engagements API Overview Engagements store data from CRM actions, including [notes, tasks, emails, meetings, and calls](https://knowledge.hubspot.com/contacts/a-guide-to-using-records#tour-of-records). Engagements are [associated](/guides/api/crm/associations) with at least one CRM record. **Use case for this API:** The Engagements API can be used to create engagements (in order to log calls, notes, or emails from an external system) and get engagement data for reporting. ### Engagement types Engagement objects all have a `type` that determines what the metadata field should look like. The types and their corresponding metadata are detailed below. The Engagements API supports the following engagement types: - **NOTE** - Note engagements are used to hold simple text information about an object record. - **EMAIL** - Email engagements are used to track emails sent on an object record. - **TASK** - Task engagements are used by users to manage tasks, and are available on the tasks dashboard as well as on associated object records. - **MEETING** - Meeting engagements are used to track face-to-face meetings on an object record. - **CALL** - Call engagements are used to track calls made by users on an object record. **NOTE:** Engagements are used internally by other HubSpot tools and new engagement types may be created., so you may interact with engagement types other than those listed here. Creating or editing non-documented engagement types are **not supported**. ### Email content and OAuth scopes Email engagements (any engagements with the type of `EMAIL`, `INCOMING_EMAIL`, or `FORWARDED_EMAIL`) can contain the content of email messages sent to or received from a contact record. These emails are sent and received through the HubSpot user's email service provider. In order for your integration to receive the content and details of these types of email engagements, your integration will need to request the `sales-email-read` scope. If your integration does not request this scope, the metadata fields of email engagements will be redacted: ```"metadata": { "cc": [], "to": [], "validationSkipped": [], "html": "The content of this email has been redacted. To include it in the response, your app must require the sales-email-read scope.", "bcc": [] } ``` The `ownerId` field will also be redacted from the `engagement` object of the response. ### Response details An example of data returned by the engagements API is shown below. Depending on your account's properties and the engagement type, additional information may be returned beyond the data listed. For more information on how the `disposition` field relates to the call outcome, please see [this endpoint](/reference/api/crm/engagements/engagement-details#get-call-engagement-dispositions). ```javascript { "engagement": { // A set of details about the engagement itself "id": 328550660, // Integer, read-only; The unique ID for the engagement "portalId": 62515, // Integer, read-only; The Hub ID the engagement belongs to. "active": true, // Boolean; Only active engagements will appear in the timeline. "createdAt": 1494352128974, // Integer, read-only; A Unix timestamp in milliseconds representing when the engagement was created. "lastUpdated": 1494352128974, // Integer, read-only; A Unix timestamp in milliseconds representing when the engagement was last updated. "ownerId": 1, // Integer; the ID of an Owner object that will be set as the owner of the engagement. "type": "NOTE", // String; One of NOTE, EMAIL, TASK, MEETING, or CALL, the type of the engagement. "timestamp": 1409172644778 // Integer; A Unix timestamp in milliseconds representing the time that the engagement should appear in the timeline }, "associations": { // A set of associated object IDs "contactIds": [2], // A list vids of associated contact records "companyIds": [], // The companyId of the associated company. Engagements should only be associated with a single comapny "dealIds": [], // A list of dealIds of associated deal records. "ownerIds": [], // Deprecated, use the ownerId to associate engagements with an owner. "workflowIds": [], // Deprecated, has no effect on the engagement "ticketIds": [] // Deprecated, has no effect on the engagement }, "attachments": [ // For NOTE type engagements, a list of ids of files from the file manager that should show up in the attachments list when viewing the record in HubSpot { "id": 4241968539 } ], "metadata": { // A set of data specific to the type of the engagement. See below for more details. } } For note engagements, the type should be 'NOTE', and the metadata needs to have the following format: "metadata": { "body": "note body" //String; The body of the note. The body has a limit of 65536 characters. } For email engagements, the type needs to be set to 'EMAIL, and the metadata needs to have the following format: "metadata": { "from": { // The details of the sender "email": "email@domain.com", // String; the email address of the sender "firstName": "First", // String; The first name of the sender "lastName": "Last" // String; The last name of the sender }, "to": [ // The details of the recipient { "email": "contact name <test@test.com>" // String; the email address of the recipient } ], "cc": [], // A list of details for anyone cc'd on the email. "bcc": [], // A list of details for anyone bcc'd on the email. "subject": "This is the subject of the email", // String; The subject of the email ", // String; The body of the HTML email "text": "This is the body of the email\n\n-Me" // String; The body of the text-only email. } For tasks, the type needs to be "TASK" The 'Assigned to' field will be the owner set by the ownerId in the engagement data. The metadata format is: "metadata": { "body": "This is the body of the task.", // String; The body or details of the task "subject": "Task title" // String; The subject or title of the task "status": "NOT_STARTED", // String; One of NOT_STARTED, COMPLETED, IN_PROGRESS, WAITING, or DEFERRED. // The status of the task "forObjectType": "CONTACT" // String; One of CONTACT or COMPANY, what object type the task is for. } For meetings, the type should be "MEETING", and the metadata is: "metadata": { "body": "This is the description.", // String; the details or body of the meeting "startTime": 1456858800000, // Integer; A Unix timestamp in milliseconds representing the start time of the meeting "endTime": 1456862400000, // Integer; A Unix timestamp in milliseconds representing the end time of the meeting "title": "Event title", // String; the title or subject of the meeting "internalMeetingNotes" : "This is the team note" // String; internal notes for the meeting, displayed in the team notes when viewed in HubSpot. } For calls, the type should be "CALL" and the metadata is: "metadata" : { "toNumber" : "5618769964", // String; the phone number that was called "fromNumber" : "(857) 829-5489", // String; the phone number that was used as the from number "status" : "COMPLETED", // String; will be COMPLETED once the call is finished. "externalId" : "CAc052aca6842211ab1c548dcfed5c9", // String; for calls made in HubSpot, this will be the internal ID of the call // Has no effect for engagements created through the API "durationMilliseconds" : 38000, // Integer; the length of the call in milliseconds "externalAccountId" : "AC890b8e6fbebd59158e26046a8dde", // String; for calls made in HubSpot, this will be the internal ID of the account used to make the call // Has no effect for engagements created through the API "recordingUrl" : "https://api.twilio.com/2010-04-01/Accounts/AC890b8e6fbe0d989bb9158e26046a8dde/Recordings/RE3079ac919116b2d22", // String; the URL of the recording file "body" : "Details go here" // String; the details or notes of the call "disposition" : "73a0d17f-1163-4015-bdd5-ec830791da20" // String; internal GUID that corresponds to the Call Outcome } ``` # Engagements | Meetings @ Use the meetings engagement API to log and manage meetings on CRM records. $ guides & api/crm/engagements --- # Meetings Use the meetings engagement API to log and manage meetings on CRM records. You can log meeting activities either [in HubSpot](https://knowledge.hubspot.com/records/manually-log-activities-on-records) or through the meetings API. You can retrieve, update, or delete meeting engagements that are manually logged on a record, scheduled using the [meetings tool](https://knowledge.hubspot.com/meetings-tool/use-meetings), or [scheduled using the Google Calendar or Office 365 calendar integration](https://knowledge.hubspot.com/integrations/schedule-a-meeting-with-a-contact-in-a-record). Below, learn the basic methods of managing meetings through the API. To view all available endpoints and their requirements, check out the [reference documentation](/reference/api/crm/engagements/meetings). ## Create a meeting To create a meeting engagement, make a `POST` request to `/crm/v3/objects/meetings`. In the request body, add meeting details in a **properties** object. You can also add an **associations** object to associate your new meeting with an existing record (e.g., contacts, companies). ### Properties In the properties object, you can include the following fields: | Field | Description | | --- | --- | | `hs_timestamp` | Required. This field marks the date and time that the meeting occurred. You can use either a Unix timestamp in milliseconds or UTC format. When the property value is missing, the value will default to `hs_meeting_start_time.` | | `hs_meeting_title` | The title of the meeting. | | `hubspot_owner_id` | The [ID of the owner](/guides/api/crm/owners) associated with the meeting. This field determines the user listed as the meeting creator on the record timeline. | | `hs_meeting_body` | The meeting description. | | `hs_internal_meeting_notes` | The internal notes you take for your team during a meeting that are not included in the attendee meeting description. | | `hs_meeting_external_url` | The external URL for the calendar event. For example, this could be a Google calendar link or a Microsoft Outlook calendar link. | | `hs_meeting_location` | Where the meeting takes place. The value could be a physical address, a conference room, a videoconference link, or a phone number. This appears on the calendar invite on the attendee's calendar. | | `hs_meeting_start_time` | The date and time when the meeting starts. The value for this property should match the value for `hs_timestamp`. | | `hs_meeting_end_time` | The date and time when the meeting ends. | | `hs_meeting_outcome` | The outcome of the meeting. The outcome values are scheduled, completed, rescheduled, no show, and canceled. | | `hs_activity_type` | The type of meeting. The options are based on the [meeting types set in your HubSpot account.](https://knowledge.hubspot.com/meetings-tool/how-do-i-create-and-use-call-and-meeting-types) | | `hs_attachment_ids` | The IDs of the meeting's attachments. Multiple attachment IDs are separated by a semi-colon. | ### Associations To create and associate a meeting with existing records, include an associations object in your request. For example, to create and associate a meeting with contacts, your request may look similar to the following: ```json // Example request body { "properties": { "hs_timestamp": "2021-03-23T01:02:44.872Z", "hubspot_owner_id": "11349275740", "hs_meeting_title": "Intro meeting", "hs_meeting_body": "The first meeting to discuss options", "hs_internal_meeting_notes": "These are the meeting notes", "hs_meeting_external_url": "https://Zoom.com/0000", "hs_meeting_location": "Remote", "hs_meeting_start_time": "2021-03-23T01:02:44.872Z", "hs_meeting_end_time": "2021-03-23T01:52:44.872Z", "hs_meeting_outcome": "SCHEDULED" }, "associations": [ { "to": { "id": 101 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 200 } ] }, { "to": { "id": 102 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 200 } ] } ] } ``` The associations object should include: | Field | Description | | --- | --- | | `to` | The record you want to associate with the meeting, specified by its unique `id` value. | | `types` | The type of the association between the meeting and the record. Include the `associationCategory`and `associationTypeId`. Default association type IDs are listed [here](/guides/api/crm/associations/associations-v4#association-type-id-values), or you can retrieve the value for custom association types (i.e. labels) via the [associations API](/guides/api/crm/associations/associations-v4#retrieve-association-types). | Learn more about batch creating meetings by checking out the [reference documentation](/reference/api/crm/engagements/meetings#post-%2Fcrm%2Fv3%2Fobjects%2Fmeetings%2Fbatch%2Fcreate). ## Retrieve meetings You can retrieve meetings individually or in bulk. Learn more about batch retrieval by checking out the [reference documentation](/reference/api/crm/engagements/meetings#post-%2Fcrm%2Fv3%2Fobjects%2Fmeetings%2Fbatch%2Fread). To retrieve an individual meeting by its meeting ID, make a `GET` request to `/crm/v3/objects/meetings/{meetingId}`. You can also include the following parameters in the request URL: | Parameter | Description | | --- | --- | | `properties` | A comma separated list of the properties to be returned. | | `associations` | A comma separated list of objects to you want to retrieve associated record IDs from. | To request a list of all of meetings, make a `GET` request to `crm/v3/objects/meetings`. You can include the following parameters in the request URL: | Parameter | Description | | ------------ | -------------------------------------------------------- | | `limit` | The maximum number of results to display per page. | | `properties` | A comma separated list of the properties to be returned. | ## Update meetings You can update meetings individually or in batches. To update an individual meeting by its meeting ID, make a `PATCH` request to `/crm/v3/objects/meetings/{meetingId}`. In the request body, include the meeting properties that you want to update. For example, your request body might look similar to the following: ```json //Example PATCH request to https://api.hubspot.com/crm/v3/objects/meetings/{meetingId} { "properties": { "hs_timestamp": "2019-10-30T03:30:17.883Z", "hubspot_owner_id": "11349275740", "hs_meeting_title": "Intro meeting", "hs_meeting_body": "The first meeting to discuss options", "hs_internal_meeting_notes": "These are the meeting notes", "hs_meeting_external_url": "https://Zoom.com/0000", "hs_meeting_location": "Remote", "hs_meeting_start_time": "2021-03-23T01:02:44.872Z", "hs_meeting_end_time": "2021-03-23T01:52:44.872Z", "hs_meeting_outcome": "SCHEDULED" } }' ``` HubSpot will ignore values for read-only and non-existent properties. To clear a property value, pass an empty string for the property in the request body. Learn more about batch updating by checking out the [reference documentation](/reference/api/crm/engagements/meetings#post-%2Fcrm%2Fv3%2Fobjects%2Fmeetings%2Fbatch%2Fupdate). ### Associate existing meetings with records To associate a meeting with records, such as a contact and its associated companies, make a `PUT` request to `/crm/v3/objects/meetings/{meetingId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}`. The request URL contains the following fields: | Field | Description | | --- | --- | | `meetingId` | The ID of the meeting. | | `toObjectType` | The type of object that you want to associate the meeting with (e.g., contact or company). | | `toObjectId` | The ID of the record that you want to associate the meeting with. | | `associationTypeId` | The ID of the association type between the meeting and the other object type. You can retrieve this value through the [associations API](/reference/api/crm/associations/association-details). | For example, your request URL might look similar to the following: `https://api.hubspot.com/crm/v3/objects/meetings/17612479134/associations/contact/104901/200` ### Remove an association To remove an association between a meeting and a record, make a `DELETE` request to the same URL as above: `/crm/v3/objects/meetings/{meetingId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}` ## Pin a meeting on a record You can [pin a meeting](https://knowledge.hubspot.com/records/pin-an-activity-on-a-record) on a record so it remains on the top of the record's timeline. The meeting must already be associated with the record prior to pinning, and you an only pin one activity per record. To pin a meeting, include the meeting's `id` in the `hs_pinned_engagement_id` field when creating or updating a record via the object APIs. Learn more about using the [companies](/guides/api/crm/objects/companies#pin-an-activity-on-a-company-record), [contacts](/guides/api/crm/objects/contacts#pin-an-activity-on-a-contact-record), [deals](/guides/api/crm/objects/deals#pin-an-activity-on-a-deal-record), [tickets](/guides/api/crm/objects/tickets#pin-an-activity-on-a-ticket-record), and [custom objects](/guides/api/crm/objects/custom-objects) APIs. ## Delete meetings You can delete meetings individually or in batches, which will add the meeting to the recycling bin in HubSpot. You can later [restore the meeting from the record timeline.](https://knowledge.hubspot.com/records/restore-deleted-activity-in-a-record) To delete an individual meeting by its meeting ID, make a `DELETE` request to `/crm/v3/objects/meetings/{meetingId}`. Learn more about batch deleting by checking out the [reference documentation](/reference/api/crm/engagements/meetings#delete-%2Fcrm%2Fv3%2Fobjects%2Fmeetings%2F%7Bmeetingid%7D). # Engagements | Notes @ Contact records store information about individuals. The contacts endpoints allow you to manage this data and sync it between HubSpot and other systems. $ guides & api/crm/engagements --- # Notes You can log notes on CRM records to add information to the record timeline or associate an attachment with a record. For example, if you need to keep track of an offline conversation you had with a contact, you can add a note to their contact record with details and documents related to the conversation. Other users in the account will then be able to view and reference that note. You can manage notes either [in HubSpot](https://knowledge.hubspot.com/contacts/manually-log-a-call-email-or-meeting-on-a-record) or through the notes API. Below, learn the basic methods of managing notes through the API. You can review all available endpoints in the [reference documentation](/reference/api/crm/engagements/notes). ## Create a note To create a note, make a `POST` request to `/crm/v3/objects/notes`. In the request body, add note details in a **properties** object. You can also add an **associations** object to associate your new note with an existing record (e.g., contacts, companies). ### Properties In the properties object, you can include the following fields: | Field | Description | | --- | --- | | `hs_timestamp` | Required. This field marks the note's time of creation and determines where the note sits on the record timeline. You can use either a Unix timestamp in milliseconds or UTC format. | | `hs_note_body` | The note's text content, limited to 65,536 characters. | | `hubspot_owner_id` | The [ID of the owner](/guides/api/crm/owners) associated with the note. This field determines the user listed as the note creator on the record timeline in HubSpot. | | `hs_attachment_ids` | The IDs of the note's attachments. Multiple attachment IDs are separated by a semi-colon. | ### Associations To create and associate a note with existing records, include an associations object in your request. For example, to create a note and associate it with a company and deal, your request body might look similar to the following: ```json // Example POST request to https://api.hubspot.com/crm/v3/objects/notes { "properties": { "hs_timestamp": "2021-11-12T15:48:22Z", "hs_note_body": "Spoke with decision maker Carla. Attached the proposal and draft of contract.", "hubspot_owner_id": "14240720", "hs_attachment_ids": "24332474034;24332474044" }, "associations": [ { "to": { "id": 301 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 190 } ] }, { "to": { "id": 401 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 214 } ] } ] } ``` In the associations object, you should include the following: | Field | Description | | --- | --- | | `to` | The record you want to associate with the note, specified by its unique `id` value. | | `types` | The type of the association between the note and the record. Include the `associationCategory`and `associationTypeId`. Default association type IDs are listed [here](/guides/api/crm/associations/associations-v4#association-type-id-values), or you can retrieve the value for custom association types (i.e. labels) via the [associations API](/guides/api/crm/associations/associations-v4#retrieve-association-types). | Learn more about batch creating notes in the [reference documentation](/reference/api/crm/engagements/notes#post-%2Fcrm%2Fv3%2Fobjects%2Fnotes%2Fbatch%2Fcreate). ## Retrieve notes You can retrieve notes individually or in batches. To retrieve an individual note, make a `GET` request to `/crm/v3/objects/notes/{noteId}`. To request a list of all notes, make a `GET` request to `/crm/v3/objects/notes`. For both endpoints, you can include the following query parameters in the request URL: | Parameter | Description | | --- | --- | | `properties` | A comma separated list of the properties to be returned in the response. If the requested note doesn't have a value for a property, it will not appear in the response. | | `associations` | A comma separated list of object types to retrieve associated IDs for. Any specified associations that don't exist will not be returned in the response. Learn more about the [associations API.](/guides/api/crm/associations/associations-v4) | For example, to retrieve notes with their text content and any associated contact IDs, your request URL might look similar to the following: `https://api.hubapi.com/crm/v3/objects/notes?limit=10&properties=hs_note_body&associations=contact&archived=false`. Learn more about retrieving a batch of notes by internal ID or unique property value in the [reference documentation](/reference/api/crm/engagements/notes#post-%2Fcrm%2Fv3%2Fobjects%2Fnotes%2Fbatch%2Fread). ## Update notes You can update notes individually or in batches. To update an individual note by its note ID, make a `PATCH` request to `/crm/v3/objects/notes/{noteId}`. In the request body, include the note properties that you want to update: ```json // Example PATCH request to https://api.hubspot.com/crm/v3/objects/notes/{noteID} { "properties": { "hs_note_body": "Spoke with decision maker Carla.", "hs_attachment_ids": "24332474034;24332474044" } } ``` HubSpot will ignore values for read-only and non-existent properties. To clear a property value, pass an empty string for the property in the request body. Learn more about batch updating notes in the [reference documentation](/reference/api/crm/engagements/notes#post-%2Fcrm%2Fv3%2Fobjects%2Fnotes%2Fbatch%2Fupdate). ### Associate existing notes with records To associate a note with other CRM records, such as a contact, make a `PUT` request to `/crm/v3/objects/notes/{noteId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}`. The request URL contains the following fields: | Field | Description | | --- | --- | | `noteId` | The ID of the note. | | `toObjectType` | The type of object that you want to associate the note with (e.g., contact or company) | | `toObjectId` | The ID of the record that you want to associate the note with. | | `associationTypeId` | A unique identifier to indicate the association type between the note and the other object. The ID can be represented numerically or in snake case (e.g., `note_to_contact`). You can retrieve the value through the [associations API](/reference/api/crm/associations/association-details). | For example, your request URL might look similar to the following: `https://api.hubspot.com/crm/v3/objects/notes/17147287858/associations/contact/581751/202` ### Remove an association To remove an association between a note and a record, make a `DELETE` request to the same URL as above: `/crm/v3/objects/notes/{noteId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}` ## Pin a note on a record You can [pin a note](https://knowledge.hubspot.com/records/pin-an-activity-on-a-record) on a record so it remains on the top of the record's timeline. The note must already be associated with the record prior to pinning, and you an only pin one activity per record. To pin a note, include the note's `id` in the `hs_pinned_engagement_id` field when creating or updating a record via the object APIs. Learn more about using the [companies,](/guides/api/crm/objects/companies#pin-an-activity-on-a-company-record)[contacts](/guides/api/crm/objects/contacts#pin-an-activity-on-a-contact-record), [deals](/guides/api/crm/objects/deals#pin-an-activity-on-a-deal-record), [tickets](/guides/api/crm/objects/tickets#pin-an-activity-on-a-ticket-record), and [custom objects](/guides/api/crm/objects/custom-objects) APIs. ## Delete notes You can delete notes individually or in batches, which will add the note to the recycling bin in HubSpot. You can later [restore the note from the record timeline](https://knowledge.hubspot.com/crm-setup/restore-deleted-activity-in-a-record). To delete an individual note by its note ID, make a `DELETE` request to `/crm/v3/objects/notes/{noteId}`. Learn more about deleting notes in the [reference documentation](/reference/api/crm/engagements/notes#delete-%2Fcrm%2Fv3%2Fobjects%2Fnotes%2F%7Bnoteid%7D). # Engagements | Postal Mail @ Use the postal mail engagement API to log any postal mail you've sent to or received from contacts or companies on their CRM records. $ guides & api/crm/engagements --- # Postal Mail Use the postal mail engagement API to log and manage postal mail on CRM records. You can log the mail you've sent or received [in HubSpot](https://knowledge.hubspot.com/contacts/manually-log-a-call-email-or-meeting-on-a-record) or through the postal mail API. You can also retrieve, update, or delete existing postal mail engagements. Below, learn the basic methods of managing postal mail through the API. To view all available endpoints and their requirements, check out the [reference documentation](/reference/api/crm/engagements/postal-mail). ## Create a postal mail engagement To create a postal mail engagement, make a `POST` request to `/crm/v3/objects/postal_mail`. In the request body, add postal mail details in a **properties** object. You can also add an **associations** object to associate your new postal mail with an existing record (e.g., contacts, companies). ### Properties In the properties object, you can include the following fields: | Field | Description | | --- | --- | | `hs_timestamp` | The date that the postal mail was sent or received. | | `hs_postal_mail_body` | The body text of the postal mail engagement. | | `hubspot_owner_id` | The ID of the user that created the postal mail engagement. | | `hs_attachment_ids` | The IDs of any attachments to the postal mail engagement. Multiple attachment IDs are separated by a semi-colon. | ### Associations To create and associate a postal mail engagement with existing records, include an associations object in your request. For example, to create postal mail and associate it with two contacts, your request body might look similar to the following: ```json //Example request body { "properties": { "hs_timestamp": "2021-11-12", "hs_postal_mail_body": "Sent copy of contract to decision maker John", "hubspot_owner_id": "9274996", "hs_attachment_ids": "24332474034" }, "associations": [ { "to": { "id": 501 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 453 } ] }, { "to": { "id": 502 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 453 } ] } ] } ``` In the associations object, you should include the following: | Parameter | Description | | --- | --- | | `to` | The record you want to associate with the postal mail, specified by its unique `id` value. | | `types` | The type of the association between the postal mail and the record. Include the `associationCategory`and `associationTypeId`. Default association type IDs are listed [here](/guides/api/crm/associations/associations-v4#association-type-id-values), or you can retrieve the value for custom association types (i.e. labels) via the [associations API](/guides/api/crm/associations/associations-v4#retrieve-association-types). | ## Retrieve postal mail engagements You can retrieve postal mail engagements individually or in bulk. To retrieve an individual postal mail engagement, make a `GET` request to `/crm/v3/objects/postal_mail/{postalMail}`. You can include the following parameters in the request: | Parameter | Description | | --- | --- | | `properties` | A comma separated list of the properties to be returned. | | `associations` | A comma separated list of object types to retrieve associated IDs for. Any specified associations that don't exist will not be returned in the response. Learn more about the [associations API.](/guides/api/crm/associations/associations-v4) | To retrieve a list of the postal mail engagements in your account, make a `GET` request to `crm/v3/objects/postal_mail`. You can include the following parameters in the request: | Parameter | Description | | ------------ | -------------------------------------------------------- | | `limit` | The maximum number of results to display per page. | | `properties` | A comma separated list of the properties to be returned. | When you make a successful batch request, the response will include the ID of each postal mail engagement, which you can use to retrieve, update, and delete postal mail engagements. ## Update postal mail engagements You can update postal mail engagements individually or in batches. To update an individual engagement by its ID, make a `PATCH` request to `/crm/v3/objects/postal_mail/{postalMail}`. In the request body, include the properties that you want to update. For example, to update the body of the engagement, your request body might look similar to the following: ```json //Example request body { "properties": { "hs_postal_mail_body": "Sent copy of contract to decision maker John. Received a call in response." } } ``` HubSpot will ignore values for read-only and non-existent properties. To clear a property value, pass an empty string for the property in the request body. Learn more about batch updating by checking out the [reference documentation](/reference/api/crm/engagements/postal-mail#post-%2Fcrm%2Fv3%2Fobjects%2Fpostal_mail%2Fbatch%2Fupdate). ### Associate existing postal mail with records You can associate postal mail engagements with contact, company, deal, or ticket records. To associate postal mail with records, make a `PUT` request to `/crm/v3/objects/postal_mail/{postalMail}/associations/{toObjectType}/{toObjectId}/{associationTypeId}`. | Parameter | Description | | --- | --- | | `postalMail` | The unique ID of the postal mail engagement. | | `toObjectType` | The type of object that you want to associate the postal mail with (e.g., `contact` or `company`). | | `toObjectId` | The ID of the record that you want to associate the postal mail with. | | `associationTypeId` | A unique identifier to indicate the association type between the postal mail and the other object. The ID can be represented numerically or in snake case (e.g., `POSTAL_MAIL_TO_CONTACT`). You can retrieve the value through the [associations API](/reference/api/crm/associations/association-details). | For example, your request URL might look similar to the following: `https://api.hubspot.com/crm/v3/objects/postal_mail/25727582880/associations/contact/104901/POSTAL_MAIL_TO_CONTACT` ### Remove an association To remove an association between a postal mail engagement and a record, make a `DELETE` request to: `/crm/v3/objects/postal_mail/{postalMail}/associations/{toObjectType}/{toObjectId}/{associationTypeId}` ## Pin a postal mail engagement on a record You can [pin a postal mail engagement](https://knowledge.hubspot.com/records/pin-an-activity-on-a-record) on a record so it remains on the top of the record's timeline. The postal mail must already be associated with the record prior to pinning, and you an only pin one activity per record. To pin postal mail, include the postal mail's `id` in the `hs_pinned_engagement_id` field when creating or updating a record via the object APIs. Learn more about using the [companies,](/guides/api/crm/objects/companies#pin-an-activity-on-a-company-record)[contacts](/guides/api/crm/objects/contacts#pin-an-activity-on-a-contact-record), [deals](/guides/api/crm/objects/deals#pin-an-activity-on-a-deal-record), [tickets](/guides/api/crm/objects/tickets#pin-an-activity-on-a-ticket-record), and [custom objects](/guides/api/crm/objects/custom-objects) APIs. ## Delete postal mail engagements You can delete a postal mail engagement individually or in bulk, which will add the engagement to the recycling bin in HubSpot. You can later [restore the engagement from the record timeline](https://knowledge.hubspot.com/crm-setup/restore-deleted-activity-in-a-record). To delete an individual postal mail engagement by its ID, make a `DELETE` request to `/crm/v3/objects/postal_mail/{postalMail}`. Learn more about deleting postal mail engagements in the [reference documentation](/reference/api/crm/engagements/postal-mail#delete-%2Fcrm%2Fv3%2Fobjects%2Fpostal_mail%2F%7Bpostalmailid%7D). # Engagements | Tasks @ Use the tasks engagement API to log and manage tasks on CRM records. $ guides & api/crm/engagements --- # Tasks Use the tasks API to create and manage tasks. You can create tasks [in HubSpot](https://knowledge.hubspot.com/contacts/manually-log-a-call-email-or-meeting-on-a-record) or via the tasks API. Below, learn the basic methods of managing tasks through the API. To view all available endpoints and their requirements, check out the [reference documentation](/reference/api/crm/engagements/tasks). ## Create a task To create a task, make a `POST` request to `/crm/v3/objects/tasks`. In the request body, add task details in a **properties** object. You can also add an **associations** object to associate your new task with an existing record (e.g., contacts, companies). ### Properties In the properties object, you can include the following fields: | Field | Description | | --- | --- | | `hs_timestamp` | Required. This field marks the task's due date. You can use either a Unix timestamp in milliseconds or UTC format. | | `hs_task_body` | The task [notes.](https://knowledge.hubspot.com/tasks/create-tasks#task-details) | | `hubspot_owner_id` | The [owner ID](/guides/api/crm/owners) of the user assigned to the task. | | `hs_task_subject` | The title of the task. | | `hs_task_status` | The status of the task, either `COMPLETED` or `NOT_STARTED`. | | `hs_task_priority` | The priority of the task. Values include `LOW`, `MEDIUM`, or `HIGH`. | | `hs_task_type` | The type of task. Values include `EMAIL`, `CALL`, or `TODO`. | | `hs_task_reminders` | The timestamp for when to send a reminder for the due date of the task. You must use Unix timestamp in milliseconds. | ### Associations To create and associate a task with existing records, include an associations object in your request. For example, to create a task and associate it with two contacts, your request body might look similar to the following: ```json // Example request body { "properties": { "hs_timestamp": "2019-10-30T03:30:17.883Z", "hs_task_body": "Send Proposal", "hubspot_owner_id": "64492917", "hs_task_subject": "Follow-up for Brian Buyer", "hs_task_status": "WAITING", "hs_task_priority": "HIGH", "hs_task_type": "CALL" }, "associations": [ { "to": { "id": 101 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 204 } ] }, { "to": { "id": 102 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 204 } ] } ] } ``` In the associations object, you should include the following: | Field | Description | | --- | --- | | `to` | The record you want to associate with the task, specified by its unique `id` value. | | `types` | The type of the association between the task and the record. Include the `associationCategory`and `associationTypeId`. Default association type IDs are listed [here](/guides/api/crm/associations/associations-v4#association-type-id-values), or you can retrieve the value for custom association types (i.e. labels) via the [associations API](/guides/api/crm/associations/associations-v4#retrieve-association-types). | Learn more about batch creating tasks by checking out the [reference documentation](/reference/api/crm/engagements/tasks#post-%2Fcrm%2Fv3%2Fobjects%2Ftasks%2Fbatch%2Fcreate). ## Retrieve tasks You can retrieve tasks individually or in bulk. Learn more about batch retrieval by checking out the [reference documentation](/reference/api/crm/engagements/tasks#post-%2Fcrm%2Fv3%2Fobjects%2Ftasks%2Fbatch%2Fread). To retrieve an individual task by its task ID, make a `GET` request to `/crm/v3/objects/tasks/{taskId}`. You can also include the following parameters in the request URL: | Parameter | Description | | --- | --- | | `properties` | A comma separated list of the properties to be returned. | | `associations` | A comma separated list of object types to retrieve associated IDs for. Any specified associations that don't exist will not be returned in the response. Learn more about the [associations API.](/guides/api/crm/associations/associations-v4) | To request a list of all of tasks, make a `GET` request to `crm/v3/objects/tasks`. You can include the following parameters in the request URL: | Parameter | Description | | ------------ | -------------------------------------------------------- | | `limit` | The maximum number of results to display per page. | | `properties` | A comma separated list of the properties to be returned. | ## Update tasks You can update tasks individually or in batches. To update an individual task by its task ID, make a `PATCH` request to `/crm/v3/objects/tasks/{taskId}`. In the request body, include the task properties that you want to update. For example, your request body might look similar to the following: ```json //Example PATCH request to https://api.hubspot.com/crm/v3/objects/tasks/{taskId} { "properties": { "hs_timestamp": "2019-10-30T03:30:17.883Z", "hs_task_body": "Send Proposal", "hubspot_owner_id": "64492917", "hs_task_subject": "Close deal", "hs_task_status": "COMPLETED", "hs_task_priority": "HIGH" } } ``` HubSpot will ignore values for read-only and non-existent properties. To clear a property value, pass an empty string for the property in the request body. Learn more about batch updating by checking out the [reference documentation](/reference/api/crm/engagements/tasks#post-%2Fcrm%2Fv3%2Fobjects%2Ftasks%2Fbatch%2Fupdate). ### Associate existing tasks with records To associate an existing task with records (e.g., contacts, deals, etc.), make a `PUT` request to `/crm/v3/objects/tasks/{taskId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}`. The request URL should contains the following fields: | Field | Description | | --- | --- | | `taskId` | The ID of the task. | | `toObjectType` | The type of object that you want to associate the task with (e.g., contact or company) | | `toObjectId` | The ID of the record that you want to associate the task with. | | `associationTypeId` | A unique identifier to indicate the association type between the task and the other object. The ID can be represented numerically or in snake case (e.g., `task_to_contact`). You can retrieve the value through the [associations API](/reference/api/crm/associations/association-details). | For example, your request URL might look similar to the following: `https://api.hubspot.com/crm/v3/objects/tasks/17687016786/associations/contacts/104901/204` ### Remove an association To remove an association between a task and a record, make a `DELETE` request to the same URL as above: `/crm/v3/objects/tasks/{taskId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}` ## Pin a task on a record You can [pin a task](https://knowledge.hubspot.com/records/pin-an-activity-on-a-record) on a record so it remains on the top of the record's timeline. The task must already be associated with the record prior to pinning, and you an only pin one activity per record. To pin a task, include the task's `id` in the `hs_pinned_engagement_id` field when creating or updating a record via the object APIs. Learn more about using the [companies,](/guides/api/crm/objects/companies#pin-an-activity-on-a-company-record)[contacts](/guides/api/crm/objects/contacts#pin-an-activity-on-a-contact-record), [deals](/guides/api/crm/objects/deals#pin-an-activity-on-a-deal-record), [tickets](/guides/api/crm/objects/tickets#pin-an-activity-on-a-ticket-record), and [custom objects](/guides/api/crm/objects/custom-objects) APIs. ## Delete tasks You can delete tasks individually or in batches, which will add the task to the recycling bin in HubSpot. You can later [restore the task from the record timeline](https://knowledge.hubspot.com/crm-setup/restore-deleted-activity-in-a-record). To delete an individual task by its task ID, make a `DELETE` request to `/crm/v3/objects/tasks/{taskId}`. Learn more about deleting tasks by checking out the [reference documentation](/reference/api/crm/engagements/tasks#delete-%2Fcrm%2Fv3%2Fobjects%2Ftasks%2F%7Btaskid%7D). # Exports @ Learn how to use the Exports API to export records in views or lists from your HubSpot account. $ guides & api/crm --- # Exports Use the exports API to export records and property data from your HubSpot account, retrieve a URL to download an export file, or see the status of an export. Within HubSpot, you can also [export records](https://knowledge.hubspot.com/import-and-export/export-records) or [view a log of past exports in your account.](https://knowledge.hubspot.com/import-and-export/view-a-log-of-your-users-exports-in-your-account) ## Start an export To start an export, make a `POST` request to `/crm/v3/exports/export/async`. Your request body should specify information such as the file format, the object and properties you want to export, and the type of export you're completing (e.g., exporting an object view or a list). You can also filter the property data to be exported based on specific operators. For both view and list exports, you can include the following fields in your request: | Parameter | Description | | --- | --- | | `exportType` | The type of export, either `VIEW` (exports a view from an object index page) or `LIST` (exports a list). | | `format` | The file format. Options include `XLSX`, `CSV`, or `XLS`. | | `exportName` | The name of the export. | | `language` | The language of the export file. Options include `DE`, `EN`, `ES`, `FI`, `FR`, `IT`, `JA`, `NL`, `PL`, `PT`, or `SV`. Learn more about [supported languages.](https://knowledge.hubspot.com/help-and-resources/hubspot-language-offerings) | | `objectType` | The name or ID of the object you're exporting. For standard objects, you can use the object's name (e.g., `CONTACT`), but for custom objects, you must use the `objectTypeId` value. You can retrieve this value by making a `GET` request to `/crm/v3/schemas`. | | `associatedObjectType` | The names or IDs of associated objects to include in the export. You can export up to four associated objects per request. For associated objects, the export will contain the associated record IDs of that object. If you export only one associated object or set the `includePrimaryDisplayPropertyForAssociatedObjects` field to `true`, the records' primary display property values will also be exported (e.g., `name` for companies). | | `objectProperties` | A list of the properties you want included in your export. By default, property names and values are shown as the human-readable labels, but you can request internal names and values using the `exportInternalValuesOptions` parameter. | | `includePrimaryDisplayPropertyForAssociatedObjects` | Include this field with the value `true` to export the primary display property values for all associated records (e.g., `name` for companies). If you're only exporting one associated object (indicated by the `associatedObjectType` field), this is set to `true` by default. If you're exporting more than one associated object, the value is set to `false` by default. | | `includeLabeledAssociations` | Include this field with the value `true` to export [association labels](https://developers.hubspot.com/docs/guides/api/crm/associations/associations-v4#create-and-manage-association-types) to describe the relationship between associated records. | | `exportInternalValuesOptions` | Include this array to export the internal values for property names and/or property values. In the array, include `NAMES` to export internal names of properties (shown as column headers) and/or `VALUES` to export internal property values (e.g., numerical values for custom pipeline stages or timestamps instead of human-readable dates). | ### Export a view If you're exporting an [index page view](https://knowledge.hubspot.com/records/view-and-filter-records), your `exportType` value should be `VIEW`, and you can include the following field to filter and sort the records you're exporting: | Parameter | Description | | --- | --- | | `publicCrmSearchRequest` | Indicates which data should be exported based on certain property values and search queries. You can include the following within the object:<br />`filters`: the properties and property values to filter records by.<br />`sorts`: the sort order of a property's values, either ascending, `ASC`, or descending, `DES`.<br />`query`: a string to search the records' values for. | For example, to export a view of contacts and associated company records, filtered by the `email` property and with the internal values of property names and values, your request would look like the following: ```json ///Example request body { "exportType": "VIEW", "exportName": "All contacts", "format": "xlsx", "language": "DE", "objectType": "CONTACT", "exportInternalValuesOptions": ["NAMES", "VALUES"], "objectProperties": ["email", "firstname", "lastname"], "associatedObjectType": "COMPANY", "publicCrmSearchRequest": { "filters": [ { "value": "hello@test.com", "propertyName": "email", "operator": "EQ" } ], "query": "hello", "sorts": [ { "propertyName": "email", "order": "ASC" } ] } } ``` ### Export a list If you're exporting a [list](https://knowledge.hubspot.com/lists/create-active-or-static-lists), your `exportType` value should be `LIST`, but you also need to specify the list you're exporting with the following field: | Parameter | Description | | --- | --- | | `listId` | The [ILS List ID](https://knowledge.hubspot.com/lists/lists-faq#ils-list) of the list to export. You can find the ILS List ID value via the list details in HubSpot. Navigate to **Contacts** > **Lists**, hover over the **list** in the table, then click **Details**. In the right panel, click **Copy** **List ID** next to the ILS List ID value. Contact lists have two different ID values, but you <u>must</u> use the ILS List ID value in your request. | For example, to export a list with the contacts' emails, your request may look like the following: ```json ///Example request body { "exportType": "LIST", "listId": 1234567, "exportName": "Marketing email contacts", "format": "xlsx", "language": "EN", "objectType": "CONTACT", "objectProperties": ["email"] } ``` ## Retrieve exports When you successfully complete an export, the export's `id` will be returned in the response. To retrieve an export from your HubSpot account, make a `GET` request to `/crm/v3/exports/export/async/tasks/{exportId}/status`. When retrieving exports, the `status` of the export will also be returned. Possible statuses include `COMPLETE`, `PENDING`, `PROCESSING`, or `CANCELED`. For exports with a `COMPLETE` status, a URL is returned that you can use to download the exported file. The download URL will expire five minutes after the completed request. Once expired, you can perform another `GET` request to generate a new unique URL. Prior to expiration, an export's download URL can be accessed without any additional authorization. To protect your data, proceed with caution when sharing a URL or integrating with HubSpot via this API. ## Limits The following limits apply: - You can complete up to thirty exports within a rolling 24 hour window, and one export at a time. Additional exports will be queued until the previous export is completed. - An exported CSV file will be automatically zipped if the resulting file is greater than 2MB. # CRM API | Calling extensions SDK @ Overview of the calling extensions SDK $ guides & api/crm/extensions --- # Calling Extensions SDK The [Calling Extensions SDK](https://github.com/HubSpot/calling-extensions-sdk) enables apps to provide a custom calling option to HubSpot users directly from a CRM record, similar to [HubSpot's provided calling tool](https://knowledge.hubspot.com/calling/use-the-calling-tool). When a user makes a call using your app, HubSpot will create an engagement on the activity timeline of the CRM record, then exchange data with your app to fill in the details. A calling extension consists of three main components: 1. The [Calling Extensions SDK](https://github.com/HubSpot/calling-extensions-sdk), a JavaScript SDK that enables communication between your app and HubSpot. 2. The **calling settings endpoints**, which are used to set the calling settings for your app. Each HubSpot account that connects to your app will use these settings. 3. The **calling iframe**, which is where your app appears to HubSpot users and is configured using the calling settings endpoints. If you don't have an app, you can [create one from your HubSpot developer account](/getting-started/account-types#app-developer-accounts). If you don't already have a HubSpot developer account, sign up for one [here](https://app.hubspot.com/signup-hubspot/developers). The documentation below covers outbound calling via the SDK. Learn more about using the SDK to [receive incoming calls (BETA)](/guides/apps/extensions/calling-extensions/receive-incoming-calls). ## Run the demo calling app You have the option to test the Calling Extensions SDK on two different demo apps: - The [demo-minimal-js](https://github.com/HubSpot/calling-extensions-sdk/tree/master/demos/demo-minimal-js) features a minimal implementation of the SDK using JavaScript, HTML, and CSS. View how the SDK is instantiated in [index.js](https://github.com/HubSpot/calling-extensions-sdk/blob/project-demo-v1/demos/demo-minimal-js/index.js). - The [demo-react-ts](https://github.com/HubSpot/calling-extensions-sdk/tree/master/demos/demo-react-ts) features a real-life implementation of the SDK using React, TypeScript, and Styled Components to act as a blueprint for your app. View how the SDK is instantiated in [useCti.ts](https://github.com/HubSpot/calling-extensions-sdk/blob/master/demos/demo-react-ts/src/hooks/useCti.ts). These demo apps aren't fully functional calling apps and use mock data to provide a more realistic experience. ### Install the demo calling app You can run the demo apps with or without installation. To install the demo on your local environment: 1. Install [Node.js](https://nodejs.org/en) on your environment. 2. Clone, fork, or [download the ZIP](https://codeload.github.com/HubSpot/calling-extensions-sdk/zip/refs/heads/master) of this repository. 3. Open your terminal, and navigate to the root directory of the project. 4. Run one of the following commands: - For the `demo-minimal-js`: ```shell cd demos/demo-minimal-js && npm i && npm start ``` - For the `demo-react-ts`: ```shell cd demos/demo-react-ts && npm i && npm start ``` These will switch to the desired demo directory, install the [Node.js](https://nodejs.org/en/) dependencies required for the project using the [npm CLI](https://docs.npmjs.com/cli/v9/), and start the app. The `npm start` command will automatically open a new tab in your browser at [https://localhost:9025/](https://localhost:9025/), and you may need to bypass a "Your connection is not secure" warning in order to access the application. ### Launch the demo calling app from HubSpot 1. Navigate to your records:. - **Contacts:** In your HubSpot account, navigate to **Contacts > Contacts**. - **Company:** In your HubSpot account, navigate to **Contacts > Companies**. 2. Open your browser's developer console, and run the following command: - If you've completed the installation steps, for the `demo-minimal-js` or the `demo-react-ts`: ```js localStorage.setItem('LocalSettings:Calling:installDemoWidget', 'local'); ``` - If you've skipped the installation steps: - For the `demo-minimal-js`: ```js localStorage.setItem('LocalSettings:Calling:installDemoWidget', 'app:js'); ``` - For the `demo-react-ts`: ```js localStorage.setItem('LocalSettings:Calling:installDemoWidget', 'app'); ``` 1. Refresh the page, and click the **Call** icon in the left sidebar. Click the **Call from** dropdown menu, and select the **name** of the demo app from step 2 (e.g. Demo App Local, Demo App JS, Demo App React). 2. Click **Call** to see how the demo app integrates with HubSpot via the Calling Extensions SDK. You can also see the events logged to your browser's developer console. ## Install the Calling Extensions SDK on your calling app To add the Calling Extensions SDK as a [Node.js](https://nodejs.org/en/) dependency to your calling app: - For npm, run: ```shell npm i --save @hubspot/calling-extensions-sdk ``` - For yarn, run: ```shell yarn add @hubspot/calling-extensions-sdk ``` ## Using the Calling Extensions SDK The Calling Extensions SDK exposes a simple API for HubSpot and a calling app to exchange messages. The messages are sent through methods exposed by the SDK and received through `eventHandlers`. You can find a full list of the available events in the [Events section](#events). The following is a description of the events: 1. **Dial number:** HubSpot sends the dial number event. 2. **Outbound call started:** App notifies HubSpot when the call is started. 3. **Create engagement:** HubSpot creates [a call engagement](/guides/api/crm/engagements/calls) with minimal information if requested by the app. 4. **Engagement created:** HubSpot created an engagement. 5. **EngagementId sent to App:** HubSpot sends the `engagementId` to the app. 6. **Call ended:** App notifies when the call is ended. 7. **Call completed:** App notifies when the user is done with the app user experience. 8. **Update engagement:** App fetches the engagement by the `engagementId`, then merges and updates the engagement with additional call details. Learn more about [updating a call engagement via the API](/guides/api/crm/engagements/calls#update-calls)or [via the SDK](/guides/api/crm/engagements/calls). To begin, create an instance of the `CallingExtensions` object. You can define the behavior of your extension by providing an option's object when you create your extensions instance. This option's object provides an `eventHandlers` field where you can specify the behavior of your extension. The following code block illustrates the available options and event handlers you can define: ```js import CallingExtensions from "@hubspot/calling-extensions-sdk"; const options = { /** @property {boolean} debugMode - Whether to log various inbound/outbound debug messages to the console. If false, console.debug will be used instead of console.log */ debugMode: boolean, // eventHandlers handle inbound messages eventHandlers: { onReady: () => { /* HubSpot is ready to receive messages. */ }, onDialNumber: event => { /* HubSpot sends a dial number from the contact */ }, onCreateEngagementSucceeded: event => { /* HubSpot has created an engagement for this call. */ } onEngagementCreatedFailed: event => { /* HubSpot has failed to create an engagement for this call. */ } onUpdateEngagementSucceeded: event => { /* HubSpot has updated an engagement for this call. */ }, onUpdateEngagementFailed: event => { /* HubSpot has failed to update an engagement for this call. */ } onVisibilityChanged: event => { /* Call widget's visibility is changed. */ } } }; const extensions = new CallingExtensions(options); ``` ## Test your app In order to launch the calling extensions iFrame for end users, HubSpot requires the following iFrame parameters. ```json { name: string /* The name of your calling app to display to users. */, url: string /* The URL of your calling app, built with the Calling Extensions SDK */, width: number /* The iFrame's width */, height: number /* The iFrame's height */, isReady: boolean /* Whether the widget is ready for production (defaults to true) */, supportsCustomObjects : true /* Whether calls can be placed from a custom object */ } ``` ### Using the calling settings endpoint Using your API tool (e.g. Postman), send the following payload to HubSpot's settings API. Ensure you get the APP_ID of your calling app and your app [DEVELOPER_ACCOUNT_API_KEY](/guides/apps/overview#authentication). The `isReady` flag indicates whether the app is ready for production. This flag should be set to `false` during testing. ```shell # Example payload to add the call widget app settings curl --request POST \ --url 'https://api.hubapi.com/crm/v3/extensions/calling/APP_ID/settings?hapikey=DEVELOPER_ACCOUNT_API_KEY' \ --header 'accept: application/json' \ --header 'content-type: application/json' \ --data '{"name":"demo widget","url":"https://mywidget.com/widget","height":600,"width":400,"isReady":false}' # Note that this endpoint also supports PATCH, GET and DELETE ``` ### Override your extension settings using localStorage You can override any of your extension settings for testing purposes. Open your browser developer console from a HubSpot tab, edit the settings below, and run the command: ```js const myExtensionSettings = { isReady: true, name: 'My app name', url: 'My local/qa/prod URL', }; localStorage.setItem( 'LocalSettings:Calling:CallingExtensions', JSON.stringify(myExtensionSettings) ); ``` ## Get your app ready for production Once you have set your app settings using the [calling settings endpoint](/guides/api/crm/extensions/calling-sdk#using-the-calling-settings-endpoint), use the PATCH endpoint to change `isReady` to true. ```shell # Example payload to add the call widget app settings curl --request PATCH \ --url 'https://api.hubapi.com/crm/v3/extensions/calling/APP_ID/settings?hapikey=DEVELOPER_ACCOUNT_API_KEY' \ --header 'accept: application/json' \ --header 'content-type: application/json' \ --data '{"isReady":true}' ``` ## Publish your calling app to the HubSpot marketplace Once your app is set up, users can install it into their account using the app's install URL. If you want to list it publicly for other HubSpot users to find, you can also choose to [list it on the HubSpot App Marketplace](/guides/apps/marketplace/listing-your-app). This is not necessary if the app is for internal use only. ## Events Available calling events: - Send messages to HubSpot[](#demo-minimal-js)[](#yarn) - [`initialized`](#initialized) - [`userAvailable`](#useravailable) - [`userUnavailable`](#userunavailable) - [`userLoggedIn`](#userloggedin) - [`userLoggedOut`](#userloggedout) - [`incomingCall`](#incomingcall) - [`outgoingCall`](#outgoingcall) - [`callAnswered`](#callanswered) - [`callEnded`](#callended) - [`callCompleted`](#callcompleted) - [`publishToChannel`](#publishtochannel) - [`navigateToRecord`](#navigatetorecord) - [`sendError`](#senderror) - [`resizeWidget`](#resizewidget) - Receive messages from HubSpot: - [`onReady`](#onready) - [`onDialNumber`](#ondialnumber) - [`onEngagementCreated`](#onengagementcreated) (deprecated) - [`onCreateEngagementSucceeded`](#oncreateengagementsucceeded) - [`onCreateEngagementFailed`](#oncreateengagementfailed) - [`onNavigateToRecordFailed`](#onnavigatetorecordfailed) - [`onPublishToChannelSucceeded`](#onpublishtochannelsucceeded) - [`onPublishToChannelFailed`](#onpublishtochannelfailed) - [`onCallerIdMatchFailed`](#oncalleridmatchfailed) - [`onCallerIdMatchSucceeded`](#oncalleridmatchsucceeded) - [`onVisibilityChanged`](#onvisibilitychanged) - [`defaultEventHandler`](#defaulteventhandler) ### Sending messages to HubSpot The `extensions` object provides the following event handlers that you can invoke to send messages to HubSpot or to specify other associated behavior. See examples below. #### initialized: Required Send a message indicating that the soft phone is ready for interaction. ```js const payload = { isLoggedIn: boolean, engagementId: number, isAvailable: boolean, }; extensions.initialized(payload); ``` | Property | Type | Description | | -------------- | ------- | ------------------------------------- | | `isLoggedIn` | Boolean | Checks if a user is logged in or not. | | `engagementId` | Number | A HubSpot created engagement ID. | | `isAvailable` | Number | Checks if a user is available or not. | #### userAvailable Sends a message indicating that the user is available. ```js extensions.userAvailable(); ``` #### userUnavailable Sends a message indicating that the user is unavailable. ```js extensions.userUnavailable(); ``` #### userLoggedIn Sends a message indicating that the user has logged in. ```js // This message is only needed when user isn't logged in when initialized extensions.userLoggedIn(); ``` #### userLoggedOut Sends a message indicating that the user has logged out. ```js extensions.userLoggedOut(); ``` #### incomingCall Sends a message to notify HubSpot that an incoming call has started. ```js const callInfo = { externalCallId: string, callStartTime: number, createEngagement: boolean, fromNumber: string, tonumber: string, }; extensions.incomingCall(callInfo); ``` | Property | Type | Description | | --- | --- | --- | | `externalCallId` | String | A calling app created call ID. Used to enable [calling in help desk](/guides/api/crm/extensions/third-party-calling). | | `callStartTime` | Number | Start time of call, in milliseconds. | | `createEngagement` | Boolean | Whether HubSpot should create an engagement for this call or not. When true, HubSpot will respond with [onCreateEngagementSucceeded](#oncreateengagementsucceeded) or [onCreateEngagementFailed](#oncreateengagementfailed). | | `fromNumber` | String | The caller's number. Required parameter. | | `toNumber` | String | The recipients phone number. | #### outgoingCall Sends a message to notify HubSpot that an outgoing call has started. ```js const callInfo = { phoneNumber: string /** @deprecated Use toNumber instead **/, callStartTime: number, createEngagement: true, toNumber: string, fromNumber: string, dialingContext: onDialEventPayload, }; extensions.outgoingCall(callInfo); ``` | Property | Type | Description | | --- | --- | --- | | `callStartTime` | Number | Start time of call, in milliseconds. | | `createEngagement` | Boolean | Whether HubSpot should create an engagement for this call or not. When true, HubSpot will respond with [onCreateEngagementSucceeded](#oncreateengagementsucceeded) or [onCreateEngagementFailed](#oncreateengagementfailed). | | `toNumber` | String | The recipients phone number. | | `fromNumber` | String | The caller's number. Required parameter. | | `dialingContext` | Object | The dialing context is used for ticket and/or engagement creation when applicable. The object includes all properties of the [`onDialNumber`](#ondialnumber) payload. This property helps ensure data consistency by passing back the full dialing context between HubSpot and your third party app. | #### callAnswered Sends a message to notify HubSpot that an outgoing call is being answered. ```js const payload = { externalCallId: string, }; extensions.callAnswered(); ``` | Property | Type | Description | | --- | --- | --- | | `externalCallId` | String | A calling app created call ID. Used to enable [calling in help desk](/guides/api/crm/extensions/third-party-calling). | #### callEnded Sends a message to notify HubSpot that the call has ended. ```js // After receiving the call ended event, the user can navigate away, can close the call widget. extensions.callEnded({ externalCallId: string, engagementId: number, callEndStatus: EndStatus, }); ``` | Property | Type | Description | | --- | --- | --- | | `externalCallId` | String | A calling app created call ID. Used to enable [calling in help desk](/guides/api/crm/extensions/third-party-calling). | | `engagementId` | Number | A HubSpot created engagement ID. | | `callEndStatus` | Enumeration | The status of the call when ended. Statuses available:<ul><li>`COMPLETED`</li><li>`FAILED`</li><li>`CANCELED`</li><li>`BUSY`</li><li>`NO_ANSWER`</li><li>`REJECTED`</li><li>`MISSED`</li></ul> | #### callCompleted Sends a message to notify HubSpot that the call has completed. Engagement properties are [owned by HubSpot](/guides/api/crm/engagements/calls), and no longer need to be created or updated manually (see highlighted). The `hideWidget` property will be ignored when the user is in a task queue with the `Call` task type. ```js // After receiving the call completed event, HubSpot will // 1) insert the engagement into the timeline // 2) set the default associations on the engagement // 3) closes the widget unless `hideWidget` is set to false. // 4) update the engagement with any engagement properties const data = { engagementId: number, hideWidget: boolean, engagementProperties: { [key: string]: string }, externalCallId: string, } extensions.callCompleted(data); ``` | Property | Type | Description | | --- | --- | --- | | `engagementId` | Number | A HubSpot created engagement ID. | | `hideWidget` | Boolean | Whether the widget should be hidden when the call is ended. Optional parameter. Defaults to true. | | `engagementProperties` | String | Opt in to hs owned engagements by [adding properties](/guides/api/crm/engagements/calls#properties). Causes HubSpot to respond with [onUpdateEngagementSucceeded](#onUpdateEngagementSucceeded) or [onUpdateEngagementFailed](#onUpdateEngagementFailed). | | `externalCallId` | String | A calling app created call ID. Used to enable [calling in help desk](/guides/api/crm/extensions/third-party-calling). | #### publishToChannel Publishes the call to a connected channel. This isn't needed for HubSpot-owned engagements. ```js const payload = { externalCallId, engagementId, }; extensions.publishToChannel(payload); ``` | Property | Type | Description | | --- | --- | --- | | `externalCallId` | String | A calling app created call ID. Used to enable [calling in help desk](/guides/api/crm/extensions/third-party-calling). | | `engagementId` | Number | A HubSpot created engagement ID. | #### navigateToRecord This event is called when navigating to a record. ```js onNavigateToRecord { const { engagementId: number, objectCoordinates: object coordinates } = data; ... } ``` | Property | Type | Description | | --- | --- | --- | | `engagementId` | Number | A HubSpot created engagement ID. | | `objectCoordinates` | Object coordinates | References a `portalId`, `objectId`, and `objectTypeID`. | #### sendError Sends a message to notify HubSpot that the calling app has encountered an error. ```js // After receiving the sendError event, HubSpot will display an alert popup to the user with the error message provided. const data = { message: string, }; extensions.sendError(data); ``` | Property | Type | Description | | --------- | ------ | ------------------------------------------------- | | `message` | String | Error message to be displayed in the alert popup. | #### resizeWidget Sends a message to notify HubSpot that the calling app needs to be resized. ```js // After receiving the resizeWidget event, HubSpot will use the provided height and width to resize the call widget. const data = { height: number, width: number, }; extensions.resizeWidget(data); ``` | Property | Type | Description | | -------- | ------ | ---------------------------------- | | `height` | Number | Desired height of the call widget. | | `width` | Number | Desired width of the call widget. | ### Receive messages from HubSpot The `extensions` object provides the following event handlers that you can invoke when receiving messages in HubSpot or to specify other associated behavior. See examples below. #### onReady Message indicating that HubSpot is ready to receive messages. ```js // Example snippet for handling onReady event onReady() { extensions.initialized(payload); } ``` | Property | Type | Description | | --- | --- | --- | | `engagementId` | Number | A HubSpot created engagement ID. | | `iframeLocation` | Enum | `widget`: drag and drop widget shown in record pages when calling app doesn't [support inbound inbound calling](/guides/apps/extensions/calling-extensions/receive-incoming-calls#9.-notify-users).<br />`remote`: frame in the navigation bar when calling app supports inbound calling.<br />`window`: iframe in the calling window when calling app supports inbound calling. | | `ownerId` | String or Number | The ID of the logged in user in HubSpot. | | `PortalId` | Number | The ID of the HubSpot account. | | `userId` | Number | The ID of the HubSpot user. | #### onDialNumber This event will trigger when a user in HubSpot triggers an outbound call. The `onDialNumber` event will provide a payload that includes all the pertinent fields associated with the call. These fields are detailed in the table below: ```js onDialNumber(data) { const { phoneNumber: string, ownerId: number, subjectId: number, objectId: number, objectType: CONTACT | COMPANY, portalId: number, countryCode: string, calleeInfo { calleeId: number, calleeObjectTypeId: string, }, startTimestamp: number, toPhoneNumberSrc: string, } = data; ... } ``` | Property | Type | Description | | --- | --- | --- | | `phoneNumber` | String | The phone number of the person that the HubSpot user called. | | `ownerId` | String or Number | The ID of the logged in user (in HubSpot). | | `subjectId` | Number | The ID of the subject. | | `objectId` | Number | Object type of the phone number. | | `objectType` | String | The object type associated with the phone number that was dialed (e.g., the phone number of the contact or company). The possible values here are `"CONTACT"` or `"COMPANY"`. | | `portalId` | Number | The ID of the HubSpot portal. | | `countryCode` | String | The country code of the phone number. | | `calleeInfo` | Array | Information about the callee. Should include the following:<ul><li>`calleeID: number`</li><li>`calleeObjectTypeId: string`</li></ul> | | `startTimestamp` | Number | Timestamp of the start of the call. | | `toPhoneNumberSrc` | String | The name of the phone numbers' [property in HubSpot](https://knowledge.hubspot.com/properties/create-and-edit-properties). The property can be a standard property value or a custom property. For example, a contact may have three contact phone numbers, one may be labeled _Office_, another may be labeled _Personal_ and the other may be labeled _Mobile_. | #### onEngagementCreated Deprecated. Use [onCreateEngagementSucceeded](#oncreateengagementsucceeded) instead. ```js /** @deprecated Use onCreateEngagementSucceeded instead **/ onEngagementCreated(data) { const { engagementId: number, } = data; ... } ``` | Property | Type | Description | | -------------- | ------ | -------------------------------- | | `engagementId` | Number | A HubSpot created engagement ID. | #### onCreateEngagementSucceeded HubSpot sends a message to notify the calling app partner that the engagement update succeeds. ```js onCreateEngagementSucceeded: (event) => {}; ``` #### onCreateEngagementFailed HubSpot sends a message to notify the calling app partner that the engagement creation failed. ```js onCreateEngagementFailed: (event) => {}; ``` #### onNavigateToRecordFailed This event is called when navigating to a record fails. ```js onNavigateToRecordFailed { const { engagementId: number, objectCoordinates: object coordinates } = data; ... } ``` | Property | Type | Description | | --- | --- | --- | | `engagementId` | Number | A HubSpot created engagement ID. | | `objectCoordinates` | Object coordinates | References a `portalId`, `objectId`, and `objectTypeID`. | #### onPublishToChannelSucceeded This event is called when publishing to a channel succeeds. ```js onPublishToChannelSucceeded{ const { engagementId: number, externalCallId: string } = data; ... } ``` | Property | Type | Description | | --- | --- | --- | | `engagementId` | Number | A HubSpot created engagement ID. | | `externalCallId` | String | A calling app created call ID. Used to enable [calling in help desk](/guides/api/crm/extensions/third-party-calling). | #### onPublishToChannelFailed This event is called when publishing to a channel fails. ```js onPublishToChannelFailed{ const { engagementId: number, externalCallId: string } = data; ... } ``` | Property | Type | Description | | --- | --- | --- | | `engagementId` | Number | A HubSpot created engagement ID. | | `externalCallId` | String | A calling app created call ID. Used to enable [calling in help desk](/guides/api/crm/extensions/third-party-calling). | #### onCallerIdMatchSucceeded This event is called when the caller ID match succeeds. ```js onCallerIdMatchSucceeded: (event) => {}; ``` #### onCallerIdMatchFailed This event is called when the caller ID match fails. ```js onCallerIDMatchFailed: (event) => {}; ``` #### onVisibilityChanged Message indicating if the user has minimized or hidden the calling app. ```js onVisibilityChanged(data) { const { isMinimized, isHidden } = data; ... } ``` #### defaultEventHandler Default handler for events. ```js defaultEventHandler(event) { console.info("Event received. Do you need to handle it?", event); } ``` ## Calling SDK | Frequently Asked Questions ### How is user authentication handled? The calling app should handle authentication. ### Is Calling Extensions hosted on a CDN? Yes. You can install the Calling Extensions SDK via [jsDeliver](https://www.jsdelivr.com/). For example, to install calling-extensions-sdk@0.2.2, you can use [https://cdn.jsdelivr.net/npm/@hubspot/calling-extensions-sdk@0.2.2/dist/main.js](https://cdn.jsdelivr.net/npm/@hubspot/calling-extensions-sdk@0.2.2/dist/main.js). ### When should an engagement be created versus updated? A user can initiate a call from inside the HubSpot UI and outside the HubSpot UI (e.g. mobile app, redirected number, etc.) If a call is initiated from within HubSpot UI, HubSpot will create a call engagement and send the engagement to the calling app. Once the call finishes, the call app can update this engagement with additional call details. If a call is initiated outside of HubSpot UI, the app should create the call engagement. ### What scopes are required as a part of the integration? Add contacts and timeline scopes are required. These scopes ensure your application has access to contacts and the ability to create and update call engagements in the CRM. ### Can this functionality be added to an already existing application in the marketplace or do I create a new app? If you already have an existing app that serves the calling use case then you can directly add this functionality to your existing app. All customers who already have your app installed will get access to this new functionality without having to install the app again. ### Can I integrate my existing soft phone application in the SDK? Yes, integrating your existing soft phone application should be very easy. Just follow the steps in the documentation above to have your application up and running. ### Can users use multiple integrations at the same time? Yes, users can use multiple third-party calling integrations at the same time. They can use the provider switcher presented after clicking on the call button to seamlessly switch between providers. ### Can free users install app integrations? Yes, all users can install the app. ### If a user already has my app installed, does the integration automatically show up? Yes, if a user already has installed your app, and you are updating the same app with the calling extensions, the integration will automatically show up. Currently, there is no way for the developer to enable the calling app only to a subset of customers. ### Can any user install or uninstall an app? No, only users who have necessary permissions can install and uninstall an app. Learn more about how to [review a user's permissions](https://knowledge.hubspot.com/user-management/manage-user-permissions). ### Can I create a custom calling property? Yes, you can create a custom calling property using the [properties API](/guides/api/crm/properties). ### Can I place a call from a custom object? Yes, calling integrations can place calls from custom objects as long as they only use the SDK to create the call. Each integration will need to verify that they only use the Calling SDK to create calls and to notify HubSpot in the `outgoingCall` event. First, verify that the integration is using the Calling SDK to create engagements in the outgoingCall event: ```js outgoingCall({ createEngagement: true }); ``` If `createEngagement` is true, learn how to update your app information [here](#get-your-app-ready-for-production). Here is the example for the entire `outgoingCall` event: ```js const callInfo = { phoneNumber: string, // optional unless call is initiated by the widget createEngagement: true // whether HubSpot should create an engagement for this call callStartTime: number // optional unless call is initiated by the widget }; extensions.outgoingCall(callInfo); ``` # CRM API | CRM cards @ These endpoints allow HubSpot users to interact with information stored in other systems. $ guides & api/crm/extensions --- # CRM cards Within a [public app](/guides/apps/public-apps/overview), you can create custom CRM cards to display information from other systems on HubSpot contact, company, deal, and ticket records. Each app can include up to 25 CRM cards. The CRM cards referenced in this article are different from the app cards you can [create as UI extensions with projects (BETA)](/guides/crm/ui-extensions/create). Classic CRM cards included in this article are the older implementation of custom cards for public apps. The newer app cards offer more flexibility, customizability, and interactivity using a more modern, React-based toolset. If your app currently includes a classic CRM card, learn how to [migrate it to the projects framework](/guides/crm/public-apps/migrate-a-public-app-to-projects) to use newer app cards. **Example use case:** you're building an integration for the App Marketplace for your bug tracking software. You want to be able to surface tracked bugs on contact records so that support reps can reference them when working with customers. Your app can define a custom card that displays this info right on the HubSpot contact record. Cards can be defined as part of your app’s feature settings. Once the app is installed and a user views the target CRM records, HubSpot makes an outbound request to the app, retrieves the relevant data, and displays it in a card on the record page. Apps can also specify custom actions the user can take based on this information. For example, your app could include an action to open a modal for displaying the app's own UI in HubSpot. ## Scope requirements To create custom CRM cards, your app has to request the OAuth scopes needed to modify the CRM records where your card will appear. For example, for a CRM card to appear on contact records, the app must have the `crm.objects.contacts.read` and `crm.objects.contacts.write` scopes. If you later need to remove CRM object scopes from your app, you'll first need to delete all existing cards for those object types. See the OAuth documentation for [more details about scopes](/guides/apps/authentication/working-with-oauth) and setting up the authorization URL for your app. ## Create a CRM card You can create CRM cards for your app either through the API or by editing your app in your developer account. To learn more about configuring a card through the API, check out the [reference documentation](/reference/api/crm/extensions/crm-cards). To create a CRM card using HubSpot's UI: - In your HubSpot developer account, navigate to **Apps** in the main navigation. - Select the **app** where you want to add a card. - In the left sidebar menu, select **CRM cards**. - In the upper right, click **Create CRM card**. Below, learn more about the configuration options in each tab. UI extensions built using developer projects offer more flexible ways to display data and allow user interaction, including displaying external content in frames. If using a private app is feasible for your integration, check out the [UI extensions quickstart guide](/guides/crm/private-apps/quickstart) to get started, or [view HubSpot's example projects](/guides/crm/ui-extensions/sample-extensions/overview) to see examples of what's possible. ## Data request When a user HubSpot views a CRM record that the CRM card is on, HubSpot will make a data fetch request to the integration. This request is made to the specified target URL, which includes a set of default query parameters, along with extra parameters containing property data as specified in the card's settings. - In the _Data fetch URL_ field, enter the **URL** that you'll be fetching data from. In the API, this URL is added to the `targetUrl` field. - In the _Target record types_ section, click to toggle the **switches** on to select which CRM records the card will appear on. Then, Use the **Properties sent from HubSpot** dropdown menus to select the **HubSpot properties** that will be included as query parameters in the request URL. In the API, each record type and its corresponding properties are added as objects in the `objectTypes` array. ```json { "title": "New CRM Card", "fetch": { "targetUrl": "https://www.example.com/demo-fetch", "objectTypes": [ { "name": "contacts", "propertiesToSend": [ "firstname", "email", "lastname" ] } ] } ... } ``` ### Example request The above configuration would result in HubSpot sending its `GET` request as follows. ```shell https://www.example.com/demo-fetch?userId=12345&userEmail=loggedinuser@hubspot.com&associatedObjectId=53701&associatedObjectType=CONTACT&portalId=987654&firstname=Tim&email=timrobinson@itysl.com&lastname=Robinson ``` | Parameter | Type | Description | | --- | --- | --- | | `userId` | Default | The ID of the HubSpot user that loaded the CRM record. | | `userEmail` | Default | The email address of the user that loaded the CRM record. | | `associatedObjectId` | Default | The ID of the CRM record that loaded. | | `associatedObjectType` | Default | The type of CRM record that loaded (e.g., contact, company, deal). | | `portalId` | Default | The ID of the HubSpot account where the CRM record loaded. | | `firstname` | Custom | The contact's first name, as specified in the _Properties sent from HubSpot_ dropdown menu (in-app) and `propertiesToSend` array (API). | | `email` | Custom | The contact's email address, as specified in the _Properties sent from HubSpot_ dropdown menu (in-app) and `propertiesToSend` array (API). | | `lastname` | Custom | The contact's last name, as specified in the _Properties sent from HubSpot_ dropdown menu (in-app) and `propertiesToSend` array (API). | A connection must be made within three seconds, and requests will timeout after five seconds. ### Example response Below is an example response that the integrator might provide to the above request. ```json { "results": [ { "objectId": 245, "title": "API-22: APIs working too fast", "link": "http://example.com/1", "created": "2016-09-15", "priority": "HIGH", "project": "API", "description": "Customer reported that the APIs are just running too fast. This is causing a problem in that they're so happy.", "reporter_type": "Account Manager", "status": "In Progress", "ticket_type": "Bug", "updated": "2016-09-28", "actions": [ { "type": "IFRAME", "width": 890, "height": 748, "uri": "https://example.com/edit-iframe-contents", "label": "Edit", "associatedObjectProperties": [] }, { "type": "IFRAME", "width": 890, "height": 748, "uri": "https://example.com/reassign-iframe-contents", "label": "Reassign", "associatedObjectProperties": [] }, { "type": "ACTION_HOOK", "httpMethod": "PUT", "associatedObjectProperties": [], "uri": "https://example.com/tickets/245/resolve", "label": "Resolve" }, { "type": "CONFIRMATION_ACTION_HOOK", "confirmationMessage": "Are you sure you want to delete this ticket?", "confirmButtonText": "Yes", "cancelButtonText": "No", "httpMethod": "DELETE", "associatedObjectProperties": ["protected_account"], "uri": "https://example.com/tickets/245", "label": "Delete" } ] }, { "objectId": 988, "title": "API-54: Question about bulk APIs", "link": "http://example.com/2", "created": "2016-08-04", "priority": "HIGH", "project": "API", "reported_by": "ksmith@hubspot.com", "description": "Customer is not able to find documentation about our bulk Contacts APIs.", "reporter_type": "Support Rep", "status": "Resolved", "ticket_type": "Bug", "updated": "2016-09-23", "properties": [ { "label": "Resolved by", "dataType": "EMAIL", "value": "ijones@hubspot.com" }, { "label": "Resolution type", "dataType": "STRING", "value": "Referred to documentation" }, { "label": "Resolution impact", "dataType": "CURRENCY", "value": "94.34", "currencyCode": "GBP" } ], "actions": [ { "type": "IFRAME", "width": 890, "height": 748, "uri": "https://example.com/edit-iframe-contents", "label": "Edit" }, { "type": "CONFIRMATION_ACTION_HOOK", "confirmationMessage": "Are you sure you want to delete this ticket?", "confirmButtonText": "Yes", "cancelButtonText": "No", "httpMethod": "DELETE", "associatedObjectProperties": ["protected_account"], "uri": "https://example.com/tickets/245", "label": "Delete" } ] } ], "settingsAction": { "type": "IFRAME", "width": 890, "height": 748, "uri": "https://example.com/settings-iframe-contents", "label": "Settings" }, "primaryAction": { "type": "IFRAME", "width": 890, "height": 748, "uri": "https://example.com/create-iframe-contents", "label": "Create Ticket" } } ``` | Prop | Type | Description | | --- | --- | --- | | `results` | Array | An array of up to five valid [card properties](#card-properties). If more card properties are available for a specific CRM object, your app can link to them. | | `objectId` | Number | A unique ID for this object. | | `title` | String | The title of this object. | | `link` | String | The URL that the user can follow to get more details about the object. If no objects have a link, you should provide a value of `null`. | | `created` | String | A [custom property](#card-properties) as defined in the card's settings that denotes the date of the object's creation. | | `priority` | String | A [custom property](#card-properties) as defined in the card's settings that denotes external ticket's priority level. | | `actions` | Array | A list of available [actions](#action-types) a user can take. | | `properties` | Properties | A list of custom properties that aren't defined in the card settings. You can use this list to display a specific object's unique properties. These properties will be shown in the order they're provided, but after the properties defined in the card settings. | | `settingsAction` | Object | An iframe action that enables users to update the app's settings. | | `primaryAction` | Object | The primary action for a record type, typically a creation action. | | `secondaryActions` | Array | A list of other actions displayed on the card. | ### Request signatures To ensure that the requests are actually coming from HubSpot, the following request header is included. This header will contain a hash of the app secret for your application and the details of the request. `X-HubSpot-Signature: <some base64 string>` To verify this signature, perform the following steps: 1. Create a string that concatenates together the following: `<app secret>` + `<HTTP method>` \+ `<URL>` + `<request body> (if present)`. 2. Create a SHA-256 hash of the resulting string. 3. Compare the hash value to the signature. If they're equal, the request passed validation. If the values do not match, the request may have been tampered with in transit or someone may be spoofing requests to your endpoint. Learn more about [validating requests from HubSpot](/guides/apps/authentication/validating-requests). ## Card properties On the _Card Properties_ tab, define any custom properties that you want HubSpot to display on the CRM card. Once defined, the integration can fill these properties by including them in its response. - Click **Add property** to add a new property for the card to display. The payload you provide in response to the data fetch call should contain values for all of these properties. - In the right panel, set the property's unique name, display label, and data type. You can select from the following types: _Currency_, _Date_, _Datetime_, _Email_, _Link_, _Numeric_, _Status_, and _String._ Learn more about [using extension property types](/guides/apps/extensions/overview). - Click **Add** to save the property. When HubSpot sends its data request, the integration can provide values for these properties in its response alongside other values in each object in `results`. In addition to the properties configured on this tab, the integration can also include its own custom properties without needing them to be defined in the card's settings. For example, in the response below, `created` and `priority` are both defined in the _Card properties_ tab, while the `properties` array sends its own property definitions and values. These object-specific properties must be defined per object. ```json { "objectId": 988, "title": "API-54: Question about bulk APIs", "link": "http://example.com/2", "created": "2016-08-04", "priority": "HIGH", "properties": [ { "label": "Resolved by", "dataType": "EMAIL", "value": "ijones@hubspot.com" }, { "label": "Resolution type", "dataType": "STRING", "value": "Referred to documentation" }, { "label": "Resolution impact", "dataType": "CURRENCY", "value": "94.34", "currencyCode": "GBP" } ], "actions": [ ... ] } ``` When sending custom properties, the `dataType` field for each property can be set to one of: `CURRENCY`, `DATE`, `DATETIME`, `EMAIL`, `LINK`, `NUMERIC`, `STATUS`, `STRING`. Depending on the property type, the integration may need to provide additional fields. Below, learn more about each property type. ### Currency properties `CURRENCY` properties must include a `currencyCode`, which needs to be a valid [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217#Active_codes) code. This will ensure the user sees the correct currency symbol and number formatting. ```json { "results": [ { "properties": [ { "label": "Resolution impact", "dataType": "CURRENCY", "value": "94.34", "currencyCode": "GBP" } ] } ] } ``` ### Date properties `DATE` properties should be in the format `yyyy-mm-dd`. These properties will be displayed in a format appropriate to the user's locale. If you need to include a timestamp, you should instead use a `DATETIME` property. ```json { "results": [ { "properties": [ { "label": "Date", "dataType": "DATE", "value": "2023-10-13" } ] } ] } ``` ### Datetime properties `DATETIME` properties indicate a specific time and must be provided as milliseconds since epoch. These properties will be displayed in a format appropriate to the user's locale. ```json { "results": [ { "properties": [ { "label": "Timestamp", "dataType": "DATETIME", "value": "1697233678777" } ] } ] } ``` ### Email properties `EMAIL` properties are for values that contain an email address. These properties will be displayed as mailto links. ```json { "results": [ { "properties": [ { "label": "Email address", "dataType": "EMAIL", "value": "hobbes.baron@gmail.com" } ] } ] } ``` ### Link properties `LINK` properties display hyperlinks and open in a new window. You can specify a `linkLabel`, otherwise the URL itself will be displayed. ```json { "results": [ { "properties": [ { "label": "Link property", "dataType": "LINK", "value": "https://www.hubspot.com", "linkLabel": "Test link" } ] } ] } ``` ### Numeric properties `NUMERIC` properties display numbers. ```json { "results": [ { "properties": [ { "label": "Number", "dataType": "NUMERIC", "value": "123.45" } ] } ] } ``` ### Status properties `STATUS` properties display as colored indicators. To define a status property, the integration must provide an `optionType` that describes the possible statuses. Statuses include: - `DEFAULT`: Grey - `SUCCESS`: Green - `WARNING`: Yellow - `DANGER`: Red - `INFO`: Blue ```json { "results": [ { "properties": [ { "label": "Status", "dataType": "STATUS", "value": "Errors occurring", "optionType": "DANGER" } ] } ] } ``` ### String properties `STRING` properties display text. ```json { "results": [ { "properties": [ { "label": "First name", "dataType": "STRING", "value": "Tim Robinson" } ] } ] } ``` ## Custom actions On the _Custom actions_ tab, you can define the base URLs that will be requested when a user clicks an action button. You can include multiple action URLs for various actions in your CRM card. Card actions must call an endpoint specified on this tab. for more information. Action URLs are accessed in the `uri` field in an action. Similar to the [data fetch request,](#data-request) action hooks will include a default set of query parameters. You can include other query parameters by including an `associatedObjectProperties` field in the action. The response will vary depending on type of action. Below, learn more about action types. ### Action types #### Iframe actions `IFRAME` actions will open a modal containing an iframe pointing at the provided URL. No request signature is sent when the iframe is opened from the CRM UI. This is because the iframe URL is returned in the original data fetch request, and no additional proxy requests are needed. ```json { "type": "IFRAME", "width": 890, "height": 748, "uri": "https://example.com/iframe-contents", "label": "Edit", "associatedObjectProperties": ["some_crm_property"] } ``` When the user is done completing an action inside the iframe, the modal should close and return the user to the CRM record they started from. To close the modal, the integration can use `window.postMessage` to signal to the CRM that the user is done. The following messages are accepted: - `{"action": "DONE"}`: the user has successfully competed the action. - `{"action": "CANCEL"}`: the user has canceled the action. ```json window.parent.postMessage(JSON.stringify({"action": "DONE"}), "*"); ``` #### Action hook actions `ACTION_HOOK` actions send a server-side request to the integrator. The only UI a users sees for this action is a success or error message. This type of action is useful for simple operations that require no further input from the user. An `X-HubSpot-Signature` header will be included in the request for verification. Learn more about [request signatures](#request-signatures). ```json { "type": "ACTION_HOOK", "httpMethod": "POST", "uri": "https://example.com/action-hook", "label": "Example action", "associatedObjectProperties": ["some_crm_property"] } ``` The `httpMethod` can be set to `GET`, `POST`, `PUT`, `DELETE`, or `PATCH`. If using `GET` or `DELETE`, the `associatedObjectProperties` values will be appended to the request URL as query parameters. Otherwise, the properties will be sent in the request body. ```json window.parent.postMessage(JSON.stringify({"action": "DONE"}), "*"); ``` #### Confirmation actions `CONFIRMATION_ACTION_HOOK` actions behave the same as `ACTION_HOOK` actions, except that a confirmation dialog is shown to the user before running the server-side request. An `X-HubSpot-Signature` header will be included in the request for verification. Learn more about [request signatures](#request-signatures). ```json { "type": "CONFIRMATION_ACTION_HOOK", "httpMethod": "POST", "uri": "https://example.com/action-hook", "label": "Example action", "associatedObjectProperties": ["some_crm_property"], "confirmationMessage": "Are you sure you want to run example action?", "confirmButtonText": "Yes", "cancelButtonText": "No" } ``` The `httpMethod` can be set to `GET`, `POST`, `PUT`, `DELETE`, or `PATCH`. If using `GET` or `DELETE`, the `associatedObjectProperties` values will be appended to the request URL as query parameters. Otherwise, the properties will be sent in the request body. ## Delete a CRM card Once created, you can delete a CRM card from the app's settings: - In your HubSpot developer account, navigate to **Apps** in the main navigation. - Click the **name** of the app that you want to delete a card from. - In the left sidebar menu, select **CRM cards**. - Hover over the card, then click **Delete**. - In the dialog box, confirm the deletion by clicking **Delete this card**. # CRM API | Set up third-party calling in help desk (BETA) @ You can set up third-party calling in your HubSpot help desk $ guides & api/crm/extensions --- # Set up third-party calling in help desk (BETA) Developers must have completed the [SDK setup](/guides/api/crm/extensions/calling-sdk) and [inbound calling API](/guides/apps/extensions/calling-extensions/receive-incoming-calls#integrate-the-hubspot-mobile-chat-sdk-into-your-ios-app-beta-) steps to proceed. To support connecting a phone number as a channel to your help desk, you’ll need to tell HubSpot how to fetch available numbers to be connected. To do this, register a webhook that'll return available numbers for the user. Whenever a HubSpot user wants to connect a device number from your extension to the help desk workspace, HubSpot will make a request to the registered webhook to get a list of available numbers that the user can select from. Check out the [_Channel connection settings_ endpoints](/reference/api/crm/extensions/calling-sdk#channel-connection-settings) in the calling SDK reference documentation for additional details on available parameters for each endpoint, along with example requests and responses. ## Webhook details ##### Request To get the phone numbers available for channel connection, HubSpot will send a `POST` request to your registered webhook with the following request body parameters: | Parameter | Type | Description | | --- | --- | --- | | `appId` | Integer | The ID of the app for which this request was made. | | `portalId` | Integer | The ID of the HubSpot portal where the request originated from. | | `userId` | Integer | The ID of the HubSpot user making the request. | The request will also include HubSpot signature headers to prove that the request came from HubSpot. Learn how to [validate requests](/guides/apps/authentication/validating-requests). ### Expected response schema The endpoint that you provide to HubSpot should return a JSON-formatted response that provides a list of available phone numbers. Each phone number in the list should include the following fields: | Parameter | Type | Description | | --- | --- | --- | | `e164PhoneNumber` | String | The phone number, which will be in E.164 format (e.g., +18001231234). | | `extension` | String | While the webhook response schema accepts phone numbers with extensions, phone numbers with extensions aren't connected to the help desk at this time. Any phone numbers with extensions won't be selectable by the user (e.g., "1"). | | `friendlyName` | String | HubSpot enforces a 24-character limit for friendly names for phone numbers. Any friendly name that's longer than 24 characters will be truncated to the first 24 characters by HubSpot. | For example, if your phone number is 18001231234, your extension is 1, and you want to label this number “My cell phone number”, the response you send back to HubSpot should be: ```json { "phoneNumbers": [ { "e164PhoneNumber": "+18001231234", "extension": "1", "friendlyName": "My cell phone number" } ] } ``` ## Manage the webhook settings for channel connection The following endpoints are available to specify the webhook URL for your channel connection settings, as well as specify whether your webhook is ready to be released to users via the `isReady` flag. Check out the reference documentation and locate the `/channel-connection` endpoints for a full list of available parameters and expected responses. ### Create channel connection settings To create channel connections settings, use the appId of your public app to make a `POST` request to `crm/v3/extensions/calling/{appId}/settings/channel-connection`, and include the following in your request body: | Parameter | Type | Description | | --- | --- | --- | | `url` | String | The target URL for this webhook. | | `isReady` | Boolean | Determines whether this webhook is ready to be released to users of this extension. Learn more about [`isReady`](#isready). | For example, if your endpoint URL was `https://example.com/my-help-desk-service` and you wanted to make it available immediately for HubSpot to call, the request body would be: ```json { "url": "https://example.com/my-help-desk-service", "isReady": true } ``` ### Fetch existing channel connection settings To fetch existing channel connection settings, use the appId of your public app to make a `GET` request to `crm/v3/extensions/calling/{appId}/settings/channel-connection` If your channel connection settings were already set up correctly, the response will resemble the following: ```json { "url": "https://example.com/my-help-desk-service", "createdAt": "2024-04-30 12:01", "updatedAt": "2024-04-30 12:01", "isReady": true } ``` ##### Update channel connections settings To update channel connections settings, make a `PATCH` request to `crm/v3/extensions/calling/{appId}/settings/channel-connection`, and include one or both of the following fields in your request body: | Parameter | Type | Description | | --- | --- | --- | | `url` | String | The target URL for this webhook. | | `isReady` | Boolean | Determines whether this webhook is ready to be released to users of this extension. Learn more about `isReady`. | ### Delete existing channel connection settings To delete existing channel connection settings, use the appId of your public app to make a `DELETE` request to `crm/v3/extensions/calling/{appId}/settings/channel-connection` A successful `DELETE` call will result in a `204 No Content` success status response code. ## Understand the isReady flag A third-party calling app is considered to support channel connection when the app has a webhook registered that's marked with `isReady=true`. If your app supports third-party connections, all users who have this app installed will have the option to select the app when they connect a number to their help desk. Registering your webhook with `isReady=false` at first allows you to register the webhook and test it out without releasing it to all the users of your application immediately. To test your webhook’s help desk channel connection, you can override the isReady flag in-browser by setting local storage flag `LocalSettings:Calling:supportsChannelConnection=true` (see below). Once you have determined that the webhook’s help desk channel connection is working properly and you're ready for users to use your webhook, you can send a `PATCH` request to update the `isReady` flag to `true` and release the functionality to users. ```js // To unlock your webhook for in-browser help desk channel connection testing, run this // command in your browser's dev tools console window.localStorage.setItem( 'LocalSettings:Calling:supportsChannelConnection', true ); ``` ### User experience Below are some examples of the user experience when choosing a calling provider for help desk. Learn more about the user experience. - If `isReady=false` and local storage flag is `false`: the app will appear greyed out for users when choosing a calling provider. - If `isReady=true` or local storage flag is `true`, the users will be able to select the app when choosing a calling provider. # CRM API | Timeline events @ Overview and walkthrough of the Timeline API. $ guides & api/crm/extensions --- # Timeline Events The CRM extensions allow information from other systems to appear on HubSpot contact, company, deal, or ticket objects. The timeline events endpoints allow you to do this by creating custom timeline events. If you'd prefer your data to be editable by users but none of the default CRM objects fit your needs, you can consider using [custom objects](/guides/api/crm/objects/custom-objects). that give you more context about the interactions contacts have with your company. ## Create an event template Before you can start creating events, you must create an event template. Event templates describe actions your app will add to the timeline of a contact, company, or deal object in HubSpot. Examples of these actions include viewing a video, registering for a webinar, or filling out a survey. A single app can create up to 750 event templates. Event templates are created for contacts by default, but they can be created for companies or deals using the `objectType` field. See creating a timeline event template for more details. Each event template has its own set of tokens and templates. You can use events created for contacts as criteria when creating new contact lists or workflows, such as: 'Create a list of all contacts with a Video Like where the video name contains XYZ,' where your event template is named "Video Like" and has an event token named "video name." ### Create event templates through the API For this example, we'll create a new 'Example Webinar Registration' event template. For authentication, use the developer API key found in your app developer account. ```shell curl -X POST -H "Content-Type: application/json" -d ' { "name": "Example Webinar Registration", "objectType": "contacts" }' \ 'https://api.hubapi.com/crm/v3/timeline/<<appId>>/event-templates?hapikey=<<developerAPIkey>>'' ``` Be sure to replace `<appId>` with your own App ID, which can be found on both the _My Apps_ and app details pages in your developer account. You'll also need to replace `<developerHapikey>` with your own developer API key, which you can find by navigating to **Apps** > **Get HubSpot API key**. The properties `headerTemplate` and `detailTemplate` could also be provided here. For more information, see _Define header and detail templates_ below. This `POST` request will return the full, saved event template definition. Be sure to note the `id` property in this response. This is the event template ID, which you'll need to make any updates to this event template or tokens in the future. You can see all event templates defined for an app via this GET command, which will also return the event template IDs: ```shell curl -X GET 'https://api.hubapi.com/crm/v3/timeline/<<appId>>/event-templates?hapikey=<<developerAPIkey>>' ``` ### Create event templates in HubSpot In addition to using the API to create and manage timeline event templates, you can also manage event templates in your HubSpot developer account. In your app settings, navigate to **Timeline events**, then click **Create event type** to create a new event template for this app. If you've created any event templates before, you'll see them here as well. You'll start with a draft of your new event template. Once you've set the object type and the detail and header templates for the event, click **Create**. When creating or editing your event template, set any tokens you want to use with it in the _Data_ tab. If you delete a template, once it's deleted, existing events using that template will be permanently removed from accounts with your app connected. You'll no longer be able to create new events of this type, but you'll still see legacy event data in lists and reports. It may take several hours to see these changes reflected in HubSpot. ### Define event tokens Once you've defined an event template, you'll likely want to define its tokens as well. Event template tokens allow you to attach custom data to events that can be displayed in the timeline and used for automation in workflows. For contacts, they can also be used for list segmentation. You can create up to 500 tokens per timeline event template. #### Create event tokens through the API Using the event template ID created in Step 1, we'll add some tokens to identify the webinar our contacts registered for. ```shell curl -X POST -H "Content-Type: application/json" -d ' { "name": "webinarName", "label": "Webinar Name", "type": "string" }' \ 'https://api.hubapi.com/crm/v3/timeline/<<appId>>/event-templates/<<eventTemplateId>>/tokens?hapikey=<<developerHapikey>>' curl -X POST -H "Content-Type: application/json" -d ' { "name": "webinarId", "label": "Webinar Id", "type": "string" }' \ 'https://api.hubapi.com/crm/v3/timeline/<<appId>>/event-templates/<<eventTemplateId>>/tokens?hapikey=<<developerHapikey>>' curl -X POST -H "Content-Type: application/json" -d ' { "name": "webinarType", "label": "Webinar Type", "type": "enumeration", "options": [ { "value": "regular", "label": "Regular" }, { "value": "ama", "label": "Ask me anything" } ] }' \ 'https://api.hubapi.com/crm/v3/timeline/<<appId>>/event-templates/<<eventTemplateId>>/tokens?hapikey=<<developerHapikey>>' ``` Similarly, a `GET` will return all tokens defined on an event template: ```shell curl -X GET -H "Content-Type: application/json" 'https://api.hubapi.com/crm/v3/timeline/<<appId>>/event-templates/<<eventTemplateId>>?hapikey=<<developerHapikey>>' ``` The supported token types include: - `string` - `number` - `enumeration` — One of a set of options. See the webinarType example above. - `date` — All dates must be in milliseconds in Unix time. _**Note**: Event tokens cannot be named log or lookup. These tokens are reserved as helpers by Handlebars.js, the library used to render in-app events. For more information, check out the Handlebars.js docs [here.](http://handlebarsjs.com/builtin_helpers.html)_ --- ### Define header and detail templates Header and detail templates define how to display a timeline event. You can specify [Markdown](http://daringfireball.net/projects/markdown/syntax) documents with [Handlebars](http://handlebarsjs.com/) templates. The header template should be a one-line description of the event; and the details template is the drill-down view of the event (examples below). The event tokens are passed as data to the templates. Using our example, you can reference the `webinarName` token in the template by using `{{webinarName}}` The `extraData` of an event (discussed below in "Understanding extraData") can only be referenced in the details template. #### Define header and detail templates through the API Header and detail templates can be defined on the event template via the event template endpoints. For example, we can add templates to our 'Example Webinar Registration' by modifying that with a `PUT`: ```shell curl -X PUT -H "Content-Type: application/json" -d ' { "id": "<<eventTemplateId>>", "name": "Example Name Change", "headerTemplate": "Registered for [{{webinarName}}](https://mywebinarsystem/webinar/{{webinarId}})", "detailTemplate": "Registration occurred at {{#formatDate timestamp}}{{/formatDate}}" }' \ 'https://api.hubapi.com/crm/v3/timeline/<<appId>>/event-templates/<<eventTemplateId>>?hapikey=<<developerHapikey>>' ``` Note the use of the `#formatDate` directive—it's something we've defined to allow for user-friendly date formatting. Once an event is created for a contact using this (see "[Creating an event](#creating-an-event)" below), here's what will show up in that contact's timeline: Clicking on "Show details" renders the details template: To set the icon that is displayed next to the events, see "[Setting up a custom icon"](/guides/api/crm/extensions/timeline#customicon) below. The 'Example App Name' text above is the name of the app. In the CRM timeline, events can be filtered by app. ### Define all aspects of an event template in a single call Now that you’ve seen each aspect of an event template is progressively defined, you can define it all in one `POST` call. ```shell curl -X POST -H "Content-Type: application/json" -d ' { "name": "Another Webinar Registration", "objectType": "contacts", "headerTemplate": "Registered for [{{webinarName}}](https://mywebinarsystem/webinar/{{webinarId}})", "detailTemplate": "Registration occurred at {{#formatDate timestamp}}{{/formatDate}}", "tokens": [ { "name": "webinarName", "label": "Webinar Name", "type": "string" }, { "name": "webinarId", "label": "Webinar Id", "type": "string" }, { "name": "webinarType", "label": "Webinar Type", "type": "enumeration", "options": [ { "value": "regular", "label": "Regular" }, { "value": "ama", "label": "Ask me anything" } ] } ] }' \ 'https://api.hubapi.com/crm/v3/timeline/<<appId>>/event-templates?hapikey=<<developerAPIkey>>' ``` ## Create an event Now that an event template is set up with tokens and templates, we're ready to create events for our customers' contacts, companies, deals, and tickets. The examples below assume we're working with the `contacts` event template created above. If the event template above is not set up to have the tokens `webinarName` and `webinarId`, then you will get an error when trying to create the event. Here's an example `POST` for creating an event: Developer API keys and private app access tokens <u>cannot</u> be used as authentication when creating events. To create an event, the associated HubSpot account needs to grant access to your app via [OAuth](/guides/apps/authentication/working-with-oauth). Once you receive an [OAuth access token,](/guides/api/app-management/oauth-tokens) you can use it to add events to the account. ```shell curl -X POST -H "Content-Type: application/json" \ -H "Authorization: Bearer <<OAuth2AccessToken>>" \ -d ' { "eventTemplateId": "<<eventTemplateId>>", "email": "a.test.contact@email.com", "tokens": { "webinarName": "A Test Webinar", "webinarId": "001001", "webinarType": "regular" } }' \ 'https://api.hubapi.com/crm/v3/timeline/events' ``` This generates an event on `a.test.contact@email.com`'s timeline (assuming the templates in 'Defining Templates' above): ### Set the event timestamp The timestamp of the event determines where the event will appear in the object's timeline. By default, the event timestamp is when the POST command is sent. You can customize the event time by providing it in the request body in a timestamp property: ```shell curl -X POST -H "Content-Type: application/json" \ -H "Authorization: Bearer <<OAuth2AccessToken>>" \ -d ' { "eventTemplateId": "<<eventTemplateId>>", "email": "a.test.contact@email.com", "timestamp": "2020-03-18T15:30:32Z", "tokens": { "webinarName": "A Test Webinar", "webinarId": "001001", "webinarType": "regular" } }' \ 'https://api.hubapi.com/crm/v3/timeline/events' ``` This is preferred if you know the exact time an action occurred. In this example, if we have the timestamp for this webinar registration, we should provide it in this POST. Timestamps can be in milliseconds epoch time or in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format. ### Associate an event with a CRM object In order to create an event, you must be able to associate the event with a contact, company, or deal in the customer's account. In the examples above, the objectType was set to contact, and we used email to associate the event with a contact. Email addresses must be unique for contacts in HubSpot, so if there's an existing contact with the provided email, that contact will be updated. If there isn't an existing contact, a new contact will be created. By default, this new contact will only have the email contact property provided. Learn more about [stamping event data onto contact properties](#stamp-event-data-onto-crm-object-properties) to add additional data to contact properties. ```shell // { "eventTemplateId": "<<eventTemplateId>>", "email": "a.test.contact@email.com", "tokens": { "webinarName": "A Test Webinar", "webinarId": "001001", "webinarType": "regular" } } ``` If you are working with known contacts, you can also use the contact `vid` to associate the event. In those cases, you would use `objectId` in the request JSON. You must include the vid of an existing contact, as you will not be able to create new contacts using `objectId`. This example uses the `objectId` instead of email: ```shell // { "eventTemplateId": "<<eventTemplateId>>", "objectId": "29851", "tokens": { "webinarName": "A Test Webinar", "webinarId": "001001", "webinarType": "regular" } } ``` You can also associate an event with a contact by usertoken, or `utk`. The usertoken is used by the HubSpot tracking code to track visitors, and is stored in the `hubspotutk` cookie. Use the `utk` parameter to associate an event with a contact by usertoken. Note: It is not possible to associate events with anonymous visitors using the usertoken, so if the event is associated using only the `utk`, and the provided usertoken is not already associated with a contact, no new contact would be created and the event would not be visible in HubSpot. However, the event would appear in the timeline if a new contact was associated with the usertoken through another means (usually through a [form submission including the hutk](/reference/api/marketing/forms/v3-legacy#submit-data-to-a-form-supporting-authentication), or through the [identify method of the Tracking Code API](/reference/api/analytics-and-events/tracking-code)). For this reason, we recommend including the `email` in addition to the `utk` to make sure that the event gets associated with a new or existing contact. If you're working with an event template for contacts, it's possible to include multiple identification parameters with the event, so any combination of the `email`, `objectId`, and `utk` parameters may be included. If multiple parameters are included, the objectId (vid) will have the highest priority when determining which contact to associate with the event, followed by `utk`, with `email` being the lowest priority. This means that you can update the email address of an existing object by including a new email address in the `email` parameter with the `vid` of a known object in `objectId`. This example uses the email address and usertoken together: ```shell // { "eventTemplateId": "<<eventTemplateId>>", "email": "a.test.contact@email.com", "utk": "89b5afb740d41f4cd6651ac5237edf09" "tokens": { "webinarName": "A Test Webinar", "webinarId": "001001", "webinarType": "regular" } ``` In addition to working with contacts, it's also possible to create event templates for companies and deals. For those event templates, you must use `objectId` to associate the event with the company or deal. For companies, the `objectId` must be set to the `companyId` of the company you want to associate the event with, and for deals you would set the `objectId` to the `dealId` of the deal object. In the example below, assuming the event template was set to the `COMPANY` `objectType`, this event would be associate with the company object with `companyId` 528253914: ```shell // { "eventTemplateId": "<<eventTemplateId>>", "objectId": "3001", "tokens": { "dealProperty": "Custom property for deal" } } ``` ### Timeline extensions The timeline extensions feature can be used to display data from an external system using an iFrame. When included, the event will display a link that will open a modal window displaying the iFrame contents when clicked. The details for the iFrame are set in the timelineIFrame field, which is an object containing the following fields: - `linkLabel` - The text used to display the link that will display the IFrame. - `headerLabel` - The label of the modal window that displays the IFrame contents. - `url` - The URI of the IFrame contents. - `width` - The width of the modal window. - `height` - The height of the modal window. For example, using this data for an event: ```shell // { "eventTemplateId": "<<eventTemplateId>>", "email": "a.test.contact@email.com", "tokens": { "webinarName": "A Test Webinar", "webinarId": "001001", "webinarType": "regular" }, "timelineIFrame": { "linkLabel":"View external data", "headerLabel":"Example iframe", "url":"https://www.example.com", "width":800, "height":300 } } ``` Would create this event, including the "View external data" link: Clicking that link would open a modal window displaying the page set in the `url`: ### Stamp event data onto CRM object properties In many cases, you'll want to modify the properties for the contacts, companies, or deals to which you're adding events. This often happens in cases where adding the event will actually create a contact—you'll likely want to update the first and last name properties on the contact so that you don't just create a contact with only an email address and an event. You can stamp data onto the associated object from an event by mapping your custom event tokens to contact, company, or deal properties. Consider this `PUT` command for updating a custom event template, note the `objectPropertyName` field: ```shell curl -X PUT -H "Content-Type: application/json" -d ' { "label" : "Updated Webinar Name", "objectPropertyName": "zz_webinar_name" }' \ 'https://api.hubapi.com/crm/v3/timeline/<<appId>>/event-templates/<<eventTemplateId>>/tokens/<<tokenName>>?hapikey=<<developerHapikey>>' ``` This uses `objectPropertyName` to map this custom event token to the `contact` Object `zz_webinar_name` property. This means that when we create a new event that specifies a `webinarName` token, the `zz_webinar_name` property of the associated `contact` will also be set. You can set these to custom or predefined HubSpot properties. For example, let's say we already created a `companyName` token referencing a `zz_company_name` custom property on the contact. Then creating an event like this causes the `zz_company_name` and `zz_webinar_name` properties to be set on the contact with the email address a.test.contact@email.com: ```shell curl -X POST -H "Content-Type: application/json" \ -H "Authorization: Bearer <<OAuth2AccessToken>>" \ -d ' { "eventTemplateId": "<<eventTemplateId>>", "email": "a.test.contact@email.com", "tokens": { "webinarName": "Test Webinar will update contact property", "companyName": "TestCo", "webinarId": "001001", "webinarType": "regular" } }' \ 'https://api.hubapi.com/crm/v3/timeline/events' ``` Note: If an event token is stamped to a custom property, and that custom property is not present for a HubSpot account, then the value will still be set for the event, but it will be ignored for the corresponding object. ### Understand `extraData` You may need to add detailed data to an event that doesn't fit the simple token-value structure used by the event template tokens. You may need to add a list or some hierarchical breakdown to an integration event. This is where `extraData` comes in. You can add an `extraData` attribute to an event’s JSON body. The value of this `extraData` can be any valid JSON. For example: ```shell curl -X POST -H "Content-Type: application/json" \ -H "Authorization: Bearer <<OAuth2AccessToken>>" \ -d ' { "eventTemplateId": "<<eventTemplateId>>", "email": "a.test.contact@email.com", "tokens": { "webinarName": "Test Webinar will update contact property", "companyName": "TestCo", "webinarId": "001001", "webinarType": "regular" }, "extraData": { "pollData": [ { "question": "How excited are you for this webinar?", "answer":"Quite!" }, { "question": "How frequently do you use our product?", "answer":"Daily" } ], "coWorkers": [ { "name": "Joe Coworker", "email":"joe.coworker@testco.com" }, { "name": "Jane Coworker", "email":"jane.coworker@testco.com" } ] } }' \ 'https://api.hubapi.com/crm/v3/timeline/events' ``` An example of using `extraData` in a details template: ```shell // Registration occurred at {{#formatDate timestamp}}{{/formatDate}} #### Poll Questions {{#each extraData.pollData}} **{{question}}**: {{answer}} {{/each}} #### Co-Workers {{#each extraData.coWorkers}} * {{name}} {{/each}} ``` Which will result in a timeline event that looks like this: Note: The `extraData` attribute can only be referenced in the details template for an event. It can't be used in either the header template or in list segmentation. ### Set up a custom icon To add visual appeal to your timeline items, you'll want to add a custom icon. This image file for this icon should: - Have roughly square dimensions - Have a transparent background - Have the content in the center of the icon - Be able to size down to 30x30 pixels - Have a file size of 5MB or less To set the icon used for timeline events, navigate to Timeline events. Click on the placeholder image or the existing icon to set or update it. Once you set the icon(s), they will be shown next to all of the timeline events associated with this application: ### Event instance limits When creating an event, each serialized event instance is subject to the following size limits of: - 500 bytes for the event instance ID - 510 KB per property/token - 1 MB in total size for the event instance --- #### Related docs [Understanding the CRM](/guides/api/crm/understanding-the-crm) [CRM cards](/guides/api/crm/extensions/crm-cards) # Video conference extension @ These endpoints are used to plug into the meeting creation flow within HubSpot and add video conference information. $ guides & api/crm/extensions --- Using the video conference API, you can configure a [public app](/guides/apps/public-apps/overview) to give HubSpot users the ability to add a video conference link when [creating a meeting](https://knowledge.hubspot.com/meetings-tool/create-and-edit-scheduling-pages#overview), similar to the [Google Meet](https://knowledge.hubspot.com/integrations/use-hubspots-integration-with-google-meet) and [Zoom](https://knowledge.hubspot.com/integrations/use-hubspots-integration-with-zoom) integration. Using this API will involve: 1. Setting up your public app with video conference extension webhooks. You'll configure your app with the URIs that HubSpot will use to notify you when meetings are being created, updated, or deleted. 2. Setting up a custom back-end to handle meeting creation and update webhooks. 3. If needed, setting up your back-end to handle user identify verification webhooks. All requests to the video conferencing API must be authenticated using your developer account's developer API key. You can include the key in a `hapikey` query parameter at the end of the request URL. You can [find your developer API key](/guides/apps/overview#developer-api-keys) in your developer account. ## Prerequisites Before getting started, ensure you've created a [developer account](/getting-started/account-types#app-developer-accounts) and a [public app](/guides/apps/public-apps/overview) within it. ## Configure webhook URLs So that HubSpot knows where to send notifications when meetings are created, updated, or deleted, you'll first configure the extension's webhook URLs. To do so, make a `PUT` request to `/crm/v3/extensions/videoconferencing/settings/{appId}?hapikey=YOUR_HUBSPOT_DEVELOPER_API_KEY`. In the request body, you'll need to include the `createMeetingUrl` field to set the URL that HubSpot will send meeting creation webhook payloads. You can optionally include additional webhook URLs. These values can be updated later using the same request URL. ```json // Example request { "createMeetingUrl": "https://example.com/create-meeting", "updateMeetingUrl": "https://example.com/update-meeting", "deleteMeetingUrl": "https://example.com/delete-meeting" } ``` | Field | Type | Description | | --- | --- | --- | | `createMeetingUrl` | String | The URL that HubSpot will notify when a new meeting has been created. This will include a JSON payload with the meeting details. The URL must use `https` protocol. | | `updateMeetingUrl` | String | The URL that HubSpot will notify when an existing meeting has been updated. This will include a JSON payload with the updated meeting details. The URL must use `https` protocol. | | `deleteMeetingUrl` | String | The URL that HubSpot will notify when an existing meeting has been deleted. This will include a `conferenceId` of the deleted meeting. The URL must use `https` protocol. | | `userVerifyUrl` | String | The URL that HubSpot will use to verify that the user exists in the external video conferencing system. | **Example Response:** ```json // Example 200 response { "createMeetingUrl": "https://example.com/create-meeting", "updateMeetingUrl": "https://example.com/update-meeting", "deleteMeetingUrl": "https://example.com/delete-meeting" } ``` ### Retrieve webhook URLs To retrieve webhook URLs configured for an existing app, make a `GET` request to `/crm/v3/extensions/videoconferencing/settings/{appId}`. The response will include the currently configured webhook URLs. ```json // Example 200 response { "createMeetingUrl": "https://example.com/create-meeting", "updateMeetingUrl": "https://example.com/update- meeting", "deleteMeetingUrl": "https://example.com/delete-meeting", "userVerifyUrl": "https://example.com/user-verify" } ``` ## Webhook payloads Once webhook URLs are configured, HubSpot will send payloads to those URLs when a meeting create, update, or delete event occurs. All webhooks sent by HubSpot are HMAC signed using your app's secret. You can learn more about this in the [webhooks security documentation](/guides/api/app-management/webhooks#security) (the rest of the page does not apply to these video conference extension webhooks). ### Create meeting webhook When a meeting is created, HubSpot will send a request to the URL specified in the `createMeetingUrl` field. The request payload includes information about the HubSpot account, associated HubSpot user, and meeting details. ```json // Example payload { "portalId": 123123, "userId": 123, "userEmail": "test.user@example.com", "topic": "A Test Meeting", "source": "MEETINGS", "startTime": 1534197600000, "endTime": 1534201200000 } ``` | Field | Type | Description | | --- | --- | --- | | `portalId` | Number | The ID of the HubSpot account that the request is coming from. | | `userId` | Number | The ID of the HubSpot user that is assigned to the meeting. | | `userEmail` | String | The email address of the HubSpot user assigned to the meeting. | | `topic` | String | The title of the meeting. | | `source` | String | Indicates where the meeting was created. Can be either:<ul><li>`MEETINGS`: the meeting was creating from a [meeting scheduling page](https://knowledge.hubspot.com/meetings-tool/create-and-edit-scheduling-pages#create-scheduling-pages).</li><li>`MANUAL`: the meeting was created manually from a CRM record.</li></ul> | | `startTime` | Number | The start time of the meeting in epoch milliseconds. | | `endTime` | Number | The end time of the meeting in epoch milliseconds. | To successfully handle this webhook, you should generate a video conference for this meeting (or link it to an existing conference line), and respond with information about this conference. The fields expected in the response are: - `conferenceId`: a unique ID associated with the conference on this event. This ID needs to be globally unique within your system. HubSpot will return this ID back to you in the update webhook. - `conferenceUrl:` the URL that users can use to join the conference. - `conferenceDetails`: plain text invitation information. This should describe how attendees of the event can access the video conference for this event. Newlines will be maintained in representations of this text, but no other formatting is supported. For example, your response might look like the following: ```json //example response { "conferenceId": "some-unique-id", "conferenceUrl": "https://example.com/join", "conferenceDetails": "Click here to join: https://example.com/join" } ``` ### Update meeting webhook If you've specified a `updateMeetingUrl` , HubSpot will send this URI a request whenever a meeting's relevant details have changed. This notification is necessary if you need to maintain the most up-to date topic or times for a video conference. The request payload includes information about the HubSpot account, associated HubSpot user, and meeting details. ```json //example request { "conferenceId": "some-unique-id", "userId": 123, "userEmail": "test.user@example.com", "portalId": 123123, "topic": "A Test Meeting (updated)", "startTime": 1534197600000, "endTime": 1534201200000 } ``` | Field | Type | Description | | --- | --- | --- | | `conferenceId` | String | The unique identifier for the conference provided by your integration in the response to the create meeting webhook. | | `userId` | Number | The ID of the HubSpot user that is assigned to the meeting. This will always be the same user as the one in the meeting creation payload. | | `userEmail` | String | The email address of the HubSpot user assigned to the meeting. This will always be the same user as the one in the meeting creation payload. | | `portalId` | Number | The ID of the HubSpot account that the request is coming from. | | `topic` | String | The title of the meeting. | | `startTime` | Number | The start time of the meeting in epoch milliseconds. | | `endTime` | Number | The end time of the meeting in epoch milliseconds. | No response body is required when responding to these requests. Only a `200` or `204` response code is required to let HubSpot know that this webhook was been received successfully. ### Deleted meeting webhook When a meeting is deleted in HubSpot, a request will be sent to the URL specified in the `deleteMeetingUrl` field. The payload will include the `conferenceId` of the deleted meeting. ```json // Example payload { "conferenceId": "some-unique-id" } ``` No response body is required when responding to these requests. Only a `200` or `204` response code is required to let HubSpot know that this webhook was been received successfully. ### User verification webhook HubSpot's systems will always communicate with you about its users in terms of their HubSpot user ID and their HubSpot account email address. There is a chance that a user in HubSpot's system may exist in your system with a different email address or identifier. Before HubSpot makes a call to your system to create, update, or delete a video conference link, it will first check the app's settings for a `userVerifyUrl` field. If that URL has been set, HubSpot will make a call to it to retrieve your native user identifier. It will then send that identifier as the user's email address in the subsequent call. If you do not configure this URL, HubSpot will always assume that the user's identity is verified. This setting is optional and is up to you to determine if it's needed. If you need to maintain a user mapping within your system, you may wish to simply map the HubSpot user ID or email to your internal user ID on each call. The payload for this webhook contains the HubSpot account ID and the user's email address. ```json //example request { "portalId": 123123, "userEmail": "test.user@example.com" } ``` You can return a `200` response or any error code (`404` would be appropriate). If you return a `200`, you should return a payload containing the new ID that we should use in place of the email address: ```json //example response { "id": "any-string-id" } ``` # CRM API | Imports @ Imports are used to populate a HubSpot account with object data that can be used with the sales, marketing, and service tools. $ guides & api/crm --- Use the imports API to import CRM records and activities into your HubSpot account, such as contacts, companies, and notes. Once imported, you can access and update records and activities through the various CRM API endpoints, including the [contacts API,](/guides/api/crm/understanding-the-crm) [associations API](/guides/api/crm/associations/associations-v4), and [engagements APIs.](/guides/api/crm/engagements/engagement-details) You can also [import records and activities using the guided import tool in HubSpot](https://knowledge.hubspot.com/import-and-export/import-objects). Before starting your import, learn more about: - [File requirements](https://knowledge.hubspot.com/import-and-export/set-up-your-import-file#file-requirements-and-technical-limits) - [Required properties for record and activity imports](https://knowledge.hubspot.com/import-and-export/set-up-your-import-file#required-properties) - [Required properties for marketing event participant imports](https://knowledge.hubspot.com/integrations/import-marketing-event-participants#prepare-your-import-file) To create custom events or associate events with records, use the [custom events API](https://developers.hubspot.com/docs/reference/api/analytics-and-events/custom-events/custom-event-completions) instead of importing. ## Start an import You can start an import by making a `POST` request to `/crm/v3/imports` with a request body that specifies how to map the columns of your import file to the associated properties in HubSpot. API imports are sent as form-data type requests, with the request body containing following fields: - [importRequest](#format-the-importrequest-data): a text field that contains the request JSON. - [files](#map-file-columns-to-hubspot-properties): a file field that contains the import file. For the request header, add a `Content-Type` header with a value of `multipart/form-data`. The screenshot below shows what your request might look like when using an application like Postman: ### Format the importRequest data In your request, define the import file details, including mapping the spreadsheet's columns to HubSpot data. Your request should include the following fields: - **name**: the name of the import. In HubSpot, this is the name [displayed in the imports tool](https://knowledge.hubspot.com/import-and-export/view-and-analyze-previous-imports), as well as the name that you can reference in other tools, such as lists. - **importOperations**: an optional field used to indicate whether the import should [create and update, <u>only</u> create, or <u>only</u> update records](https://knowledge.hubspot.com/import-and-export/import-objects#choose-how-to-import) for a certain object or activity. Include the `objectTypeId` for the object/activity and whether you want to `UPSERT` (create and update), `CREATE`, or `UPDATE` records. For example, the field would look like this in your request: `"importOperations": {"0-1": "CREATE"}`. If you don't include this field, the default value used for the import is `UPSERT`. - **dateFormat**: the format for dates included in the file. By default, this is set to `MONTH_DAY_YEAR`, but you can also use `DAY_MONTH_YEAR` or `YEAR_MONTH_DAY`. - **marketableContactImport**: an optional field to indicate the [marketing status](https://knowledge.hubspot.com/records/marketing-contacts) of contacts in your import file. This is only used when importing contacts into accounts that have [access to marketing contacts](https://knowledge.hubspot.com/records/marketing-contacts#check-your-access-to-marketing-contacts). To set the contacts in the file as marketing, use the value `true`. To set the contacts in the file as non-marketing, use the value `false`. - **createContactListFromImport**: an optional field to [create a static list of the contacts](https://knowledge.hubspot.com/import-and-export/import-objects#create-list-from-import) from your import. To create a list from your file, use the value `true`. - **files**: an array that contains your import file information. - **fileName**: the name of the import file. - **fileFormat:** the import file's format. For CSV files, use a value of `CSV`. For Excel files, use a value of `SPREADSHEET`. - **fileImportPage**: contains the `columnMappings` array required to map data from your import file to HubSpot data. Learn more about column mapping below. ### Map file columns to HubSpot properties Within the `columnMappings` array, include an entry for each column in your import file, matching the order of your spreadsheet. For each column, include the following fields: - **columnObjectTypeId:** the name or `objectTypeId` value of the object or activity to which the data belongs. Refer to [this article](/guides/api/crm/understanding-the-crm#object-type-id) for a full list of `objectTypeId` values. - **columnName:** the name of the column header. - **propertyName:** the internal name of the HubSpot property that the data will map to. For the common column in [multi-file imports](#import-multiple-files), `propertyName` should be `null` when the `toColumnObjectTypeId` field is used. - **columnType:** used to specify that a column contains a [unique identifier property.](https://knowledge.hubspot.com/import-and-export/understand-the-import-tool#unique-identifiers) Depending on the property and goal of the import, use one of the following values: - **HUBSPOT_OBJECT_ID:** the ID of a record. For example, your contact import file might contain a _Record ID_ column that stores the ID of the company you want to associate the contacts with. - **HUBSPOT_ALTERNATE_ID:** a unique identifier other than the record ID. For example, your contact import file might contain an _Email_ column that stores the contacts' email addresses. - **FLEXIBLE_ASSOCIATION_LABEL**: include this column type to indicate the column contains association labels. - **ASSOCIATION_KEYS**: for same object association imports only, include this column type for the unique identifier of the same object records you're associating. For example, in your request for a contacts association import, the _Associated contact \[email/Record ID\]_ column must have a `columnType` of `ASSOCIATION_KEYS`. Learn more about [setting up your import file for a same object association import](https://knowledge.hubspot.com/import-and-export/set-up-your-import-file#same-object-association-imports). - **toColumnObjectTypeId**: for [multi-file](#import-multiple-files) or multiple object imports, the name or `objectTypeId` of the object the [common column](https://knowledge.hubspot.com/import-and-export/set-up-your-import-file#sample-import-files:~:text=To%20import%20and%20associate%20multiple%20objects%20in%20one%20file%2C%20include%20information%20about%20associated%20records%20and/or%20activities%20in%20the%20same%20row.%20In%20two%20files%2C%20use%20a%20common%20column%20to%20connect%20the%20records%20in%20each%20file.%20You%20can%20refer%20to%20the%20example%20files%20for%20more%20help%20importing%20and%20associating%20records.) property or association label belongs to. Include this field for the common column property in the file of the object the property does <u>not</u> belong to. For example, if you're associating contacts and companies in two files with the contact property _Email_ as the common column, include the `toColumnObjectTypeId` for the _Email_ column in the <u>company</u> file. - **foreignKeyType**: for [multi-file imports](#import-multiple-files) only, the type of association the common column should use, specified by the `associationTypeId` and `associationCategory`. Include this field for the common column property in the file of the object the property does <u>not</u> belong to. For example, if you're associating contacts and companies in two files with the contact property _Email_ as the common column, include the `foreignKeyType` for the _Email_ column in the <u>company</u> file. - **associationIdentifierColumn**: for [multi-file imports](#import-multiple-files) only, indicates the property used in the common column to associate the records. Include this field for the common column property in the file of the object the property belongs to. For example, if you're associating contacts and companies in two files with contact property _Email_ as the common column, set the `associationIdentifierColumn` as `true` for the _Email_ column in the <u>contact</u> file. ## Import one file with one object Below is an example request body of importing one file to create contacts: ```json // Example POST to https://api.hubspot.com/crm/v3/imports // Content-Type header set to multipart/form-data { "name": "November Marketing Event Leads", "importOperations": { "0-1": "CREATE" }, "dateFormat": "DAY_MONTH_YEAR", "files": [ { "fileName": "Nov-event-leads.csv", "fileFormat": "CSV", "fileImportPage": { "hasHeader": true, "columnMappings": [ { "columnObjectTypeId": "0-1", "columnName": "First Name", "propertyName": "firstname" }, { "columnObjectTypeId": "0-1", "columnName": "Last Name", "propertyName": "lastname" }, { "columnObjectTypeId": "0-1", "columnName": "Email", "propertyName": "email", "columnType": "HUBSPOT_ALTERNATE_ID" } ] } } ] } ``` ```py # This example a local file named 'test_import.csv' # This file contains a list of contact records to import. import requests import json import os url = "https://api.hubapi.com/crm/v3/imports" YOUR_ACCESS_TOKEN = 'xxxxxxx'; # Content-Type header will be set automatically by the requests library headers = { 'authorization': 'Bearer %s' % YOUR_ACCESS_TOKEN } data = { "name": "November Marketing Event Leads", "importOperations": { "0-1": "CREATE" }, "dateFormat": "DAY_MONTH_YEAR", "files": [ { "fileName": "Nov-event-leads.csv", "fileFormat": "CSV", "fileImportPage": { "hasHeader": True, "columnMappings": [ { "columnObjectTypeId": "0-1", "columnName": "First Name", "propertyName": "firstname" }, { "columnObjectTypeId": "0-1", "columnName": "Last Name", "propertyName": "lastname" }, { "columnObjectTypeId": "0-1", "columnName": "Email", "propertyName": "email", "columnType": "HUBSPOT_ALTERNATE_ID" } ] } } ] } datastring = json.dumps(data) payload = {"importRequest": datastring} current_dir = os.path.dirname(__file__) relative_path = "./test_import.csv" absolute_file_path = os.path.join(current_dir, relative_path) files = [ ('files', open(absolute_file_path, 'rb')) ] print(files) response = requests.request("POST", url, data=payload, files=files, headers=headers) print(response.text.encode('utf8')) print(response.status_code) ``` ```shell # Using this endpoint requires using sending multi-part form encoded data. Here is an example curl request: # importing a file named import_file.csv # create a variable for the importRequest JSON myJSON=$(cat <<EOF { "name": "November Marketing Event Leads", "importOperations": { "0-1": "CREATE" }, "dateFormat": "DAY_MONTH_YEAR", "files": [ { "fileName": "import_file.csv", "fileFormat": "CSV", "fileImportPage": { "hasHeader": true, "columnMappings": [ { "columnObjectTypeId": "0-1", "columnName": "First Name", "propertyName": "firstname" }, { "columnObjectTypeId": "0-1", "columnName": "Last Name", "propertyName": "lastname" }, { "columnObjectTypeId": "0-1", "columnName": "Email", "propertyName": "email", "columnType": "HUBSPOT_ALTERNATE_ID" } ] } } ] } EOF ) YOUR_ACCESS_TOKEN="xxx-xxx-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" curl -v \ -F "files=@import_file.csv;type=text/csv" \ -F "importRequest=$myJSON;type=application/json" \ -H "Authorization: Bearer $YOUR_ACCESS_TOKEN" \ https://api.hubapi.com/crm/v3/imports ``` Below is another example, importing one file to create and update marketing event partcipants: ```json // Example POST to https://api.hubspot.com/crm/v3/imports // Content-Type header set to multipart/form-data { "name": "Marketing Events Import Example", "marketingEventObjectId": 377224141223, "importOperations": { "0-1": "UPSERT", "0-54": "CREATE" }, "dateFormat": "YEAR_MONTH_DAY", "timeZone": "America/New_York", "files": [ { "fileName": "Marketing Events Import Example.csv", "fileFormat": "CSV", "fileImportPage": { "hasHeader": true, "columnMappings": [ { "columnName": "First Name", "columnObjectTypeId": "0-1", "propertyName": "firstname", "columnType": "STANDARD" }, { "columnName": "Last Name", "columnObjectTypeId": "0-1", "propertyName": "lastname", "columnType": "STANDARD" }, { "columnName": "Email", "columnObjectTypeId": "0-1", "propertyName": "email", "columnType": "HUBSPOT_ALTERNATE_ID" }, { "columnName": "Registered", "columnObjectTypeId": "0-54", "columnType": "EVENT_TIMESTAMP", "marketingEventSubmissionType": "REGISTERED" }, { "columnName": "Attended", "columnObjectTypeId": "0-54", "columnType": "EVENT_TIMESTAMP", "marketingEventSubmissionType": "JOINED" }, { "columnName": "Left", "columnObjectTypeId": "0-54", "columnType": "EVENT_TIMESTAMP", "marketingEventSubmissionType": "LEFT" }, { "columnName": "Cancelled", "columnObjectTypeId": "0-54", "columnType": "EVENT_TIMESTAMP", "marketingEventSubmissionType": "CANCELLED" } ] } } ] } ``` ## Import one file with multiple objects Below is an example request body of importing and associating contacts and companies in one file with association labels: ```json // Example POST to https://api.hubspot.com/crm/v3/imports // Content-Type header set to multipart/form-data { "name": "Association Label Import", "dateFormat": "DAY_MONTH_YEAR", "files": [ { "fileName": "association label example.xlsx", "fileFormat": "SPREADSHEET", "fileImportPage": { "hasHeader": true, "columnMappings": [ { "columnObjectTypeId": "0-1", "columnName": "Email", "propertyName": "email" }, { "columnObjectTypeId": "0-2", "columnName": "Domain", "propertyName": "domain" }, { "columnName": "Association Label", "columnType": "FLEXIBLE_ASSOCIATION_LABEL", "columnObjectTypeId": "0-1", "toColumnObjectTypeId": "0-2" } ] } } ] } ``` ## Import multiple files Below is an example request body of importing and associating contacts and companies in two files, where the contact property _Email_ is the common column in the files: ```json // Example POST to https://api.hubspot.com/crm/v3/imports // Content-Type header set to multipart/form-data { "name": "Contact Company import", "dateFormat": "YEAR_MONTH_DAY", "files": [ { "fileName": "contact-import-file.csv", "fileFormat": "CSV", "fileImportPage": { "hasHeader": true, "columnMappings": [ { "columnObjectTypeId": "0-1", "columnName": "First name", "propertyName": "firstname" }, { "columnObjectTypeId": "0-1", "columnName": "Last name", "propertyName": "lastname" }, { "columnObjectTypeId": "0-1", "columnName": "Email", "propertyName": "email", "associationIdentifierColumn": true } ] } }, { "fileName": "company-import-file.csv", "fileFormat": "CSV", "fileImportPage": { "hasHeader": true, "columnMappings": [ { "columnObjectTypeId": "0-2", "columnName": "Company name", "propertyName": "name" }, { "columnObjectTypeId": "0-2", "columnName": "Company domain name", "propertyName": "domain", "columnType": "HUBSPOT_ALTERNATE_ID" }, { "columnObjectTypeId": "0-2", "toColumnObjectTypeId": "0-1", "columnName": "Email", "propertyName": null, "foreignKeyType": { "associationTypeId": 280, "associationCategory": "HUBSPOT_DEFINED" } } ] } } ] } ``` On a successful request, the response will include an `importId` which you can use to retrieve or cancel the import. Once completed, you can [view the import in HubSpot](https://knowledge.hubspot.com/import-and-export/view-and-analyze-previous-imports), but imports completed via API will not be available as an option when filtering records by import in views, lists, reports, or workflows. ## Get previous imports To retrieve all imports from your HubSpot account, make a `GET` request to `/crm/v3/imports/`. To retrieve information for a specific import, make a `GET` request to `/crm/v3/imports/{importId}`. When you retrieve imports, information will be returned including the import's name, source, file format, language, date format, and column mappings. The import's `state` will also be returned, which can be any of the following: - `STARTED`: HubSpot recognizes that the import exists, but the import hasn't started processing yet. - `PROCESSING`: The import is actively being processed. - `DONE`: The import is complete. All the objects, activities, or associations have been updated or created. - `FAILED`: There was an error that was not detected when the import was started. The import was not completed. - `CANCELED`: User cancelled the export while it was in any of the `STARTED`, `PROCESSING`, or `DEFERRED` states. - `DEFERRED`: The maximum number of imports (three) are processing at the same time. The import will start once one of the other imports finishes processing. Learn more about paging and limiting results in the _[reference documentation](https://developers.hubspot.com/docs/reference/api/crm/imports)._ When retrieving imports using a private app access token, the response will only include imports performed by that private app. Imports completed in HubSpot or via another private app will not be returned. ## Cancel an import To cancel an active import, make a `POST` request to `/crm/v3/imports/{importId}/cancel`. ## View and troubleshoot import errors To view errors for a specific import, make a `GET` request to `/crm/v3/imports/{importId}/errors`. Learn more about [common import errors and how to resolve them](https://knowledge.hubspot.com/import-and-export/troubleshoot-import-errors). For errors such as _Incorrect number of columns_, _Unable to parse_ JSON or _404 text/html is not accepted_: - Ensure that there is a column header for each column in your file, and that the request body contains a `columnMapping` entry for each column. The following criteria should be met: - The column order in the request body and import file should match. If the column order doesn't match, the system will attempt to automatically reorder but may be unsuccessful, resulting in an error when the import is started. - Every column needs to be mapped. If a column is not mapped, the import request may still be successful, but would result in the _Incorrect number of columns_ error when the import is started. - Ensure that the file's name and the `fileName` field in your request JSON match, and that you've included the file extension in the `fileName` field. For example, _import_name.csv._ - Ensure that your header includes `Content-Type` with a value of `multipart/form-data`. ```json Example POST URL: https://api.hubapi.com/crm/v3/imports? Example importRequest JSON data: This example contains 3 columns: - First name, mapped to the firstname contact property - Email, mapped to the email contact property - Company ID, which contains a list of company record IDs that the contact will be assocated with. { "name": "test_import", "files": [ { "fileName": "final_emails.csv", "fileImportPage": { "hasHeader": true, "columnMappings": [ { "ignored": false, "columnName": "First Name", "idColumnType": null, "propertyName": "firstname", "foreignKeyType": null, "columnObjectType": "CONTACT", "associationIdentifierColumn": false }, { "ignored": false, "columnName": "Email", "idColumnType": "HUBSPOT_ALTERNATE_ID", "propertyName": "email", "foreignKeyType": null, "columnObjectType": "CONTACT", "associationIdentifierColumn": false }, { "ignored": false, "columnName": "Company ID", "idColumnType": "HUBSPOT_OBJECT_ID", "propertyName": null, "foreignKeyType": { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 1 }, "columnObjectType": "CONTACT", "associationIdentifierColumn": false } ] } } ] } ``` ```py # This example a local file named 'final_emails.csv' # This file contains a list of contact records to import. # Each row in the file contains a column named "Company ID" that contains the companyId # that the contact should be associated with. import requests import json post_url = 'http://api.hubapi.com/crm/v3/imports?hapikey=4324...5432' importRequest = { "name": "test_import", "files": [ { "fileName": "final_emails.csv", "fileImportPage": { "hasHeader": True, "columnMappings": [ { "ignored": False, "columnName": "First Name", "idColumnType": None, "propertyName": "firstname", "foreignKeyType": None, "columnObjectType": "CONTACT", "associationIdentifierColumn": False }, { "ignored": False, "columnName": "Email", "idColumnType": "HUBSPOT_ALTERNATE_ID", "propertyName": "email", "foreignKeyType": None, "columnObjectType": "CONTACT", "associationIdentifierColumn": False }, { "ignored": False, "columnName": "Company ID", "idColumnType": "HUBSPOT_OBJECT_ID", "propertyName": None, "foreignKeyType": { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 1 }, "columnObjectType": "CONTACT", "associationIdentifierColumn": False } ] } } ] ``` ```php <?php // This example imports a local file named 'import_file.csv' $post_url = "http://api.hubapi.com/crm/v3/imports?hapikey=1234...5342"; $csv_file = new CURLFile('import_file.csv','text/csv'); $config_json = '{"name":"test_import","files":[{"fileName":"final_emails.csv","fileImportPage":{"hasHeader":true,"columnMappings":[{"ignored":false,"columnName":"First Name","idColumnType":null,"propertyName":"firstname","foreignKeyType":null,"columnObjectType":"CONTACT","associationIdentifierColumn":false},{"ignored":false,"columnName":"Email","idColumnType":"HUBSPOT_ALTERNATE_ID","propertyName":"email","foreignKeyType":null,"columnObjectType":"CONTACT","associationIdentifierColumn":false},{"ignored":false,"columnName":"Company ID","idColumnType":"HUBSPOT_OBJECT_ID","propertyName":null,"foreignKeyType":{"associationCategory":"HUBSPOT_DEFINED","associationTypeId":1},"columnObjectType":"CONTACT","associationIdentifierColumn":false}]}}]};type=application/json'; $post_data = array( "files" => $csv_file, "importRequest" => $config_json ); $ch = curl_init(); @curl_setopt($ch, CURLOPT_POST, true); @curl_setopt($ch, CURLOPT_URL, $post_url); @curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data); $response = @curl_exec($ch); //Log the response from HubSpot as needed. $status_code = @curl_getinfo($ch, CURLINFO_HTTP_CODE); //Log the response status code @curl_close($ch); echo $status_code . " " . $response; ?> ``` ```shell # Using this endpoint requires using sending multi-part form encoded data. Here is an example curl request: # importing a file named import_file.csv # create a variable for the importRequest JSON myJSON=$(cat <<EOF { "name": "test_import", "files": [ { "fileName": "final_emails.csv", "fileImportPage": { "hasHeader": true, "columnMappings": [ { "ignored": false, "columnName": "First Name", "idColumnType": null, "propertyName": "firstname", "foreignKeyType": null, "columnObjectType": "CONTACT", "associationIdentifierColumn": false }, { "ignored": false, "columnName": "Email", "idColumnType": "HUBSPOT_ALTERNATE_ID", "propertyName": "email", "foreignKeyType": null, "columnObjectType": "CONTACT", "associationIdentifierColumn": false }, { "ignored": false, "columnName": "Company ID", "idColumnType": "HUBSPOT_OBJECT_ID", "propertyName": null, "foreignKeyType": { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 1 }, "columnObjectType": "CONTACT", "associationIdentifierColumn": false } ] } } ] } EOF ) curl -v \ -F "files=@import_file.csv;type=text/csv" \ -F "importRequest=$myJSON;type=application/json" \ http://api.hubapi.com/crm/v3/imports?hapikey=4123...4321 ``` If you receive an error, check if there are any duplicate headers, such as `Content-Type`. This may occur if you're using Postman or if it's included in the header of your Python script. Remove the duplicate before completing the request. ## Limits When using the imports API, you can import up to 80,000,000 rows per day. However, individual import files are limited to 1,048,576 rows or 512 MB, whichever is reached first. If your request exceeds either the row or size limit, HubSpot will respond with a 429 HTTP error. When approaching these limits, it's recommended to split your import into multiple requests. # Limits tracking @ Learn how to view an account's usage and limits for CRM data such as records, associations, pipelines, and properties. $ guides & api/crm --- # Limits Tracking In your HubSpot account, there are limits to the amount of CRM data you can store. Some limits are technical, but others depend on your [HubSpot subscription](https://legal.hubspot.com/hubspot-product-and-services-catalog). You can [track your usage in HubSpot](https://knowledge.hubspot.com/data-management/track-crm-data-limits) or use the limits tracking API to check your limits and usage for the following data: - Records - Associations - Custom properties - Pipelines - Calculated properties (_Professional_ and _Enterprise_ only) - Association labels (_Professional_ and _Enterprise_ only) - Custom objects (_Enterprise_ only) ## Retrieve record limits and usage To view the number of records your account can store per object and how many records you're currently storing, make a `GET` request to `/crm/v3/limits/records`. In the response, default objects are returned in the `hubspotDefinedObjectTypes` array. For each object, the following are returned: - `limit`: the number of records you can store for the object. - `usage`: the number of records in the account for the object. - `percentage`: the percentage of the total limit used. If applicable, custom objects are returned in the `customObjectTypes` object with individual object data and total custom object record limits and usage. Learn more about [custom object definition limits](#retrieve-custom-object-limits-and-usage). Your response will look similar to the following: ```json //Example response GET crm/v3/limits/records { "hubspotDefinedObjectTypes": [ { "objectTypeId": "0-421", "singularLabel": "Appointment", "pluralLabel": "Appointments", "limit": 500000, "usage": 0, "percentage": 0 }, { "objectTypeId": "0-2", "singularLabel": "Company", "pluralLabel": "Companies", "limit": 15000000, "usage": 94, "percentage": 0.001 }, { "objectTypeId": "0-1", "singularLabel": "Contact", "pluralLabel": "Contacts", "limit": 15000000, "usage": 133, "percentage": 0.001 }, { "objectTypeId": "0-3", "singularLabel": "Deal", "pluralLabel": "Deals", "limit": 15000000, "usage": 188, "percentage": 0.001 }, { "objectTypeId": "0-53", "singularLabel": "Invoice", "pluralLabel": "Invoices", "limit": 15000000, "usage": 1, "percentage": 0 }, { "objectTypeId": "0-54", "singularLabel": "Marketing event", "pluralLabel": "Marketing events", "limit": 15000000, "usage": 2, "percentage": 0 }, { "objectTypeId": "0-7", "singularLabel": "Product", "pluralLabel": "Products", "limit": 15000000, "usage": 5, "percentage": 0 }, { "objectTypeId": "0-162", "singularLabel": "Service", "pluralLabel": "Services", "limit": 500000, "usage": 0, "percentage": 0 }, { "objectTypeId": "0-5", "singularLabel": "Ticket", "pluralLabel": "Tickets", "limit": 15000000, "usage": 28, "percentage": 0 } ], "customObjectTypes": { "overallLimit": 1500000, "overallUsage": 8, "overallPercentage": 0.001, "byObjectType": [ { "objectTypeId": "2-19187792", "singularLabel": "Car", "pluralLabel": "Cars", "usage": 1 }, { "objectTypeId": "2-2140579", "singularLabel": "Pet", "pluralLabel": "Pets", "usage": 7 } ] } } ``` ## Retrieve record association limits and usage In HubSpot, an individual _record_ can only be associated _to_ a specific number of records _of the same or another object. For example, by default, a single contact record can be associated with up to 50,000 company records. You can use the association limit endpoints to check for records that are close to (80%+) or at the association limit._ Learn more about technical [limits for record associations.](https://legal.hubspot.com/hubspot-product-and-services-catalog) - To check which objects have records are close to or at the association limit, make a `GET` request to `crm/v3/limits/associations/records/from`. An object will be included in the response if it has records approaching or at the limit. If no records are close to or have reached an association limit, the response will be blank. To drill down into the specific object associations, you can use the endpoints below. - To check the associated objects for which an object has records close to or at an association limit, make a `GET` request to `crm/v3/limits/associations/records/{fromObjectTypeId}/to`. If records of the `fromObjectType` are approaching or at the association limit for an associated object, that object will be included in the response. For example, if you make a request for companies (i.e. `crm/v3/limits/associations/records/0-2/to`) and there is a company record associated with 48,000 contacts and 46,000 deals, contacts and deals will be included in the response. - To view a sample of specific records approaching or at an association limit between two objects, make a `GET` request `crm/v3/limits/associations/records/{fromObjectTypeId}/{toObjectTypeId}`. In the response, the following are returned: - `limit`: refers to the number of associated records each record can have. - `totalRecordsNearLimit`: counts the number of records using 80% or more of their association limit. - `totalRecordsAtLimit`: counts the number of records that have reached the limit. - If any records are near or at the limit, a sample of record `id` values will be included in the `nearLimitFromRecordSamples` and `atLimitFromRecordSamples` arrays. For example, to retrieve records approaching limits for contacts to companies, you'd make a `GET` request to `crm/v3/limits/associations/records/0-1/0-2`. If no records are close to or at the limit, the response would look like the following: ```json //Example response GET crm/v3/limits/associations/records/0-1/0-2 { "limit": 50000, "totalRecordsNearLimit": 0, "totalRecordsAtLimit": 0, "nearLimitFromRecordSamples": [], "atLimitFromRecordSamples": [] } ``` ## Retrieve association label limits and usage If your account has a _Professional_ or _Enterprise_ subscription, you can check the limits and usage for custom association labels. - To track limits for all association labels in the account, make a `GET` request to `crm/v3/limits/associations/labels`. - To track limits for association labels from one object from or to all other objects, make a `GET` request to `crm/v3/limits/associations/labels` and include the `fromObjectTypeId` or `toObjectTypeId` parameter. For example, `crm/v3/limits/associations/labels/?fromObjectTypeId=0-1` or `crm/v3/limits/associations/labels/?toObjectTypeId=0-1`. - To track limits for association labels from one object to another specific object, make a `GET` request to `crm/v3/limits/associations/labels` and include the `fromObjectTypeId`and `toObjectTypeId` parameters. For example, `crm/v3/limits/associations/labels/?fromObjectTypeId=0-1&toObjectTypeId=0-2`. In the response, the following are returned: - `limit`: refers to the number of association labels you can create between the objects. - `usage`: the number of labels created in the account. - `percentage`: the percentage of the total limit used. - `allLabels`: an array with the names of the custom labels. For example, to retrieve association label limits and usage for contacts to companies, you'd make a `GET` request to `crm/v3/limits/associations/labels/?fromObjectTypeId=0-1&toObjectTypeId=0-2`. Your response would look similar to the sample below. In this example, six out of 50 labels are created for contact to company associations. ```json //Example response GET crm/v3/limits/associations/labels/?fromObjectTypeId=0-1&toObjectTypeId=0-2 { "results": [ { "limit": 50, "usage": 6, "percentage": 12, "fromObjectType": { "objectTypeId": "0-1", "singularLabel": "Contact", "pluralLabel": "Contacts" }, "toObjectType": { "objectTypeId": "0-2", "singularLabel": "Company", "pluralLabel": "Companies" }, "allLabels": [ "Billing contact", "Chef", "Contract worker", "Decision maker", "Franchise location", "Manager" ] } ] } ``` ## Retrieve property limits and usage You can view limits and usage for properties, including the number of custom properties or calculation properties you've created. The number of properties can create depends on your [HubSpot subscription](https://legal.hubspot.com/hubspot-product-and-services-catalog). ### Custom properties To track how many custom properties you've created overall and per object, make a `GET` request to `crm/v3/limits/custom-properties`. In the response, the following are returned: - `overallLimit`: the limit for custom properties across all objects. - `overallUsage`: the total number of custom properties in the account. - `overallPercentage`: the percentage of the limit used. - The `byObjectType` array includes the following for each object: - `limit`: the number of custom properties you can create for the object. - `usage`: the number of custom properties you've created for the object. - `percentage`: the percentage of the total limit used. The `overallLimit` and by object `limit` values are distinct and separate, so the overall limit may not equal the sum of by object limits. Your response will look similar to the following: ```json //Example response GET crm/v3/limits/custom-properties { "overallLimit": 10000, "overallUsage": 59, "overallPercentage": 0.59, "byObjectType": [ { "objectTypeId": "0-421", "singularLabel": "Appointment", "pluralLabel": "Appointments", "limit": 1000, "usage": 4, "percentage": 0.4 }, { "objectTypeId": "2-19187792", "singularLabel": "Car", "pluralLabel": "Cars", "limit": 1000, "usage": 1, "percentage": 0.1 }, { "objectTypeId": "0-2", "singularLabel": "Company", "pluralLabel": "Companies", "limit": 1000, "usage": 4, "percentage": 0.4 }, { "objectTypeId": "0-1", "singularLabel": "Contact", "pluralLabel": "Contacts", "limit": 1000, "usage": 58, "percentage": 5.8 }, { "objectTypeId": "0-3", "singularLabel": "Deal", "pluralLabel": "Deals", "limit": 1000, "usage": 15, "percentage": 1.5 }, { "objectTypeId": "2-19188239", "singularLabel": "Dress", "pluralLabel": "Dresses", "limit": 1000, "usage": 3, "percentage": 0.3 }, { "objectTypeId": "2-7282133", "singularLabel": "Menu item", "pluralLabel": "Menu items", "limit": 1000, "usage": 1, "percentage": 0.1 }, { "objectTypeId": "2-2140579", "singularLabel": "Pet", "pluralLabel": "Pets", "limit": 1000, "usage": 7, "percentage": 0.7 }, { "objectTypeId": "0-162", "singularLabel": "Service", "pluralLabel": "Services", "limit": 1000, "usage": 0, "percentage": 0 }, { "objectTypeId": "0-5", "singularLabel": "Ticket", "pluralLabel": "Tickets", "limit": 1000, "usage": 1, "percentage": 0.1 } ] } ``` ### Calculated properties If your account has a _Professional_ or _Enterprise_ subscription, you can track how many [calculation properties](https://knowledge.hubspot.com/properties/create-calculation-properties) you've created. To track calculation property usage overall and per object, make a `GET` request to `crm/v3/limits/calculated-properties`. In the response, the following are returned: - `overallLimit`: the limit for calculation properties across all objects. - `overallUsage`: the total number of calculation properties in the account. - `overallPercentage`: the percentage of the limit used. - The `byObjectType` array includes each object with a `usage` value, which refers to the number of calculation properties you've created for the object. Your response would look similar to the following: ```json //Example response GET crm/v3/limits/calculated-properties { "overallLimit": 200, "overallUsage": 8, "overallPercentage": 4, "byObjectType": [ { "objectTypeId": "0-421", "singularLabel": "Appointment", "pluralLabel": "Appointments", "usage": 0 }, { "objectTypeId": "2-19187792", "singularLabel": "Car", "pluralLabel": "Cars", "usage": 0 }, { "objectTypeId": "0-2", "singularLabel": "Company", "pluralLabel": "Companies", "usage": 0 }, { "objectTypeId": "0-1", "singularLabel": "Contact", "pluralLabel": "Contacts", "usage": 3 }, { "objectTypeId": "0-3", "singularLabel": "Deal", "pluralLabel": "Deals", "usage": 3 }, { "objectTypeId": "2-19188239", "singularLabel": "Dress", "pluralLabel": "Dresses", "usage": 0 }, { "objectTypeId": "2-7282133", "singularLabel": "Menu item", "pluralLabel": "Menu items", "usage": 0 }, { "objectTypeId": "2-2140579", "singularLabel": "Pet", "pluralLabel": "Pets", "usage": 0 }, { "objectTypeId": "0-162", "singularLabel": "Service", "pluralLabel": "Services", "usage": 0 }, { "objectTypeId": "0-5", "singularLabel": "Ticket", "pluralLabel": "Tickets", "usage": 0 } ] } ``` ## Retrieve pipeline limits and usage To view pipeline limits and usage per object, make a `GET` request to `crm/v3/limits/pipelines`. Default objects with [pipelines](/guides/api/crm/pipelines) will be returned in the `hubspotDefinedObjectTypes` array. For each object, the following are returned: - `limit`: the number of pipelines you can create for the object. - `usage`: the number of pipelines created for the object. - `percentage`: the percentage of the total limit used. If applicable, custom objects are returned in the `customObjectTypes` object with individual object data and total custom object pipeline limits and usage. Learn more about [custom object definition limits](#retrieve-custom-object-limits-and-usage). Your response will look similar to the following: ```json //Example response GET crm/v3/limits/pipelines { "hubspotDefinedObjectTypes": [ { "objectTypeId": "0-421", "singularLabel": "Appointment", "pluralLabel": "Appointments", "limit": 15, "usage": 1, "percentage": 6.667 }, { "objectTypeId": "0-3", "singularLabel": "Deal", "pluralLabel": "Deals", "limit": 100, "usage": 5, "percentage": 5 }, { "objectTypeId": "0-136", "singularLabel": "Lead", "pluralLabel": "Leads", "limit": 15, "usage": 1, "percentage": 6.667 }, { "objectTypeId": "0-123", "singularLabel": "Order", "pluralLabel": "Orders", "limit": 50, "usage": 1, "percentage": 2 }, { "objectTypeId": "0-162", "singularLabel": "Service", "pluralLabel": "Services", "limit": 15, "usage": 1, "percentage": 6.667 }, { "objectTypeId": "0-5", "singularLabel": "Ticket", "pluralLabel": "Tickets", "limit": 100, "usage": 4, "percentage": 4 } ], "customObjectTypes": { "overallLimit": 100, "overallUsage": 3, "overallPercentage": 3, "byObjectType": [ { "objectTypeId": "2-19187792", "singularLabel": "Car", "pluralLabel": "Cars", "usage": 0 }, { "objectTypeId": "2-19188239", "singularLabel": "Dress", "pluralLabel": "Dresses", "usage": 1 }, { "objectTypeId": "2-2140579", "singularLabel": "Pet", "pluralLabel": "Pets", "usage": 2 } ] } } ``` ## Retrieve custom object limits and usage If your account has a _Enterprise_ subscription, you can view limits and usage for [custom object schemas,](https://developers.hubspot.com/docs/guides/api/crm/objects/custom-objects#create-a-custom-object)as in the types of custom objects (e.g., Pets, Cars, etc.). If you want to view how many custom object <u>records</u> you can create, use the [record limits endpoint](#retrieve-record-limits-and-usage) instead. To retrieve custom object limits and usage, make a `GET` request to `crm/v3/limits/custom-object-types`. In the response, the following are returned: - `limit`: the number of custom objects you can create. - `usage`: the number of custom objects you've created. - `percentage`: the percentage of the total limit used. Your response would look similar to the sample below. In this example, the account has created four custom objects. ```json //Example response GET crm/v3/limits/custom-object-types { "limit": 20, "usage": 4, "percentage": 20 } ``` # List filters overview @ Learn how to structure filters when using the Lists API. $ guides & api/crm/lists --- # List filters If you select a `SNAPSHOT` or `DYNAMIC` list processing type when creating a list, use filters to determine which records are members of the list. - List filters have conditional logic that is defined by filter branches which have `AND` or `OR` operation types. This is defined by the `filterBranchType` parameter. - Within these branches, there are groups of individual filters that contain logic to assess records to determine if they should be included in the list. These are defined by the `filterType` parameter. - Filter branches can also have nested filter branches. ## Filters There are a variety of different filter types that can be used to build out filters in a list's filter definition. These filters types can be used together in different combinations to construct the logic that is needed for a particular list definition structure. HubSpot uses **PASS**/**FAIL** logic with filters to determine if a record should be in a list. If a record passes all filters, it will be a member of the list. ## Filter evaluation steps To determine which records pass a list's filters, the following steps occur in order: 1. Select or fetch the relevant records based on the filter selected. For example, the property values for all records being evaluated for a [property filter.](#property-filter-options) 2. If applicable, use the `pruningRefineBy` parameter to refine the data to a specific time range (see Refine By Operation section). 3. Apply the filtering rules against the refined data to determine if the records **PASS** or **FAIL**. For example, if the filter "First Name is equal to "John"" is selected, **PASS** all records that have "John" for their First Name contact property. 4. If applicable, use the `coalescingRefineBy` parameter to further refine the data to a specific number of occurrences. For example, the filter "contact has filled out a form at least 2 times". - If a `coalescingRefineBy` parameter is present, then **PASS** records that meet the number of occurrences selected. - If no `coalescingRefineBy` parameter is present, then records **PASS** or **FAIL** based on the criteria set in step 3. ## Filter branches A filter branch is a data structure used to build out the conditional logic of a list's definition. Filter branches are defined by a specific type, an operator, a list of filters, and a list of sub-branches. The filter branch's operator `OR` or `AND` dictate how the filter branch will be evaluated relative to the rest of the branches. The list of filters and sub-filter branches determine which records will be members of a list. - If the filter branch has an `AND` operator, then the record is accepted by the branch if it passes **all** the branch's filters and is accepted by **all** the sub-filter branches. - If the filter branch has an `OR` operator, then the record is accepted by the branch if it passes **any** of the branch's filters or is accepted by **any** of the branch's sub-filter branches. A record is a member of a list if it is accepted by the root filter branch in the list definition. ## Structure All filter definitions must start with a root-level `OR` `filterBranchType` (see [filter branch types](#filter-branch-types) for more details). This root level `OR` filter branch must then have one or more `AND` sub-filter branches. The root-level **`OR`** filter branch can be thought of as the parent filter branch while the `AND` sub-filter branches can be thought of as child filter groups. Together, these branches make up the base filter branch structure. ```json { "filterBranch": { "filterBranches": [ { "filterBranches": [], "filterBranchType": "AND", "filters": [ // can have filters here ] } // can have more filterBranches here ], "filterBranchType": "OR", "filters": [] } } ``` For example, to create a list of contacts who have the first name "John" OR <u>do not</u> have the last name "Smith", the `POST` body would be: ```json { "filterBranch": { "filterBranchType": "OR", "filters": [], "filterBranches": [ { "filterBranchType": "AND", "filters": [ { "filterType": "PROPERTY", "property": "firstname", "operation": { "operationType": "MULTISTRING", "operator": "IS_EQUAL_TO", "values": ["John"] } } ], "filterBranches": [] }, { "filterBranchType": "AND", "filters": [ { "filterType": "PROPERTY", "property": "lastname", "operation": { "operationType": "MULTISTRING", "operator": "IS_NOT_EQUAL_TO", "values": ["Smith"] } } ], "filterBranches": [] } ] } } ``` In the above example, there is one parent `OR` `filterBranchType` parameter with two nested `AND` `filterBranchType` parameters. The nested filter branches each have one `filterType` that sets the criteria for the list. The two `filterType` parameters are nested within `AND` filter branches, rather than directly within an `OR` filter branch. This structure is enforced by HubSpot's API so that the list's filters can be properly rendered in the HubSpot user interface. In the HubSpot user interface, the above code would look like the image below. Any contacts that meet <u>either</u> of the criteria will be a member of the list. To create a list of contacts who have the first name "John" <u>and</u> the last name "Smith": ```json { "filterBranch": { "filterBranchType": "OR", "filters": [], "filterBranches": [ { "filterBranchType": "AND", "filters": [ { "filterType": "PROPERTY", "property": "firstname", "operation": { "operationType": "MULTISTRING", "operator": "IS_EQUAL_TO", "values": ["John"] } }, { "filterType": "PROPERTY", "property": "lastname", "operation": { "operationType": "MULTISTRING", "operator": "IS_EQUAL_TO", "values": ["Smith"] } } ], "filterBranches": [] } ] } } ``` In the above example, there is one parent `OR` `filterBranchType` with one nested `AND` `filterBranchType`. The `AND` `filterBranchType` has two `filterType` parameters, one for each criteria. In the HubSpot user interface, the above code would look like the image below. Any contacts that meet the criteria will be a member of the list. ## Filtering on events and associated objects There are two special versions of `AND` `filterBranchType` parameters: - `UNIFIED_EVENTS`: used to filter on events. - `ASSOCIATION`: used to filter on records that are associated with the primary record being evaluated. These branches should be used within an `AND` filter branch. For example: ```json { "filterBranches": [ { "filterBranches": [ { "filterBranchType": "UNIFIED_EVENTS", "filterBranchOperator": "AND", "filters": [ // must have one or more filters here ] // additional UNIFIED_EVENTS filter branch details omitted }, { "filterBranchType": "ASSOCIATION", "filterBranchOperator": "AND", "filters": [ // may have one or more filters here ], "filterBranches": [ // may have another ASSOCIATION branch here if filtering based on line item properties from a contact list ] // additional ASSOCIATION filter branch details omitted } ], "filterBranchOperator": "AND", "filterBranchType": "AND", "filters": [ // can have filters here ] } // can have more filterBranches here ], "filterBranchOperator": "OR", "filterBranchType": "OR", "filters": [] } ``` ## Filter branch types Below, review the different `filterBranchType` parameters that can be used to construct your list's filter definition structure. ## OR filter branch Begin your filter definition structure with an `OR` `filterBranchType`. It is used to apply **OR** conditional logic against records that are accepted by the nested `AND` filter branches. `OR` filter branches: - Must have one or more `AND` type sub-filter branches. - <u> <u>C</u> </u> <u>annot</u> have any filters. If a record is accepted by <u>any</u> of its nested filter branches, an `OR` filter branch will accept the record as well. ```json { "filterBranchType": "OR", "filterBranches": [], // One or more nested AND filter branches "filters": [] // Zero filters } ``` ## AND filter branch The `AND` `filterBranchType` is used as a nested filter branch within the parent `OR` filter branch. All filter definitions must have at least one `AND` filter branch for it to be valid. It is used to apply **AND** conditional logic against the records that pass evaluation as defined by its filters and have also been accepted by nested filter branches. **AND** filter branches: - Can have zero or more filters. - Can have zero or more nested `UNIFIED_EVENTS` and/or `ASSOCIATION` `filterBranchType` parameters. An `AND` filter branch accepts a record if the record is accepted by <u>all</u> of its nested filter branches and the record passes <u>all</u> filters in the filter branch. ```json { "filterBranchType": "AND", "filterBranches": [], // Zero or more nested UNIFIED_EVENTS or ASSOCIATION filter branches "filters": [] // Zero or more filters } ``` ## UNIFIED_EVENTS filter branch The `UNIFIED_EVENTS` `filterBranchType` can only be used as a nested filter branch within an `AND` `filterBranchType`. It is used to determine which records have or have not completed a given unified event. `UNIFIED_EVENTS` type filter branches: - Can have one or more `PROPERTY` type filters. - Cannot have any additional filter branches. A `UNIFIED_EVENTS` filter branch accepts a record if the record passes <u>all</u> filters on the filter branch and the criteria defined by the `UNIFIED_EVENTS` filter branch. ```json { "filterBranchType": "UNIFIED_EVENTS", "filterBranches": [], // Zero filter branches "filters": [], // Zero or more filters "eventTypeId": "0-1", "operator": "HAS_COMPLETED" } ``` | Parameter | Accepted Values | | ---------- | ------------------------------------ | | `operator` | `HAS_COMPLETED`, `HAS_NOT_COMPLETED` | ## ASSOCIATION filter branch The `ASSOCIATION` `filterBranchType` can only be used as a nested filter branch within an `AND` `filterBranchType`. It is used to filter on records which are associated with the primary record being evaluated. `ASSOCIATION` filter branches: - Must have one or more filters. - Cannot have any additional nested filter branches. An `ASSOCIATION` filter branch accepts a record if it is accepted by <u>all</u> of its nested filter branches and if the record **PASSES** all filters of the filter branch. You can only have additional `filterBranches` in the case of a `CONTACT` to `LINE_ITEM` association. ```json { "filterBranchType": "ASSOCIATION", "filterBranches": [], // Zero or one nested ASSOCIATION filter branches "filters": [], // Zero or more filters "objectTypeId": "0-1", "operator": "IN_LIST", "associationTypeId": 280, "associationCategory": "HUBSPOT_DEFINED" } ``` | Parameter | Accepted Values | | --- | --- | | `operator` | `IN_LIST` | | `associationCategory` | `HUBSPOT_DEFINED`, `USER_DEFINED`, `INTEGRATOR_DEFINED` | ## Filter types Review the table below for the different types of filters that can be used. The `filterType` parameter is used to define the filter within the `filterBranch`. For more information, see the [list filters reference page](/reference/api/crm/lists). | Parameter | Description | | --- | --- | | `ADS_TIME` | Evaluates whether a contact has seen any ads in the timeframe defined by the `pruningRefineBy` parameter. | | `ADS_SEARCH` | Evaluates whether a contact has performed the ad interactions as defined by the filter. | | `CTA` | Evaluates whether a contact has or has not interacted with a specific call-to-action as defined by the filter. | | `EMAIL_EVENT` | Evaluate the opt-in status of a contact for specific email subscriptions as defined by the filter. | | `EVENT` | Evaluates whether a contact has or does not have a specific event as defined by the filter. | | `FORM_SUBMISSION` | Evaluates whether a contact has or has not filled out a specific form or any form as defined by the filter. | | `FORM_SUBMISSION_ON_PAGE` | Evaluates whether a contact has or has not filled out a specific or any form on a specific page as defined by the filter. | | `IN_LIST` | Evaluates whether a record is or is not a member of a specific list, import, or workflow as defined by the filter. | | `PAGE_VIEW` | Evaluates whether a contact has or has not viewed a specific page as defined by the filter. | | `PRIVACY` | Evaluates whether a contact does or does not have privacy consent for a specific privacy type as defined by the filter. | | `PROPERTY` | Evaluates whether a record’s property value satisfies the property filter operation as defined by the filter. See the [property filter operations](#property-filter-operations) section for more details. | | `SURVEY_MONKEY` | Evaluates whether a contact has or has not responded to a specific Survey Monkey survey as defined by the filter. | | `SURVEY_MONKEY_VALUE` | Evaluates whether a contact has or has not responded to a specific Survey Monkey survey’s question with a specified value as defined by the filter. | | `WEBINAR` | Evaluates whether a contact has or has not registered or attended any webinars or a specific webinar as defined by the filter. | | `INTEGRATION_EVENT` | Integration event filters can be used to filter specific contacts based on whether or not they have interacted with integration events that have properties as specified by the filter lines. | ## Property filter operations When filtering for records with the `PROPERTY`, `INTEGRATION_EVENT`, or `SURVEY_MONKEY_VALUE` filter type, you'll include an `operation` object to define the parameters of the filter. This object can contain the following fields: - `operationType`: the type of operator that you're filtering by (e.g., `NUMBER`). Each type of property supports a set of operation types, and each operation type supports a set of operators, which you'll define with the `operator` field. (e.g., `IS` and `IS_NOT`). - `operator`: the operator that will be applied to `operationType` (e.g., `IS` and `IS_NOT`). Each property type supports a set of operators. Learn more about property types and their supporter operators in the table below. - `value` / `values`: the value or values to filter by. Some operators can accept one value, while others can accept multiple values in an array. - `includeObjectsWithNoValueSet`: defines how the operation should treat records that do not have a value set for the defined property. - If `true`, a record without a value for the evaluated property will be <u>accepted</u>. - If `false` (default), a record without a value for the evaluated property will be <u>rejected</u>. For example, the filter below would filter for contacts with a `firstname` property value of `John`. ```json "filters": [ { "filterType": "PROPERTY", "property": "firstname", "operation": { "operationType": "MULTISTRING", "operator": "IS_EQUAL_TO", "values": ["John"] } } ] ``` The table below gives an overview of the available operation types. For more information, along with example code, check out the [property operation definitions reference](/reference/api/crm/lists). | OperationType | Supported operators | Description | | --- | --- | --- | | `ALL_PROPERTY` | String | Used to determine whether a property value is known or is unknown as defined by the property operation. | | `BOOL` | String | Used to determine whether a current (or historical) boolean property value is or is not (or has or has not) equaled a specific value. | | `ENUMERATION` | String | Used to determine whether an enumeration/multi-select property value is any of, is none of, is exactly, is not exactly, contains all of, does not contain all of, has ever been any of, has never been any of, has ever been exactly, has never been exactly, has ever contained all, or has never contained all of a given set of values as defined by the property operation. | | `MULTISTRING` | String | Used to determine whether a string property value is equal to, is not equal to, contains, does not contain, starts with, or ends with any of a given set of values as defined by the property operation. | | `NUMBER` | String | Used to determine whether a current (or historical) number property value is or is not (or has or has not) equaled a specific value as defined by the property operation. | | `STRING` | String | Used to determine whether a current (or historical) string property value is or is not (or has or has not) equaled a specific value as defined by the property operation. | | `TIME_POINT` | String | Used to determine if a property has been updated before or after a specific time. That time can be specified as a specific date or relative to the current day. This operation also allows the comparison of two properties or their last updated time. | | `TIME_RANGED` | String | Used to determine if a property has been updated between or outside of two specific times. These times can be specified as a specific date or relative to the current day. | ## TIME_POINT and TIME_RANGED examples Below, review some examples when using the `TIME_POINT` and `TIME_RANGED` parameter. These parameters can be used in both time-referenced and property-referenced requests. For more information, see the ## Is equal to date The request below filters for _Last activity date is equal to 03/11/2024 (EDT)_. ```json { "name": "Sample Date Equal", "objectTypeId": "0-1", "processingType": "DYNAMIC", "filterBranch": { "filterBranches": [ { "filterBranches": [], "filters": [ { "property": "notes_last_updated", "operation": { "operator": "IS_BETWEEN", "includeObjectsWithNoValueSet": false, "lowerBoundEndpointBehavior": "INCLUSIVE", "upperBoundEndpointBehavior": "INCLUSIVE", "propertyParser": "VALUE", "lowerBoundTimePoint": { "timeType": "DATE", "timezoneSource": "CUSTOM", "zoneId": "America/New_York", "year": 2024, "month": 3, "day": 11, "hour": 0, "minute": 0, "second": 0, "millisecond": 0 }, "upperBoundTimePoint": { "timeType": "DATE", "timezoneSource": "CUSTOM", "zoneId": "US/Eastern", "year": 2024, "month": 3, "day": 11, "hour": 23, "minute": 59, "second": 59, "millisecond": 999 }, "type": "TIME_RANGED", "operationType": "TIME_RANGED" }, "filterType": "PROPERTY" } ], "filterBranchType": "AND", "filterBranchOperator": "AND" } ], "filters": [], "filterBranchOperator": "OR", "filterBranchType": "OR" } ``` ## In Last X Number of Days The example below filters for _Last activity date is less than 3 days ago_. ```json { "name": "Sample Date Last 3 days", "objectTypeId": "0-1", "processingType": "DYNAMIC", "filterBranch": { "filterBranches": [ { "filterBranches": [], "filters": [ { "property": "notes_last_updated", "operation": { "operator": "IS_BETWEEN", "includeObjectsWithNoValueSet": false, "lowerBoundEndpointBehavior": "INCLUSIVE", "upperBoundEndpointBehavior": "INCLUSIVE", "propertyParser": "VALUE", "lowerBoundTimePoint": { "timeType": "INDEXED", "timezoneSource": "CUSTOM", "zoneId": "US/Eastern", "indexReference": { "referenceType": "TODAY" }, "offset": { "days": -3 } }, "upperBoundTimePoint": { "timeType": "INDEXED", "timezoneSource": "CUSTOM", "zoneId": "America/New_York", "indexReference": { "referenceType": "NOW" } }, "type": "TIME_RANGED", "operationType": "TIME_RANGED" }, "filterType": "PROPERTY" } ], "filterBranchType": "AND", "filterBranchOperator": "AND" } ], "filters": [], "filterBranchOperator": "OR", "filterBranchType": "OR" } } ``` ## In Next X Number of Days The example below filters for _Last activity date is less than 5 days from now_. ```json { "name": "Sample Date Next 5 Days", "objectTypeId": "0-1", "processingType": "DYNAMIC", "filterBranch": { "filterBranches": [ { "filterBranches": [], "filters": [ { "property": "notes_last_updated", "operation": { "operator": "IS_BETWEEN", "includeObjectsWithNoValueSet": false, "lowerBoundEndpointBehavior": "INCLUSIVE", "upperBoundEndpointBehavior": "INCLUSIVE", "propertyParser": "VALUE", "lowerBoundTimePoint": { "timeType": "INDEXED", "timezoneSource": "CUSTOM", "zoneId": "US/Eastern", "indexReference": { "referenceType": "NOW" } }, "upperBoundTimePoint": { "timeType": "INDEXED", "timezoneSource": "CUSTOM", "zoneId": "US/Eastern", "indexReference": { "referenceType": "TODAY" }, "offset": { "days": 5 } }, "type": "TIME_RANGED", "operationType": "TIME_RANGED" }, "filterType": "PROPERTY" } ], "filterBranchType": "AND", "filterBranchOperator": "AND" } ], "filters": [], "filterBranchOperator": "OR", "filterBranchType": "OR" } } ``` ## Updated or Not Updated in the Last X Days The example below filters for _Last activity date updated in last 7 days_. To filter for _Last activity date <u>not updated in last</u> 7 days_, change the `operator` parameter to `IS_NOT_BETWEEN`. ```json { "name": "Sample Date Updated Last 7 Days", "objectTypeId": "0-1", "processingType": "DYNAMIC", "filterBranch": { "filterBranches": [ { "filterBranches": [], "filters": [ { "property": "notes_last_updated", "operation": { "operator": "IS_BETWEEN", "includeObjectsWithNoValueSet": false, "lowerBoundEndpointBehavior": "INCLUSIVE", "upperBoundEndpointBehavior": "INCLUSIVE", "propertyParser": "UPDATED_AT", "lowerBoundTimePoint": { "timeType": "INDEXED", "timezoneSource": "CUSTOM", "zoneId": "America/New_York", "indexReference": { "referenceType": "TODAY" }, "offset": { "days": -7 } }, "upperBoundTimePoint": { "timeType": "INDEXED", "timezoneSource": "CUSTOM", "zoneId": "America/New_York", "indexReference": { "referenceType": "NOW" } }, "type": "TIME_RANGED", "operationType": "TIME_RANGED" }, "filterType": "PROPERTY" } ], "filterBranchType": "AND", "filterBranchOperator": "AND" } ], "filters": [], "filterBranchOperator": "OR", "filterBranchType": "OR" } } ``` ## Is After Date The example below filters for _Last activity date is <u>after</u> 03/04/2024 (EST)_. ```json { "name": "Sample Date After", "objectTypeId": "0-1", "processingType": "DYNAMIC", "filterBranch": { "filterBranches": [ { "filterBranches": [], "filters": [ { "property": "notes_last_updated", "operation": { "operator": "IS_AFTER", "includeObjectsWithNoValueSet": false, "timePoint": { "timeType": "DATE", "timezoneSource": "CUSTOM", "zoneId": "America/New_York", "year": 2024, "month": 3, "day": 4, "hour": 23, "minute": 59, "second": 59, "millisecond": 999 }, "endpointBehavior": "EXCLUSIVE", "propertyParser": "VALUE", "type": "TIME_POINT", "operationType": "TIME_POINT" }, "filterType": "PROPERTY" } ], "filterBranchType": "AND", "filterBranchOperator": "AND" } ], "filters": [], "filterBranchOperator": "OR", "filterBranchType": "OR" } } ``` ## Is Relative to Today The example below can either represent _Last activity date is more than x days from <u>now</u>_ or _Last activity date is more than x days <u>ago</u>_. To filter for the latter, set the value for the `offset` parameter to `<=0`. ```json { "name": "Sample Days From Now", "objectTypeId": "0-1", "processingType": "DYNAMIC", "filterBranch": { "filterBranches": [ { "filterBranches": [], "filters": [ { "property": "notes_last_updated", "operation": { "operator": "IS_AFTER", "includeObjectsWithNoValueSet": false, "timePoint": { "timeType": "INDEXED", "timezoneSource": "CUSTOM", "zoneId": "America/New_York", "indexReference": { "referenceType": "TODAY" }, "offset": { "days": 2 } }, "endpointBehavior": "EXCLUSIVE", "propertyParser": "VALUE", "type": "TIME_POINT", "operationType": "TIME_POINT" }, "filterType": "PROPERTY" } ], "filterBranchType": "AND", "filterBranchOperator": "AND" } ], "filters": [], "filterBranchOperator": "OR", "filterBranchType": "OR" } } ``` ## Is Before or After another property (value or last updated) The example below compares the values where _Last activity date is <u>before</u> Latest Open Lead Date_. To filter for _Last activity date is <u>after</u> Latest Open Lead Date_: set the `operator` value to `IS_AFTER`. To filter for when the Latest Open Lead Date was updated: set the `referenceType` value to `UPDATED_AT`. ```json { "name": "Sample Property Before", "objectTypeId": "0-1", "processingType": "DYNAMIC", "filterBranch": { "filterBranches": [ { "filterBranches": [], "filters": [ { "property": "notes_last_updated", "operation": { "operator": "IS_BEFORE", "includeObjectsWithNoValueSet": false, "timePoint": { "timeType": "PROPERTY_REFERENCED", "timezoneSource": "CUSTOM", "zoneId": "US/Eastern", "property": "hs_latest_open_lead_date", "referenceType": "VALUE" }, "endpointBehavior": "EXCLUSIVE", "propertyParser": "VALUE", "type": "TIME_POINT", "operationType": "TIME_POINT" }, "filterType": "PROPERTY" } ], "filterBranchType": "AND", "filterBranchOperator": "AND" } ], "filters": [], "filterBranchType": "OR", "filterBranchOperator": "OR" } } ``` ## Refine by operation There are two types of refine by operations that can be used in certain filters: - `pruningRefineBy`: refine the data set to a particular timeframe. - `coalescingRefineBy`: determines whether the record **PASSES** the filter the number of times defined. Filters only support up to one refine by operation at a time, even if some filters allow for both a coalescing and pruning refine by operation to be passed in. This is enforced by HubSpot's API. ## Pruning Refine By operations Pruning refine by operations are used to narrow down the dataset that will be used for filter evaluation by refining the dataset to a particular timeframe. Pruning refine by operations are classified into two types: relative and absolute. - **Relative**: narrow the dataset down based on a time offset of a number of days or weeks in the past or in the future. - **Absolute**: narrow the dataset down based on the data being before or after a specific timestamp For both relative and absolute refine by operations, there are ranged and comparative derivatives. ## Absolute Comparative timestamp Used to narrow the relevant dataset down based on whether the timestamp of the data being evaluated is before or after a specific timestamp as defined by the refine by operation. ```json { "type": "ABSOLUTE_COMPARATIVE", "comparison": "BEFORE", "timestamp": 1698915170126 } ``` | Parameter | Description | | --- | --- | | `comparison` | Whether the data's timestamp is before or after the defined `timestamp`. Values include:`BEFORE`, `AFTER` | ## Absolute Ranged timestamp Used to narrow the relevant dataset down based on whether the timestamp of the data being evaluated is between or is not between an upper and lower bound timestamp as defined by the refine by operation. ```json { "type": "ABSOLUTE_RANGED", "rangeType": "BETWEEN", "lowerTimestamp": 1698915170126, "upperTimestamp": 1682406908449 } ``` | Parameter | Description | | --- | --- | | `rangeType` | Whether the data's timestamp is between or not between the defined `lowerTimestamp`and `upperTimestamp`. Values include:`BETWEEN`, `NOT_BETWEEN` | ## Relative Comparative timestamp Used to narrow the relevant dataset down based on whether the timestamp of the data being evaluated is before or after a certain number of days or weeks in the past or future relative to the current timestamp as defined by the refine by operation. ```json { "type": "RELATIVE_COMPARATIVE", "comparison": "BEFORE", "timeOffset": [ "offsetDirection": "PAST", "timeUnit": "DAYS", "amount": 4 ], } ``` | Parameter | Description | | --- | --- | | `comparison` | Whether the data's timestamp is before or after the defined `timeOffset` values. Values include: `BEFORE`, `AFTER` | | `offsetDirection` | Values include:`PAST`, `FUTURE` | | `timeUnit` | Values include:`DAYS`, `WEEKS` | | `amount` | A number value. | ## Relative Ranged timestamp Used to narrow the relevant dataset down based on whether the timestamp of the data being evaluated is between or is not between an upper and lower bound time offset relative to the current timestamp. The time offsets are a certain number of days or weeks in the past or the future as defined by the refine by operation. ```json { "type": "RELATIVE_RANGED", "rangeType": "BETWEEN", "timeOffset": [ "offsetDirection": "PAST", "timeUnit": "DAYS", "amount": 4 ], } ``` | Parameter | Description | | --- | --- | | `rangeType` | Whether the data's timestamp is between or not between the defined `timeOffset` values. Values include: `BETWEEN`, `NOT_BETWEEN` | | `offsetDirection` | Values include:`PAST`, `FUTURE` | | `timeUnit` | Values include:`DAYS`, `WEEKS` | | `amount` | A number value. | ## Coalescing Refine By operations Coalescing refine by operations are used once the filter criteria has been applied to the relevant dataset. The only coalescing refine by operation supported by the Lists API is a “number of occurrences” operation that determines whether an object in the dataset **PASSED** the filter evaluation at least a minimum number of times and less than a maximum number of times. ```json { "type": "NUM_OCCURRENCES", "minOccurrences": 1, // Optional "maxOccurrences": 4 // Optional } ``` | Parameter | Description | | --- | --- | | `NUM_OCCURRENCES` | Used to determine whether an object in the relevant dataset PASSED the filter evaluation at least a minimum number of times (or zero times if a minimum is not provided) and at most a maximum number of times (or any number of times if a maximum is not provided) as defined by the refine by operation. | ## v1 list filters (legacy) If you're using the v1 lists API, the way that you filter lists is similar to the v3 API. However, there are minor differences in syntax and options available. Learn more about v1 list filters below. Starting May 30, 2025, the [legacy v1 Lists API](/guides/api/crm/lists/overview) will be sunset. If you were previously using the v1 Lists API, review the [guide below](#migrate-from-v1-to-v3-api-endpoints) to transition to the v3 API. Filter branches are AND / OR clauses that contain sets of filters and other filter branches. The only supported `filterType` for v1 lists is `PROPERTY`. Below, ```json "filterBranch": { "filterBranchType": "AND", "filters": [ { "property": "amount", "operation": { "value": 50, "operator": "IS_GREATER_THAN", "propertyType": "number" }, "filterType": "PROPERTY" } ], "filterBranches": [] } ``` ### All property types operations For any property, filters for whether the property value is known or unknown. Available operators are: `IS_KNOWN` and `IS_NOT_KNOWN`. ```json // alltypes filter { "filterType": "PROPERTY", "property": "any_property", "operation": { "propertyType": "alltypes", "operator": "IS_KNOWN" } } ``` ### String operations Operations for string type properties. Available operators are: `IS_EQUAL_TO`, `IS_NOT_EQUAL_TO`, `CONTAINS`, `DOES_NOT_CONTAIN`, `STARTS_WITH`, `ENDS_WITH`, `HAS_EVER_BEEN_EQUAL_TO`, `HAS_NEVER_BEEN_EQUAL_TO`, `HAS_EVER_CONTAINED`, `HAS_NEVER_CONTAINED`. ```json // string property filter { "filterType": "PROPERTY", "property": "string_property_name", "operation": { "propertyType": "string", "operator": "CONTAINS", "value": "def" } } ``` ### Multi-string operations Operations for multiple string values. Available operators are: `IS_EQUAL_TO`, `IS_NOT_EQUAL_TO`, `CONTAINS`, `DOES_NOT_CONTAIN`, `STARTS_WITH`, `ENDS_WITH`. ```json // multi-string property filter { "filterType": "PROPERTY", "property": "string_property_name", "operation": { "propertyType": "multistring", "operator": "CONTAINS", "values": ["def", "abc", "123"] } } ``` ### Number operations Operations for number type properties. Available operators are: `IS_EQUAL_TO`, `IS_NOT_EQUAL_TO`, `CONTAINS`, `DOES_NOT_CONTAIN`, `STARTS_WITH`, `ENDS_WITH`, `HAS_EVER_BEEN_EQUAL_TO`, `HAS_NEVER_BEEN_EQUAL_TO`, `IS_BETWEEN`, `IS_NOT_BETWEEN`. ```json // number property filter { "filterType": "PROPERTY", "property": "number_property_name", "operation": { "propertyType": "number", "operator": "IS_GREATER_THAN", "value": 100 } } ``` To filter for ranges, include a `lowerBound` and `upperBound`. ```json // number property filter { "filterType": "PROPERTY", "property": "datetime_property_name", "operation": { "propertyType": "number-ranged", "operator": "IS_BETWEEN", "lowerBound": 0, "upperBound": 10 } } ``` ### Boolean operations Operations for boolean type properties. Can only filter for a `value` of `true` or `false`. Available operators are: `IS_EQUAL_TO`, `IS_NOT_EQUAL_TO`, `HAS_EVER_BEEN_EQUAL_TO, HAS_NEVER_BEEN_EQUAL_TO`. ```json // boolean property filter { "filterType": "PROPERTY", "property": "boolean_property_name", "operation": { "propertyType": "bool", "operator": "IS_EQUAL_TO", "value": true } } ``` ### Enumeration operations Operations for enumeration type properties. Available operators are: `IS_EQUAL_TO`, `IS_NOT_EQUAL_TO`, `HAS_EVER_BEEN_ANY_OF, HAS_NEVER_BEEN_ANY_OF`. ```json // enumeration property filter { "filterType": "PROPERTY", "property": "enumeration_property_name", "operation": { "propertyType": "enumeration", "operator": "IS_ANY_OF", "values": ["abc", "def", "xyz"] } } ``` ### Datetime operations There are five different operation types for datetime properties, as set by the `propertyType` field: - `datetime`: compares the property value to a specific datetime stamp. Available operators are: `IS_EQUAL_TO`, `IS_BEFORE_DATE` (millisecond of day's start), `IS_AFTER_DATE` (last millisecond of day's end). ```json // datetime property filter { "filterType": "PROPERTY", "property": "datetime_property_name", "operation": { "propertyType": "datetime", "operator": "IS_EQUAL_TO", "timestamp": 1504703360618 } } ``` - `datetime-comparative`: compares the property value to another other datetime property on the contact record. Available operators are: `IS_BEFORE`, `IS_AFTER`. ```json // datetime comparative property filter { "filterType": "PROPERTY", "property": "datetime_property_name", "operation": { "propertyType": "datetime-comparative", "operator": "IS_AFTER", "comparisonPropertyName": "other_datetime_property" } } ``` - `datetime-ranged`: compares the property value to a specific timestamp range. Available operators are: `IS_BETWEEN`, `IS_NOT_BETWEEN`. ```json // datetime range property filter { "filterType": "PROPERTY", "property": "datetime_property_name", "operation": { "propertyType": "datetime-ranged", "operator": "IS_BETWEEN", "lowerBoundTimestamp": 1, "upperBoundTimestamp": 2 } } ``` - `datetime-rolling`: compares the property value to a rolling number of days. Available operators are: `IS_LESS_THAN_X_DAYS_AGO`, `IS_MORE_THAN_X_DAYS_AGO, IS_LESS_THAN_X_DAYS_FROM_NOW`, `IS_MORE_THAN_X_DAYS_FROM_NOW`. ```json // datetime rolling property filter { "filterType": "PROPERTY", "property": "datetime_property_name", "operation": { "propertyType": "datetime-rolling", "operator": "IS_LESS_THAN_X_DAYS_AGO" "numberOfDays": 5, } } ``` - `rolling-property-updated`: compares the last time the property was updated to a rolling number of days. Available operators are: `UPDATED_IN_LAST_X_DAYS`, `NOT_UPDATED_IN_LAST_X_DAYS`. ```json // datetime rolling property filter { "filterType": "PROPERTY", "property": "datetime_property_name", "operation": { "propertyType": "rolling-property-updated", "operator": "UPDATED_IN_LAST_X_DAYS", "numberOfDays": 3 } } ``` # Lists API @ Learn how to create and manage lists using the Lists API. $ guides & api/crm/lists --- # Lists Lists are a collection of records of the same object that can be used for record segmentation, filtering, and grouping to serve your business needs. The v3 Lists API allows you to create, edit, and fetch lists, as well as convert active lists into static lists. A list consists of a list definition and list memberships: - **List definition**: stores essential information about the list. - **List memberships**: mappings between the list and object record. The [legacy v1 lists API](/reference/api/crm/lists/v1-contacts) will be [sunset on May 30th, 2025](https://developers.hubspot.com/changelog/upcoming-sunset-v1-lists-api). If you're using the v1 lists API, review the [guide below](#migrate-from-v1-to-v3-api-endpoints) to transition to the v3 API. ## List processing types There are three types of list processing types: `MANUAL`, `DYNAMIC`, and `SNAPSHOT`. - `MANUAL`: this processing type indicates that object records can only be added to or removed from the list via manual actions by the user or API call. There is no list processing or list membership management done in the background by HubSpot's systems. This type of list is helpful for when you need a set list of records that won't change unless manually updated. - `DYNAMIC`: this processing type gives the possibility to specify [list filters](/guides/api/crm/lists/lists-filters) to match records that will become list members. This type of list is processed in the background by HubSpot to ensure that the list only contains records that match the list's filters. Whenever a record changes, it is reevaluated against the list's filters and either added to or removed from it. This type of list is helpful for when you want to keep a running list that you expect to change over time. - `SNAPSHOT`: [list filters](/guides/api/crm/lists/lists-filters) are specified at the time of list creation. After initial processing is completed, records can only be added to or removed from the list by manual actions. This type of list is helpful for when you want to create a list of records based on specific criteria, but don't want that list to change automatically after initial processing. ## Create a list To create a list, make a `POST` request to `/crm/v3/lists`. In the request body, you must include the following fields: `name`, `objectTypeId`, and `processingType`. The `filterBranch` parameter is optional, and can be included to create branching logic for `DYNAMIC` and `SNAPSHOT` type lists. Learn more about [configuring list filters and branches](/guides/api/crm/lists/lists-filters). Once created, a `listId` (the ILS list ID) will be generated. This ID is used for future updates and modifications. ```json // Example request body { "name": "My static list", "objectTypeId": "0-1", "processingType": "MANUAL" } ``` ## Retrieve lists Depending on your use case, there are multiple ways to retrieve lists:A list can be retrieved by using either the [ILS list ID](https://knowledge.hubspot.com/lists/lists-faq) or the name and object type for the list. - To retrieve a list by name, make a `GET` request to `/crm/v3/lists/object-type-id/{objectTypeId}/name/{listName}`. The `objectTypeId` is the ID that corresponds to the type of object stored by the list. See the [full list of object type IDs](/guides/api/crm/understanding-the-crm#object-type-ids). - To retrieve an individual list by [ILS list ID](https://knowledge.hubspot.com/lists/lists-faq), make a `GET` request to `/crm/v3/lists/{listId}`. Learn more about finding the ILS list ID below. - To retrieve multiple listsby ILS list ID, make a `GET` request to `/crm/v3/lists` and include a `listIds` query parameter for each list. For example: `?listIds=940&listIds=938`. When retrieving lists, you can include a query parameter of `includeFilters=true` to return list filter definitions in the response. To find a list's ILS list ID, you can navigate to the lists tool in HubSpot, then hover over the **list** and click **Details**. Learn more about [viewing lists](https://knowledge.hubspot.com/lists/create-active-or-static-lists#view-and-edit-a-list). You can also [search for lists](#search-for-a-list) by other criteria, then view the list's ID in the response. ## Search for a list You can search for lists by making a `POST` request to `/crm/v3/lists/search`. In the request body, you'll specify the criteria that you want to search by. For example, to search for lists that contain specific words in their name, you can include the `query` field. Or, to search for lists of a specific processing type, include a `processingTypes` array with each of the processing types you want to search by. For example, to search for all static lists that contain "HubSpot" in the name, your request body would be as follows: ```json // Example request body { "query": "HubSpot", "processingTypes": ["MANUAL"] } ``` ## Update lists To update a list name, make a `PUT` request to `/crm/v3/lists/{listId}/update-list-name` with the `listName` query parameter. If the list with the provided ILS list ID exists, then its name will be updated to the provided `listName`. The `listName` must be unique amongst all other public lists in the portal. You can also include a query parameter of `includeFilters=true` to return list filter definitions in the response. ### Update a list filter branch To update a `DYNAMIC` list's [filter branches](/guides/api/crm/lists/lists-filters), make a `PUT` request to `/crm/v3/lists/{listId}/update-list-filters`. In the request body, include the new filter branch definition. This A [list filter](/guides/api/crm/lists/lists-filters) branch can be updated by sending a `PUT` request to `/crm/v3/lists/{listId}/update-list-filters` with a request body containing the new filter branch definition. If the list with the provided ILS list ID exists, then its filter branch definition will be updated to the provided filter branch. Once the filter branch is updated, the list will begin processing its new memberships. ## Delete and restore a list To delete a list, make a `DELETE` request to `/crm/v3/lists/{listId}`. Once deleted, lists can be restored within 90 days of deletion by making a `PUT` request to `/crm/v3/lists/{listID}/restore`. Lists deleted more than 90 days ago <u>cannot</u> be restored. ## Manage list membership To view and manage records included in a list, you can use the `/memberships/` endpoints below. List membership endpoints can only be used on `MANUAL` or `SNAPSHOT` list processing types. `DYNAMIC` lists will add and remove records based on the filter criteria set. ### Add records to an existing list To add records to an existing list, make a `PUT` request to `/crm/v3/lists/{listId}/memberships/add` with a list of `recordID`s in the request body. To add records from one list to another, make a `PUT` request to ​`/crm/v3​/lists/{listId}/memberships​/add-from​/{sourceListId}`, where the `sourceListId` is the list you're retrieving the records from. You can move a limit of 100,000 records at a time. ### View records in an existing list To view all records in an existing list, make a `GET` request to `/crm/v3/lists/{listId}/memberships`. This returns all members of a list ordered by `recordId`. ### Delete records from an existing list To remove all records from an existing list, make a `DELETE` request to `/crm/v3/lists/{listId}/memberships`. This will <u>not</u> delete the list from your account. To remove specific records from an existing list, make a `PUT` request to `/crm/v3/lists/{listId}/memberships/remove` with a list of `recordID`s in the request body. ## Convert lists from active to static You can [convert existing active lists into static lists](https://knowledge.hubspot.com/lists/convert-active-lists-to-static-lists) by scheduling the conversion for a specific date or based on inactivity. You can use the lists endpoints to schedule conversions, retrieve scheduled or past conversions, and delete scheduled conversions. ### Schedule or update a list conversion To schedule a conversion or update an existing scheduled conversion, make a `PUT` request to `/crm/v3/lists/{listId}/schedule-conversion`. In the request body, include one of the following `conversionType` values and the type's related fields: - `CONVERSION_DATE`: schedules the conversion for a specific date. Include `year`, `month`, and `day` fields to specify the desired date. This date must be in the future. - `INACTIVITY`: schedules the conversion if the list hasn't been active for a set amount of time, based on when the last record was added or removed. Include the `timeUnit` field to specify the unit of time (`DAY`, `WEEK`, or `MONTH`) and the `offset` field to specify the amount of time after which the list is considered inactive. Only one `timeUnit` can be specified and the `offset` value must be positive. For example, to schedule an active list to be converted to a static list on January 31, 2025, your request would look like the following: ```json { "conversionType": "CONVERSION_DATE", "year": 2025, "month": 1, "day": 31 } ``` To schedule an active list to be converted to a static list after five days of inactivity, your request would look like the following: ```json { "conversionType": "INACTIVITY", "timeUnit": "DAY", "offset": 5 } ``` ### Retrieve a list conversion To retrieve information about a list's conversion, make a `GET` request to `/crm/v3/lists/{listId}/schedule-conversion`. The response will include the `requestedConversionTime` object with the `conversionType` ​and the relevant fields for that type. If the list was already converted, the `convertedAt` field will be returned with the timestamp of the conversion. For example, for a list that completed a conversion, your response would look similar to the following: ```json { "listId": "35", "requestedConversionTime": { "conversionType": "CONVERSION_DATE", "year": 2025, "month": 1, "day": 1 }, "convertedAt": "2025-01-01T05:20:34.002Z" } ``` ### Delete a scheduled list conversion To delete a conversion, make a `DELETE` request to `/crm/v3/lists/{listId}/schedule-conversion`. If the conversion does not exist, a 404 will be returned. ## Migrate from v1 to v3 API endpoints If you were previously using any of the v1 list endpoints, you can migrate over to the equivalent endpoints detailed in the sections below. ### Get static lists To get a static list, make a `POST` request to `/crm/v3/lists/search` and include `SNAPSHOT` and `MANUAL` within an array provided as the `processingTypes` parameter in your request body. ```json // Example request body for POST request to /crm/v3/lists/search { "additionalProperties": [ "hs_is_public", "hs_is_read_only", "hs_is_limit_exempt", "hs_all_team_ids", "hs_folder_id", "hs_folder_name" ], "offset": 0, "processingTypes": ["MANUAL", "SNAPSHOT"] } ``` ### Get dynamic lists To get a dynamic list, make a `POST` request to `/crm/v3/lists/search` and include `DYNAMIC` within an array provided as the processingTypes parameter in your request body. ```json // Example request body for POST request to /crm/v3/lists/search { "additionalProperties": [ "hs_is_public", "hs_is_read_only", "hs_is_limit_exempt", "hs_all_team_ids", "hs_folder_id", "hs_folder_name" ], "offset": 0, "processingTypes": ["DYNAMIC"] } ``` ### Get a batch of lists by list ID To get a batch of lists by the listIds, make a `POST` request to `/crm/v3/lists/search`. In the request, include the desired list IDs in the `listIds` parameter and specify any additional properties. The response will not include any filter branches. ```json // Example request body for POST request to /crm/v3/lists/search { "additionalProperties": [ "hs_is_public", "hs_is_read_only", "hs_is_limit_exempt", "hs_all_team_ids", "hs_folder_id", "hs_folder_name" ], "offset": 0, "listIds": ["42", "51"] } ``` To include filters in your response, make a `GET` request to `/crm/v3/lists`. In the request, append the query parameters `includeListFilters=true` and the desired list IDs as the `listIds` parameter. E.g. `/crm/v3/lists?includeFilters=true&listIds=42&listIds=51` ### Get recent list members with properties First, make a `GET` request to `/crm/v3/lists/{listId}/memberships/join-order` to get the record IDs of the list members. Then, make a `POST` request to `/crm/v3/objects/{object}/search` for the specific `objectTypeId` and include the record IDs in the `values` parameter. ```json // Example request body for POST request to /crm/v3/objects/{object}/search { "properties": [ "firstname", "lastname", "email", "hs_object_id", "createdate", "lastmodifieddate", "hs_all_accessible_team_ids" ], "filterGroups": [ { "filters": [ { "propertyName": "hs_object_id", "operator": "IN", "values": ["808431983", "802539655", "101"] } ] } ] } ``` ### Get all/recently modified records with properties Use the CRM search endpoint to search for records in your HubSpot account. To get all records, make a `POST` request to `/crm/v3/objects/{object}/search` with the object you want to search for. ```json // Example POST request to /crm/v3/objects/contacts/search { "properties": [ "firstname", "lastname", "email", "hs_object_id", "createdate", "lastmodifieddate", "hs_all_accessible_team_ids" ] } ``` ```json // Example POST request to /crm/v3/objects/deals/search { "properties": ["hs_object_id", "createdate", "dealstage", "lastmodifieddate"] } ``` To get recently modified records, make a `POST` request to `/crm/v3/objects/{object}/search` and filter by `lastmodifieddate`. ```json // Example POST request to /crm/v3/objects/contacts/search { "properties": [ "firstname", "lastname", "email", "hs_object_id", "createdate", "lastmodifieddate" ], "filterGroups": [ { "filters": [ { "propertyName": "lastmodifieddate", "operator": "GT", "value": "2024-02-22" } ] } ] } ``` ## Migrate list IDs from v1 to v3 endpoints Contact lists have two list IDs: a legacy list ID that corresponds with the legacy v1 lists API endpoint, and one that corresponds with the v3 lists API endpoint. As the [legacy v1 lists API](/reference/api/crm/lists/v1-contacts) will be sunset starting May 30th, 2025, HubSpot API users must migrate their legacy v1 list IDs to the v3 list IDs. To do so, use the ID mapping endpoints to fetch the v3 list ID (`listId`) from one or more v1 list IDs (`legacyListId`). The ID mapping endpoints will be sunset on May 30th, 2025. To fetch mappings one at a time, make a `GET` request to `/crm/v3/lists/idmapping?legacyListId=<legacyListId>` with the v1 list ID in the `legacyListID` parameter. ```json // Example response from a GET request to /crm/v3/lists/idmapping?legacyListId=64 { "listId": "61", "legacyListId": "64" } ``` To fetch multiple ID mappings in one batch, make a `POST` request to `/crm/v3/lists/idmapping` and include the legacy list ID values within an array. The limit on this API is 10, 000 entries. ```json // Example POST request to /crm/v3/lists/idmapping ["64", "33", "22566"] // Response { "missingLegacyListIds": [ "22566" ], "legacyListIdsToIdsMapping": [ { "listId": "61", "legacyListId": "64" }, { "listId": "38", "legacyListId": "33" } ] } ``` # Object library @ Learn how to check whether or not objects in the object library are activated for use in a HubSpot account. $ guides & api/crm --- # Object Library As objects are added to the [object library](https://knowledge.hubspot.com/data-management/data-model-templates) in HubSpot, you can use the object library enablement endpoints to check whether or not an object is activated for use in a HubSpot account. The endpoints can check the status of an object, but you can only [activate or deactivate an object from within HubSpot](https://knowledge.hubspot.com/data-management/data-model-templates#view-the-object-library). If an object is enabled, an app will need the corresponding object [scopes](https://developers.hubspot.com/docs/guides/apps/authentication/scopes#list-of-available-scopes) to access and manage records for the object. Learn more about creating and managing records [using the object APIs](https://developers.hubspot.com/docs/guides/api/crm/using-object-apis). ## Retrieve enablement statuses of all objects in the object library To check if all objects in the object library are activated in the account, make a `GET` request to `crm/v3/object-library/enablement`. Only objects which can be [activated or deactivated](https://knowledge.hubspot.com/data-management/data-model-templates#view-the-object-library) will be returned (i.e. contacts, companies, deals, and tickets will not be included). In your response, the `enablementByObjectTypeId` object will be returned with `objectTypeId` values and a value of `true` or `false` for each. If the value is `true`, the object is activated for use in the account. If the value is `false`, the object is deactivated. You can view each object's `objectTypeId` value in [the table in this article](https://developers.hubspot.com/docs/guides/api/crm/understanding-the-crm#object-type-ids). For example, the following response shows an account where appointments and services are activated, but courses and listings are not. ```json //Example response GET crm/v3/object-library/enablement { "enablementByObjectTypeId": { "0-420": false, "0-421": true, "0-162": true, "0-410": false } } ``` ## Retrieve an object's enablement status To check if a specific object is activated, make a `GET` request to `crm/v3/object-library/enablement/{objectTypeId}`. Refer to [the table in this article](https://developers.hubspot.com/docs/guides/api/crm/understanding-the-crm#object-type-ids) to find an object's `objectTypeId`. The object's `enablement` value will be returned as `true` or `false`. If the value is `true`, the object is activated for use in the account. If the value is `false`, the object is deactivated. For example, to check if appointments are activated in an account, make a `GET` request to `crm/v3/object-library/enablement/0-421`. If appointments are activated, the response will look like: ```json //Example response GET crm/v3/object-library/enablement/0-421 { "enablement": true } ``` # CRM API | Companies @ Company records store data about businesses. The companies endpoints allow you to manage this data and sync it between HubSpot and other systems.  $ guides & api/crm/objects --- In HubSpot, companies store information about the organizations that interact with your business. The companies endpoints allow you to manage create and manage company records, as well as sync company data between HubSpot and other systems. Learn more about objects, records, properties, and associations APIs in the [Understanding the CRM](/guides/api/crm/understanding-the-crm) guide. For more general information about objects and records in HubSpot, [learn how to manage your CRM database](https://knowledge.hubspot.com/get-started/manage-your-crm-database). ## Create companies To create new companies, make a `POST` request to `/crm/v3/objects/companies`. In your request, include your company data in a properties object. You can also add an associations object to associate your new company with existing records (e.g., contacts, deals), or activities (e.g., meetings, notes). ### Properties Company details are stored in company properties. There are [default HubSpot company properties](https://knowledge.hubspot.com/properties/hubspot-crm-default-company-properties), but you can also [create custom properties](https://knowledge.hubspot.com/properties/create-and-edit-properties). When creating a new company, you should include <u>at least one</u> of the following properties in your request: `name` or `domain`. It is recommended to always include `domain`, because domain names are the [primary unique identifier](https://knowledge.hubspot.com/records/deduplication-of-records#automatic-deduplication-in-hubspot) to avoid duplicate companies in HubSpot. If a company has [multiple domains](https://knowledge.hubspot.com/records/add-multiple-domain-names-to-a-company-record), you can add them through the API by using the `hs_additional_domains` field with semicolons separating each domain. For example: `"hs_additional_domains" : "domain.com; domain2.com; domain3.com"`. To view all available properties, you can retrieve a list of your account's company properties by making a `GET` request to `/crm/v3/properties/companies`. Learn more about the the [properties API](/guides/api/crm/properties). If you've included `lifecyclestage` in your request, values must refer to the lifecycle stage's internal name. The internal names of default stages are text values, and do not change even if you edit the stage's [label](https://knowledge.hubspot.com/properties/create-and-edit-properties#:~:text=the%20properties%20settings.-,Label/Name%3A,-enter%20a%20unique)(e.g., `subscriber` or `marketingqualifiedlead`). The internal names of [custom stages](https://knowledge.hubspot.com/object-settings/create-and-customize-lifecycle-stages) are numeric values. You can find a stage's internal ID in your [lifecycle stage settings,](https://knowledge.hubspot.com/object-settings/create-and-customize-lifecycle-stages#:~:text=To%20edit%20a%20lifecycle%20stage%2C%20hover%20over%20the%20stage%20and%20click%20Edit.%20In%20the%20right%20panel%2C%20edit%20the%20Stage%20name%2C%20then%20click%20Edit%20lifecycle%20stage%20to%20confirm.%20Click%20the%20code%20codcode%20icon%20to%20view%20the%20stage%27s%20internal%20ID%2C%20which%20is%20used%20by%20integrations%20and%20APIs.) or by retrieving the lifecycle stage property via API. For example, to create a new company, your request may look similar to the following: ```json ///Example request body { "properties": { "name": "HubSpot", "domain": "hubspot.com", "city": "Cambridge", "industry": "Technology", "phone": "555-555-555", "state": "Massachusetts", "lifecyclestage": "51439524" } } ``` ### Associations When creating a new company, you can also associate the company with [existing records](https://knowledge.hubspot.com/records/associate-records) or [activities](https://knowledge.hubspot.com/records/associate-activities-with-records) in an associations object. For example, to associate a new company with an existing contact and email, your request would look like the following: ```json ///Example request body { "properties": { "name": "HubSpot", "domain": "hubspot.com", "city": "Cambridge", "industry": "Technology", "phone": "555-555-555", "state": "Massachusetts", "lifecyclestage": "51439524" }, "associations": [ { "to": { "id": 101 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 280 } ] }, { "to": { "id": 556677 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 185 } ] } ] } ``` In the associations object, you should include the following: | Parameter | Description | | --- | --- | | `to` | The record or activity you want to associate with the company, specified by its unique `id` value. | | `types` | The type of the association between the company and the record/activity. Include the `associationCategory`and `associationTypeId`. Default association type IDs are listed [here](/guides/api/crm/associations/associations-v4#association-type-id-values), or you can retrieve the value for custom association types (i.e. labels) via the [associations API](/guides/api/crm/associations/associations-v4#retrieve-association-types). | ## Retrieve companies You can retrieve companies individually or in batches. - To retrieve an individual company, make a `GET` request to `/crm/v3/objects/companies/{companyId}`. - To request a list of all companies, make a `GET` request to `/crm/v3/objects/companies`. For these endpoints, you can include the following query parameters in the request URL: | Parameter | Description | | --- | --- | | `properties` | A comma separated list of the properties to be returned in the response. If the requested company doesn't have a value for a property, it will not appear in the response. | | `propertiesWithHistory` | A comma separated list of the current and historical properties to be returned in the response. If the requested company doesn't have a value for a property, it will not appear in the response. | | `associations` | A comma separated list of objects to retrieve associated IDs for. Any specified associations that don't exist will not be returned in the response. Learn more about the [associations API.](/guides/api/crm/associations/associations-v4) | - To retrieve a batch of specific companies by record ID or a [custom unique identifier property](/guides/api/crm/properties#create-unique-identifier-properties), make a `POST` request to `crm/v3/objects/companies/batch/read`. The batch endpoint <u>cannot</u> retrieve associations. Learn how to batch read associations with the [associations API](/guides/api/crm/associations/associations-v4). For the batch read endpoint, you can also use the optional `idProperty` parameter to retrieve companies by a custom [unique identifier property](/guides/api/crm/properties#create-unique-identifier-properties). By default, the `id` values in the request refer to the record ID (`hs_object_id`), so the `idProperty` parameter is not required when retrieving by record ID. To use a custom unique value property to retrieve companies, you must include the `idProperty` parameter. For example, to retrieve a batch of companies, your request could look like either of the following: ```json ///Example request body with record ID { "properties": ["name", "domain"], "inputs": [ { "id": "56789" }, { "id": "23456" } ] } ``` ```json ///Example request body with a unique value property { "properties": ["name", "domain"], "idProperty": "uniquepropertyexample", "inputs": [ { "id": "abc" }, { "id": "def" } ] } ``` To retrieve companies with current and historical values for a property, your request could look like: ```json ///Example request body with record ID (current and historical values) { "propertiesWithHistory": ["name"], "inputs": [ { "id": "56789" }, { "id": "23456" } ] } ``` ## Update companies You can update companies individually or in batches. For existing companies, the company's record ID is a unique value that you can use to update the company via API. To update an individual company by its company ID, make a `PATCH` request to `/crm/v3/objects/companies/{companyId}`, and include the data you want to update. If updating the `lifecyclestage` property, you can only set the value <u>forward</u> in the stage order. To set the lifecycle stage backward, you'll first need to clear the record's existing lifecycle stage value. The value can be [cleared manually](https://knowledge.hubspot.com/records/update-a-property-value-for-a-record), or may be automatically cleared via a [workflow](https://knowledge.hubspot.com/records/change-record-lifecycle-stages-in-bulk) or an integration that syncs contact data. ### Associate existing companies with records and activities To associate a company with other CRM records or an activity, make a `PUT` request to `/crm/v3/objects/companies/{companyId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}`. To retrieve the `associationTypeId` value, refer to [this list](/guides/api/crm/associations/associations-v4#association-type-id-values) of default values, or make a `GET` request to `/crm/v4/associations/{fromObjectType}/{toObjectType}/labels`. Learn more about associating records with the [associations API](/guides/api/crm/associations/associations-v4). ### Remove an association To remove an association between a company and a record or activity, make a `DELETE` request to the following URL: `/crm/v3/objects/companies/{companyId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}`. ## Pin an activity on a company record You can pin an activity on a company record via API by including the `hs_pinned_engagement_id` field in your request. In the field, include the `id` of the activity to pin, which can be retrieved via the [engagements APIs](/guides/api/crm/engagements/engagement-details). You can pin one activity per record, and the activity must already be associated with the company prior to pinning. To set or update a company's pinned activity, your request could look like: ```json ///Example request body PATCH /crm/v3/objects/companies/{companyId} { "properties": { "hs_pinned_engagement_id": 123456789 } } ``` You can also create a company, associate it with an existing activity, and pin the activity in the same request. For example: ```json ///Example request body POST /crm/v3/objects/companies { "properties": { "domain": "example.com", "name": "Example Company", "hs_pinned_engagement_id": 123456789 }, "associations": [ { "to": { "id": 123456789 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 189 } ] } ] } ``` ## Delete companies You can delete companies individually or in batches, which will add the company to the recycling bin in HubSpot. You can later [restore the company within HubSpot](https://knowledge.hubspot.com/records/restore-deleted-records). To delete an individual company by its ID, make a `DELETE` request to `/crm/v3/objects/companies/{companyId}`. Learn more about batch deleting companies in the [reference documentation](/reference/api/crm/objects/companies). # CRM API | Contacts @ Contact records store information about individuals. The contacts endpoints allow you to manage this data and sync it between HubSpot and other systems. $ guides & api/crm/objects --- In HubSpot, contacts store information about the individual people that interact with your business. The contacts endpoints allow you to create and manage contact records in your HubSpot account, as well as sync contact data between HubSpot and other systems. Learn more about objects, records, properties, and associations APIs in the [Understanding the CRM](/guides/api/crm/understanding-the-crm) guide. For more general information about objects and records in HubSpot, [learn how to manage your CRM database](http://knowledge.hubspot.com/contacts-user-guide). ## Create contacts To create new contacts, make a `POST` request to `/crm/v3/objects/contacts`. In your request, include your contact data in a properties object. You can also add an associations object to associate your new contact with existing records (e.g., companies, deals), or activities (e.g., meetings, notes). ### Properties Contact details are stored in contact properties. There are [default HubSpot contact properties](https://knowledge.hubspot.com/properties/hubspots-default-contact-properties), but you can also [create custom contact properties](https://knowledge.hubspot.com/properties/create-and-edit-properties). When creating a new contact, you should include <u>at least one</u> of the following properties in your request: `email`, `firstname`, or `lastname`. It is recommended to always include `email`, because email address is the [primary unique identifier](https://knowledge.hubspot.com/records/deduplication-of-records#automatic-deduplication-in-hubspot) to avoid duplicate contacts in HubSpot. To view all available properties, you can retrieve a list of your account's contact properties by making a `GET` request to `/crm/v3/properties/contacts`. Learn more about the the [properties API](/guides/api/crm/properties). If you've included `lifecyclestage` in your request, values must refer to the lifecycle stage's internal name. The internal names of default stages are <u>text</u> values, and do not change even if you edit the stage's [label](https://knowledge.hubspot.com/properties/create-and-edit-properties#:~:text=the%20properties%20settings.-,Label/Name%3A,-enter%20a%20unique)(e.g., `subscriber` or `marketingqualifiedlead`). The internal names of [custom stages](https://knowledge.hubspot.com/object-settings/create-and-customize-lifecycle-stages)are <u>numeric</u> values. You can find a stage's internal ID in your [lifecycle stage settings,](https://knowledge.hubspot.com/object-settings/create-and-customize-lifecycle-stages#:~:text=To%20edit%20a%20lifecycle%20stage%2C%20hover%20over%20the%20stage%20and%20click%20Edit.%20In%20the%20right%20panel%2C%20edit%20the%20Stage%20name%2C%20then%20click%20Edit%20lifecycle%20stage%20to%20confirm.%20Click%20the%20code%20codcode%20icon%20to%20view%20the%20stage%27s%20internal%20ID%2C%20which%20is%20used%20by%20integrations%20and%20APIs.)or by retrieving the lifecycle stage property via API. For example, to create a new contact, your request may look similar to the following: ```json ///Example request body { "properties": { "email": "example@hubspot.com", "firstname": "Jane", "lastname": "Doe", "phone": "(555) 555-5555", "company": "HubSpot", "website": "hubspot.com", "lifecyclestage": "marketingqualifiedlead" } } ``` ### Associations When creating a new contact, you can also associate the contact with [existing records](https://knowledge.hubspot.com/records/associate-records) or [activities](https://knowledge.hubspot.com/records/associate-activities-with-records) by including an associations object. For example, to associate a new contact with an existing company and email, your request would look like the following: ```json ///Example request body { "properties": { "email": "example@hubspot.com", "firstname": "Jane", "lastname": "Doe", "phone": "(555) 555-5555", "company": "HubSpot", "website": "hubspot.com", "lifecyclestage": "marketingqualifiedlead" }, "associations": [ { "to": { "id": 123456 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 279 } ] }, { "to": { "id": 556677 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 197 } ] } ] } ``` In the associations object, you should include the following: | Parameter | Description | | --- | --- | | `to` | The record or activity you want to associate with the contact, specified by its unique `id` value. | | `types` | The type of the association between the contact and the record/activity. Include the `associationCategory`and `associationTypeId`. Default association type IDs are listed [here](/guides/api/crm/associations/associations-v4#association-type-id-values), or you can retrieve the value for custom association types (i.e. labels) via the [associations API](/guides/api/crm/associations/associations-v4#retrieve-association-types). | ## Retrieve contacts by record ID, email, or custom unique value property You can retrieve contacts individually or in batches. - To retrieve an individual contact, make a `GET` request to `/crm/v3/objects/contacts/{contactId} or` `/crm/v3/objects/contacts/{email}?idProperty=email`. - To request a list of all contacts, make a `GET` request to `/crm/v3/objects/contacts`. For these endpoints, you can include the following query parameters in the request URL: | Parameter | Description | | --- | --- | | `properties` | A comma separated list of the properties to be returned in the response. If the requested contact doesn't have a value for a property, it will not appear in the response. | | `propertiesWithHistory` | A comma separated list of the current and historical properties to be returned in the response. If the requested contact doesn't have a value for a property, it will not appear in the response. | | `associations` | A comma separated list of objects to retrieve associated IDs for. Any specified associations that don't exist will not be returned in the response. Learn more about the [associations API.](/guides/api/crm/associations/associations-v4) | - To retrieve a batch of specific contacts by record ID, email address, or a [custom unique identifier property](/guides/api/crm/properties#create-unique-identifier-properties), make a `POST` request to `crm/v3/objects/contacts/batch/read`. The batch endpoint <u>cannot</u> retrieve associations. Learn how to batch read associations with the [associations API](/guides/api/crm/associations/associations-v4). For the batch read endpoint, you can use the optional `idProperty` parameter to retrieve contacts by `email` or a custom [unique identifier property](/guides/api/crm/properties#create-unique-identifier-properties). By default, the `id` values in the request refer to the record ID (`hs_object_id`), so the `idProperty` parameter is not required when retrieving by record ID. If you're using `email` or a custom unique value property to retrieve contacts, you must include the `idProperty` parameter. For example, to retrieve a batch of contacts based on their record ID values, your request could look like the following (current values only, or current and historical values): ```json // Example request body with record ID (current values) { "properties": ["email", "lifecyclestage", "jobtitle"], "inputs": [ { "id": "1234567" }, { "id": "987456" } ] } ``` ```json // Example request body with record ID (current and historical values) { "propertiesWithHistory": ["lifecyclestage", "hs_lead_status"], "inputs": [ { "id": "1234567" }, { "id": "987456" } ] } ``` To retrieve contacts based on email address or a custom unique identifier property (e.g., a customer ID number unique for your business), your request would look like: ```json // Example request body with email address { "properties": ["email", "lifecyclestage", "jobtitle"], "idProperty": "email", "inputs": [ { "id": "lgilmore@thedragonfly.com" }, { "id": "sstjames@thedragonfly.com" } ] } ``` ```json // Example request body with a unique value property { "properties": ["email", "lifecyclestage", "jobtitle"], "idProperty": "internalcustomerid", "inputs": [ { "id": "12345" }, { "id": "67891" } ] } ``` ## Update contacts You can update contacts individually or in batches. To update <u>individual</u> contacts, you can use record ID (`id`) or the contact's email address (`email`). - To update an individual contact by its record ID, make a `PATCH` request to `/crm/v3/objects/contacts/{contactId}`, and include the data you want to update. - To update an individual contact by its email, make a `PATCH` request to `/crm/v3/objects/contacts/{email}?idProperty=email`, and include the data you want to update. For example: ```json // Example request body with record ID // PATCH /crm/v3/objects/contacts/123456789 { "properties": { "favorite_food": "burger", "jobtitle": "Manager", "lifecyclestage": "Customer" } } ``` If updating the `lifecyclestage` property, you can only set the value <u>forward</u> in the stage order. To set the lifecycle stage backward, you'll first need to clear the record's existing lifecycle stage value. The value can be [cleared manually](https://knowledge.hubspot.com/records/update-a-property-value-for-a-record), or may be automatically cleared via a [workflow](https://knowledge.hubspot.com/records/change-record-lifecycle-stages-in-bulk) or an integration that syncs contact data. To update contacts in <u>batches</u>, you can use the contacts' record ID values (`id`). To update multiple contacts, make a `POST` request to `/crm/v3/objects/contacts/batch/update`. In your request body, include each contact's record ID as the `id` ​and include the properties you want to update. For example: ```json // Example request body // POST /crm/v3/objects/contacts/batch/update { "inputs": [ { "id": "123456789", "properties": { "favorite_food": "burger" } }, { "id": "56789123", "properties": { "favorite_food": "Donut" } } ] } ``` ## Upsert contacts You can also batch create and update contacts at the same time using the upsert endpoint. For this endpoint, you can use `email` or a [custom unique identifier property](/guides/api/crm/properties#create-unique-identifier-properties). Following the request, if the contacts already exist, they'll be updated and if the contacts don't exist, they'll be created. To upsert contacts, make a `POST` request to `/crm/v3/objects/contacts/batch/upsert`. In your request body, include the `idProperty` parameter to identify whether you're using `email` or a custom unique identifier property. Include that property's value as the `id` ​and add the other properties you want to set or update. Partial upserts are not supported when using `email` as the `idProperty` for contacts. To complete a partial upsert, use a custom unique identifier property as the `idProperty` instead. For example, your request could look like the following: ```json // Example request body with email // POST /crm/v3/objects/contacts/batch/upsert { "inputs": [ { "properties": { "phone": "5555555555" }, "id": "test@test.com", "idProperty": "email" }, { "properties": { "phone": "7777777777" }, "id": "example@hubspot.com", "idProperty": "email" } ] } ``` ## Associate existing contacts with records or activities To associate a contact with other CRM records or an activity, make a `PUT` request to `/crm/v3/objects/contacts/{contactId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}`. To retrieve the `associationTypeId` value, refer to [this list](/guides/api/crm/associations/associations-v4#association-type-id-values) of default values, or make a `GET` request to `/crm/v4/associations/{fromObjectType}/{toObjectType}/labels`. Learn more about the [associations API.](/guides/api/crm/associations/associations-v4) ### Remove an association To remove an association between a contact and a record or activity, make a `DELETE` request to the following URL: `/crm/v3/objects/contacts/{contactID}/associations/{toObjectType}/{toObjectId}/{associationTypeId}`. ## Pin an activity on a contact record You can [pin an activity](https://knowledge.hubspot.com/records/pin-an-activity-on-a-record) on a contact record by including the `hs_pinned_engagement_id` field in your request. In the field, include the `id` of the activity to pin, which can be retrieved via the [engagements APIs](/guides/api/crm/engagements/engagement-details). You can pin one activity per record, and the activity must already be associated with the contact prior to pinning. To set or update a contact's pinned activity, your request could look like: ```json // Example request body PATCH /crm/v3/objects/contacts/{contactId} { "properties": { "hs_pinned_engagement_id": 123456789 } } ``` You can also create a contact, associate it with an existing activity, and pin the activity in the same request. For example: ```json // Example request body POST /crm/v3/objects/contacts { "properties": { "email": "example@hubspot.com", "firstname": "Jane", "lastname": "Doe", "phone": "(555) 555-5555", "hs_pinned_engagement_id": 123456789 }, "associations": [ { "to": { "id": 123456789 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 201 } ] } ] } ``` ## Delete contacts You can delete contacts individually or in batches, which will add the contact to the recycling bin in HubSpot. You can later [restore the contact within HubSpot](https://knowledge.hubspot.com/records/restore-deleted-records). To delete an individual contact by its ID, make a `DELETE` request to `/crm/v3/objects/contacts/{contactId}`. Learn more about batch deleting contacts in the [reference documentation](/reference/api/crm/objects/contacts). ## Additional emails Additional email addresses are used when a contact has more than one email. These can be added [manually on a contact record in HubSpot](https://knowledge.hubspot.com/records/add-multiple-email-addresses-to-a-contact) or can be added automatically following a [contact merge](https://knowledge.hubspot.com/records/merge-records#contact-merge-exceptions). Additional emails are still unique identifiers for contacts, so multiple contacts cannot have the same additional email addresses. To view additional emails for contacts, when you retrieve all or individual contacts, include the `properties` parameter with the properties `email` and `hs_additional_emails`. A contact's primary email address will be displayed in the `email` field and additional emails will be displayed in the `hs_additional_emails` field. ## Limits Batch operations are limited to 100 records at a time. For example, you cannot batch update more than 100 contacts in one request. There are also limits for [contacts and form submissions](https://developers.hubspot.com/docs#limits_contacts). # CRM | custom objects @ HubSpot custom objects allow organizations to represent and organize that data based on your business requirements with the custom objects API. $ guides & api/crm/objects --- In each HubSpot account, there are the standard CRM objects: contacts, companies, deals, and tickets. To represent and organize your CRM data based on your business needs, you can also create custom objects. You can [create a custom object](https://knowledge.hubspot.com/object-settings/create-custom-objects) in HubSpot, or use the custom objects API to define custom objects, properties, and associations to other CRM objects. Below, learn how to create and manage custom objects through the API, and see a [walkthrough of creating an example custom object](#custom-object-example). To learn more about creating custom objects, check out the following posts on the HubSpot developer blog: - [How to build scalable custom objects](https://developers.hubspot.com/blog/how-to-think-like-an-architect-by-building-scalable-custom-objects) - [How to build custom objects using private apps](https://developers.hubspot.com/blog/how-to-build-a-custom-object-using-private-apps) Custom objects are specific to each account, and depending on your subscription, there are limits on the number of custom objects you can create. Learn more about your limits in the [HubSpot Products & Services catalog](https://legal.hubspot.com/hubspot-product-and-services-catalog). ## Authentication methods You can create, read, and update custom objects using one of the following methods of authentication: - [OAuth](https://developers.hubspot.com/custom-objects-schema-pilot) - [Private app access tokens](/guides/apps/private-apps/overview#make-api-calls-with-your-app-s-access-token) As of November 30, 2022, HubSpot API Keys are being deprecated and are no longer supported. Continued use of HubSpot API Keys is a security risk to your account and data. During this deprecation phase, HubSpot may deactivate your key at any time. You should instead authenticate using a private app access token or OAuth. Learn more about [this change](https://developers.hubspot.com/changelog/upcoming-api-key-sunset) and how to [migrate an API key integration](/guides/apps/private-apps/migrate-an-api-key-integration-to-a-private-app) to use a private app instead. ## Create a custom object To create a custom object, you'll first need to define the object schema. The schema includes the object name, properties, and associations to other CRM objects. You can find the full schema request details in the _Object schema_ tab at the top of this article. You can also view a sample request in the [example walkthrough below](#custom-object-example). To create the custom object schema, make a `POST` request to `crm/v3/schemas`. In the request body, include definitions for your object schema, including its name, properties, and associations. When naming your custom object, keep the following in mind: - Once you create an object, its name and label cannot be changed. - The name can only contain letters, numbers, and underscores. - The first character of the name must be a letter. - Long labels may be cut off in certain parts of the product. Below, read about the required definitions for the object's properties and associations. ### Properties The properties you define in the request body will be used to store information on individual custom object records. You can have up to 10 [unique value properties](/guides/api/crm/understanding-the-crm#:~:text=Creating%20your%20own%20unique%20identifiers) for each custom object in your HubSpot account. You'll use your defined properties to populate the following property-based fields: - **requiredProperties:** the properties that are required when creating a new custom object record. - **searchableProperties:** the properties that are indexed for searching in HubSpot. - **primaryDisplayProperty:** the property used for naming individual custom object records. - **secondaryDisplayProperties:** the properties that appear on individual records under the primaryDisplayProperty. - The first property listed in `secondaryDisplayProperties` will be also added as a fourth filter on the object index page if it’s one of the following property types: - `string` - `number` - `enumeration` - `boolean` - `datetime` - To remove a display property from the UI, you'll need to first delete the property, then recreate it. By default, when creating properties through the schema request, property `type` is set to `string`, and the `fieldType` is set to `text`. Below are the values you can use to create different types of properties. | `type` | Description | Valid `fieldType` values | | --- | --- | --- | | `enumeration` | A string representing a set of options, separated by semicolons. | `booleancheckbox`, `checkbox`, `radio`, `select` | | `date` | An [ISO 8601 formatted value](https://en.wikipedia.org/wiki/ISO_8601) representing a specific day, month, and year. | `date` | | `dateTime` | An [ISO 8601 formatted value](https://en.wikipedia.org/wiki/ISO_8601) representing a specific day, month, year and time of day. The HubSpot app will not display the time of day. | `date` | | `string` | A plain text strings, limited to 65,536 characters. | `file`, `text`, `textarea` | | `number` | A number value containing numeric digits and at most one decimal. | `number` | | `fieldType` | Description | | --- | --- | | `booleancheckbox` | An input that will allow users to select one of either Yes or No. When used in a form, it will be displayed as a single checkbox. | | `checkbox` | A list of checkboxes that will allow a user to select multiple options from a set of options allowed for the property. | | `date` | A date value, displayed as a date picker. | | `file` | Allows for a file to be uploaded to a form. Stored and displayed as a URL link to the file. | | `number` | A string of numerals or numbers written in decimal or scientific notation. | | `radio` | An input that will allow users to select one of a set of options allowed for the property. When used in a form, this will be displayed as a set of radio buttons. | | `select` | A dropdown input that will allow users to select one of a set of options allowed for the property. | | `text` | A plain text string, displayed in a single line text input. | | `textarea` | A plain text string, displayed as a multi-line text input. | ### Associations HubSpot will automatically associate a custom object with the emails, meetings, notes, tasks, calls, and conversations objects. You can further associate your custom object with other standard HubSpot objects or other custom objects. When creating associations through the create schema request, identify standard objects using their name and custom objects using their `objectTypeId` value. _For example:_ ```json // Example associatedObjects array "associatedObjects": [ "CONTACT", "COMPANY", "TICKET", "DEAL", "2-3453932" ] ``` ## Retrieve existing custom objects To retrieve all custom objects, make a `GET` request to `/crm/v3/schemas`. To retrieve a specific custom object, make a `GET` request to one of the following endpoints: - `/crm/v3/schemas/{objectTypeId}` - `/crm/v3/schemas/p_{object_name}` - `/crm/v3/schemas/{fullyQualifiedName}`. You can find an object's `fullyQualifiedName` in its schema, which is derived from `p{portal_id}_{object_name}`. You can find your account's portal ID using the [account information API.](/guides/api/settings/account-information-api) For example, for an account with an ID of `1234` and an object named `lender`, your request URL could look like any of the following: - `https://api.hubapi.com/crm/v3/schemas/2-3465404` - `https://api.hubapi.com/crm/v3/schemas/p_lender` - [`https://api.hubapi.com/crm/v3/schemas/p1234_lende`](https://api.hubapi.com/crm/v3/schemas/p1234_lender) ## Retrieve custom object records You can also retrieve a custom object's records. - To retrieve a specific record by its record ID value, make a `GET` request to `crm/v3/objects/{objectType}/{recordId}`. For this endpoint, you can include the following query parameters in the request URL: | Parameter | Description | | --- | --- | | `properties` | A comma separated list of the properties to be returned in the response. If the requested custom object record doesn't have a value for a property, it will not appear in the response. | | `propertiesWithHistory` | A comma separated list of the current and historical properties to be returned in the response. If the requested custom object record doesn't have a value for a property, it will not appear in the response. | | `associations` | A comma separated list of objects to retrieve associated IDs for. Any specified associations that don't exist will not be returned in the response. Learn more about the [associations API.](/guides/api/crm/associations/associations-v4) | - To retrieve multiple records, make a `POST` request to `crm/v3/objects/{objectType}/batch/read`. The batch endpoint <u>cannot</u> retrieve associations. Learn how to batch read associations with the [associations API](/guides/api/crm/associations/associations-v4). In your request, you can retrieve records by their record ID (`hs_object_id`), or by a custom [unique identifier property](/guides/api/crm/properties#create-unique-identifier-properties). By default, the `id` values in the request refer to the record ID, so the `idProperty` parameter is not required when retrieving by record ID. To use a custom unique value property, you must include the `idProperty` parameter. For example, to retrieve a batch of custom object records, your request could look like either of the following: ```json ///Example request body for record ID { "properties": ["petname"], "inputs": [ { "id": "12345" }, { "id": "67891" } ] } ``` ```json ///Example request body for unique value property { "properties": ["petname"], "idProperty": "uniquepropertyexample", "inputs": [ { "id": "abc" }, { "id": "def" } ] } ``` To retrieve custom object records with current and historical values for a property, your request could look like: ```json ///Example request body for record ID (current and historical values) { "propertiesWithHistory": ["pet_owner"], "inputs": [ { "id": "12345" }, { "id": "67891" } ] } ``` ## Update existing custom objects To update an object's schema, make a `PATCH` request to `https://api.hubapi.com/crm/v3/schemas/{objectTypeId}`. Once your custom object is defined: - The object's name and labels (singular and plural) <u>cannot</u> be changed. - The `requiredProperties`, `searchableProperties`, `primaryDisplayProperty`, and `secondaryDisplayProperties` can be changed by updating the object's schema. To set a new property as a required, searchable, or display property, you need to create the property prior to updating the schema. - You can create and edit custom object properties either [in HubSpot](https://knowledge.hubspot.com/properties/create-and-edit-properties#view-and-edit-properties) or via the [properties API](/guides/api/crm/properties). ### Update associations To add other object associations to your custom object, make a `POST` request to `/crm/v3/schemas/_{objectTypeId}_/associations`. You can only associate your custom object with standard HubSpot objects (e.g. _contact_, _company_, _deal_, or _ticket_) or other custom objects. In the `toObjectTypeId` field, identify custom objects by their `objectTypeId` value and standard objects by their name. For example: ```json // Example association request body { "fromObjectTypeId": "2-3444025", "toObjectTypeId": "ticket", "name": "cat_to_ticket" } ``` ## Delete a custom object You can only delete a custom object after all object instances of that type are deleted. To delete a custom object, make a `DELETE` request to `/crm/v3/schemas/{objectType}`. If you need to create a new custom object with the same name as the deleted object, you must hard delete the schema by making a `DELETE` request to `/crm/v3/schemas/{objectType}?archived=true`. You can only delete a custom object type after all object instances of that type, associations, and custom object properties are deleted. ## Custom object example The following is a walkthrough of creating an example custom object. For full details of the requests shown, view the Object Definition tab at the top of the article. This walkthrough covers: 1. creating a custom object schema. 2. creating a custom object record. 3. associating a custom object record with a HubSpot contact. 4. creating a new association definition between the custom object and HubSpot ticket. 5. creating a new property definition. 6. updating the object schema (i.e. `secondaryDisplayProperties`) with the new property. *** **Goal:** a car dealership called CarSpot wants to store their inventory in HubSpot using a custom object. To track vehicle ownership and purchases, they'll associate cars with contact records. Along the way, they'll also track vehicle maintenance using HubSpot tickets and custom properties. ### Create the object schema CarSpot needs to create an object schema that can represent the following attributes as properties: 1. **Condition (new or used):** enumeration 2. **Date received at dealership:** date 3. **Year:** number 4. **Make:** string 5. **Model:** string 6. **VIN:** string (unique value) 7. **Color:** string 8. **Mileage:** number 9. **Price:** number 10. **Notes:** string They'll also add a description to provide context about how to use the object, and define an association between their custom object and the standard contacts object so that they can connect cars to potential buyers. With their data model finalized, they'll create the object schema by making a `POST` request to `/crm/v3/schemas` with the following request body: ```json // Example POST request to https://api.hubspot.com/crm/v3/schemas { "name": "cars", "description": "Cars keeps track of cars currently or previously held in our inventory.", "labels": { "singular": "Car", "plural": "Cars" }, "primaryDisplayProperty": "model", "secondaryDisplayProperties": ["make"], "searchableProperties": ["year", "make", "vin", "model"], "requiredProperties": ["year", "make", "vin", "model"], "properties": [ { "name": "condition", "label": "Condition", "type": "enumeration", "fieldType": "select", "options": [ { "label": "New", "value": "new" }, { "label": "Used", "value": "used" } ] }, { "name": "date_received", "label": "Date received", "type": "date", "fieldType": "date" }, { "name": "year", "label": "Year", "type": "number", "fieldType": "number" }, { "name": "make", "label": "Make", "type": "string", "fieldType": "text" }, { "name": "model", "label": "Model", "type": "string", "fieldType": "text" }, { "name": "vin", "label": "VIN", "type": "string", "hasUniqueValue": true, "fieldType": "text" }, { "name": "color", "label": "Color", "type": "string", "fieldType": "text" }, { "name": "mileage", "label": "Mileage", "type": "number", "fieldType": "number" }, { "name": "price", "label": "Price", "type": "number", "fieldType": "number" }, { "name": "notes", "label": "Notes", "type": "string", "fieldType": "text" } ], "associatedObjects": ["CONTACT"] } ``` After creating the object schema, CarSpot makes sure to note the new object's `{objectTypeId}` field, as they'll use this for fetching and updating the object later. They can also use the `{fullyQualifiedName}` value, if they prefer. ### Create a custom object record _With the custom object created, CarSpot can now create records on the object for each car in their inventory._ _They'll create their first car by making a `POST` request to `/crm/v3/objects/2-3465404` with the following request body:_ ```json // Example POST request to https://api.hubspot.com/crm/v3/objects/2-3465404 { "properties": { "condition": "used", "date_received": "1582416000000", "year": "2014", "make": "Nissan", "model": "Frontier", "vin": "4Y1SL65848Z411439", "color": "White", "mileage": "80000", "price": "12000", "notes": "Excellent condition. No accidents." } } ``` The response for this API call would look similar to: ```json // Example response body { "id": "181308", "properties": { "color": "White", "condition": "used", "make": "Nissan", "mileage": "80000", "model": "Frontier", "vin": "4Y1SL65848Z411439", "notes": "Excellent condition. No accidents.", "price": "12000", "year": "2014", "date_received": "1582416000000" }, "createdAt": "2020-02-23T01:44:11.035Z", "updatedAt": "2020-02-23T01:44:11.035Z", "archived": false } ``` With the record created, they can use the `id` value to later associate the car with an existing contact. If they wanted to later retrieve this record along with specific properties, they could make a `GET` request to `https://api.hubapi.com/crm/v3/objects/2-3465404/181308?portalId=1234567&properties=year&properties=make&properties=model` ### Associate the custom object record to another record You can use the ID of the new car record (`181308`) and the ID of another record to associate a custom object record with a record of another object. To create an association, make a `PUT` request to `/crm/v3/objects/{objectType}/{objectId}/associations/{toObjectType}/{toObjectId}/{associationType}`. If the object relationship is already [defined](#defining-a-new-association), to determine the `associationType` value, make a `GET` request to `crm/v3/schemas/{objectType}`. For example, with the contact _ID `51` and the association type `75`,_ CarSpot can associate the car record with a contact. Using the above IDs, the request URL will be constructed as follows: `https://api.hubspot.com/crm/v3/objects/2-3465404/181308/associations/contacts/51/75` ### Define a new association CarSpot now wants to start tracking post-sale services for their cars. To do so, they'll use HubSpot tickets to log any maintenance performed. To allow associations between cars and tickets, they'll create a new association by making a `POST` request to **`/crm/v3/schemas/2-3465404/associations`** with the following request body: ```json // Example POST request to https://api.hubspot.com/crm/v3/schemas/2-3465494/associations { "fromObjectTypeId": "2-3465404", "toObjectTypeId": "ticket", "name": "car_to_ticket" } ``` The response for this API call would look similar to: ```json // Example response { "id": "121", "createdAt": "2020-02-23T01:52:12.893826Z", "updatedAt": "2020-02-23T01:52:12.893826Z", "fromObjectTypeId": "2-3465404", "toObjectTypeId": "0-5", "name": "car_to_ticket" } ``` When creating a new association between two custom objects, specify the custom objects by their _objectTypeId_ in the _toObjectTypeId_ field. For standard objects, you can identify them by name or use the following values: - **Contact:** 0-1 - **Company:** 0-2 - **Deal:** 0-3 - **Ticket:** 0-5 ### Define a new property As they continue to track maintenance, CarSpot sees an opportunity to bundle maintenance services into packages. To track these maintenance packages on individual car records, they'll create a new enumeration property containing the available packages. To define a new property, they'll make a `POST` request to `/crm/v3/properties/2-3465404` with the following request body: ```json // Example POST request to https://api.hubspot.com/crm/v3/properties/2-3465404 { "groupName": "car_information", "name": "maintenance_package", "label": "Maintenance Package", "type": "enumeration", "fieldType": "select", "options": [ { "label": "Basic", "value": "basic" }, { "label": "Oil change only", "value": "oil_change_only" }, { "label": "Scheduled", "value": "scheduled" } ] } ``` The response for this API call would look similar to: ```json // Example response { "updatedAt": "2020-02-23T02:08:20.055Z", "createdAt": "2020-02-23T02:08:20.055Z", "name": "maintenance_package", "label": "Maintenance Package", "type": "enumeration", "fieldType": "select", "groupName": "car_information", "options": [ { "label": "Basic", "value": "basic", "displayOrder": -1, "hidden": false }, { "label": "Oil change only", "value": "oil_change_only", "displayOrder": -1, "hidden": false }, { "label": "Scheduled", "value": "scheduled", "displayOrder": -1, "hidden": false } ], "displayOrder": -1, "calculated": false, "externalOptions": false, "archived": false, "hasUniqueValue": false, "hidden": false, "modificationMetadata": { "archivable": true, "readOnlyDefinition": false, "readOnlyValue": false }, "formField": false } ``` Now that the property has been created, they want it to appear in the sidebar of each car record so that the information is readily available to their sales reps and technicians. To do this, they'll add the property to `secondaryDisplayProperties` by making a `PATCH` request to `/crm/v3/schemas/2-3465404` with the following request body: ```json // Example PATCH request to https://api.hubspot.com/crm/v3/schemas/2-3465404 { "secondaryDisplayProperties": ["maintenance_package"] } ``` The response for this API call would look similar to: ```json // Example response { "id": "3465404", "createdAt": "2020-02-23T01:24:54.537Z", "updatedAt": "2020-02-23T02:12:24.175874Z", "labels": { "singular": "Car", "plural": "Cars" }, "requiredProperties": ["year", "model", "vin", "make"], "searchableProperties": ["year", "model", "vin", "make"], "primaryDisplayProperty": "model", "secondaryDisplayProperties": ["maintenance_package"], "portalId": 1234567, "name": "car" } ``` Now, when a technician opens a contact record that has an associated car, the property will be displayed in the custom object card in the sidebar: As CarSpot continues to use HubSpot, they'll likely find ways to refine and expand this custom object and more using HubSpot's API. They might even decide to [build dynamic pages using their custom object data.](/guides/cms/content/data-driven-content/dynamic-pages/crm-objects) # CRM API | Deal splits @ Learn how to view and edit deal splits via API. $ guides & api/crm/objects --- # Deal splits If your HubSpot account has a _**Sales Hub** Enterprise_ subscription, you can set up [deal splits](https://knowledge.hubspot.com/records/split-deal-credit-among-users) to split credit for deal amounts between multiple users. Once [deal splits are turned on](https://knowledge.hubspot.com/records/split-deal-credit-among-users#turn-on-deal-splits) in your HubSpot account, you can use the deal splits API to create new deal splits or view and update existing splits. ## Create or update deal splits To add new splits or update existing splits for deals, make a `POST` request to `crm/v3/objects/deals/splits/batch/upsert`. In your request, include the following fields for each deal: | Field | Description | | --- | --- | | `id` | The ID of the deal. You can retrieve this via the [deals API](/guides/api/crm/objects/deals#retrieve-deals). | | `splits` | An array that contains the user to assign a split to, and the percentage of the deal amount to assign. In the array, include the following fields:`ownerId`: the [owner ID](/guides/api/crm/owners) assigned to the HubSpot user.`percentage`: the percentage of the deal amount to assign to the owner. | When adding deal splits, the deal's owner must be included as a split owner, the split percentages must add up to 1.0, and you must meet the limits for the split user maximum and split percentage minimum set in your [deal split settings.](https://knowledge.hubspot.com/records/split-deal-credit-among-users#turn-on-deal-splits)If any deals included in the batch request fail validation for these requirements, the request will result in an error. For example, to assign even deal credit to two users on a deal, your request would look like: ```json ///Example request body { "inputs": [ { "id": 5315919905, "splits": [ { "ownerId": 41629779, "percentage": 0.5 }, { "ownerId": 60158084, "percentage": 0.5 } ] } ] } ``` If a deal didn't have existing splits, the splits will appear on the deal record following your request. If the deal had existing splits, they will be replaced by the splits you created in your request. ## Retrieve deal splits To view split information for deals, make a `POST` request to `crm/v3/objects/deals/splits/batch/read`. In your request, include the `id` values of the deals with splits you want to view. You can retrieve a deal's `id` via the [deals API](/guides/api/crm/objects/deals#retrieve-deals). For example, to retrieve deal splits for two deals, your request could look like: ```json ///Example request body { "inputs": [ { "id": "5315919905" }, { "id": "17137567105" } ] } ``` For each deal, the response will include the date and time the splits were created or updated, the IDs of users splitting the deal, and the percentage of the deal amount assigned to each user. For two deals, the response would look similar to: ```json ///Example response { "status": "COMPLETE", "results": [ { "id": "17137567105", "splits": [ { "id": "311226010924", "properties": { "hs_deal_split_percentage": "0.5", "hubspot_owner_id": "41629779" }, "createdAt": "2024-03-11T19:55:26.219Z", "updatedAt": "2024-03-11T19:55:26.219Z", "archived": false }, { "id": "311226010925", "properties": { "hs_deal_split_percentage": "0.25", "hubspot_owner_id": "60158084" }, "createdAt": "2024-03-11T19:55:26.219Z", "updatedAt": "2024-03-11T19:55:26.219Z", "archived": false }, { "id": "311226010926", "properties": { "hs_deal_split_percentage": "0.25", "hubspot_owner_id": "61891281" }, "createdAt": "2024-03-11T19:55:26.219Z", "updatedAt": "2024-03-11T19:55:26.219Z", "archived": false } ] }, { "id": "5315919905", "splits": [ { "id": "57675010822", "properties": { "hs_deal_split_percentage": "0.3333", "hubspot_owner_id": "81538190" }, "createdAt": "2021-06-16T21:04:09.264Z", "updatedAt": "2021-06-16T21:04:09.264Z", "archived": false }, { "id": "57675010821", "properties": { "hs_deal_split_percentage": "0.3333", "hubspot_owner_id": "81426347" }, "createdAt": "2021-06-16T21:04:09.264Z", "updatedAt": "2021-06-16T21:04:09.264Z", "archived": false }, { "id": "57675010820", "properties": { "hs_deal_split_percentage": "0.3334", "hubspot_owner_id": "60158084" }, "createdAt": "2021-06-16T21:04:09.264Z", "updatedAt": "2021-06-16T21:04:09.264Z", "archived": false } ] } ], "startedAt": "2024-03-11T19:56:42.555Z", "completedAt": "2024-03-11T19:56:42.596Z" } ``` # CRM API | Deals @ A deal stores data about an ongoing transaction. The deals endpoints allow you to manage this data and sync it between HubSpot and other systems.  $ guides & api/crm/objects --- # Deals In HubSpot, deals represent transactions with contacts or companies. Deals are tracked through your sales process in [pipeline stages](https://knowledge.hubspot.com/object-settings/set-up-and-customize-pipelines) until they're won or lost. The deals endpoints allow you to manage create and manage deal records, as well as sync deal data between HubSpot and other systems. Learn more about objects, records, properties, and associations APIs in the [Understanding the CRM](/guides/api/crm/understanding-the-crm) guide. For more general information about objects and records in HubSpot, [learn how to manage your CRM database](https://knowledge.hubspot.com/get-started/manage-your-crm-database). ## Create deals To create new deals, make a `POST` request to `/crm/v3/objects/deals`. In the request body, include your deal data in a `properties` object. You can also add an `associations` object to associate your new deal with existing records (e.g., contacts, companies), or activities (e.g., meetings, notes). ### Properties Deal details are stored in deal properties. HubSpot provides a set of [default deal properties](https://knowledge.hubspot.com/properties/hubspots-default-deal-properties), but you can also [create custom properties](https://knowledge.hubspot.com/properties/create-and-edit-properties). When creating a new deal, you should include the following properties in the request: `dealname`, `dealstage`, and if you have multiple pipelines, `pipeline`. If a pipeline isn't specified, the default pipeline will be used. To view all available properties, you can retrieve a list of your account's deal properties by making a `GET` request to `/crm/v3/properties/deals`. Learn more about the the [properties API](/guides/api/crm/properties). You must use the internal ID of a deal stage or pipeline when creating a deal via the API. The internal ID will also be returned when you retrieve deals via the API. You can find a deal stage's or pipeline's internal ID in your [deal pipeline settings](https://knowledge.hubspot.com/object-settings/set-up-and-customize-pipelines#edit-or-delete-pipelines). For example, to create a new deal, your request may look similar to the following: ```json { "properties": { "amount": "1500.00", "closedate": "2019-12-07T16:50:06.678Z", "dealname": "New deal", "pipeline": "default", "dealstage": "contractsent", "hubspot_owner_id": "910901" } } ``` ### Associations When creating a new deal, you can also associate the deal with [existing records](https://knowledge.hubspot.com/records/associate-records) or [activities](https://knowledge.hubspot.com/records/associate-activities-with-records) in an `associations` object. For example, to associate a new deal with an existing contact and company, your request would look like the following: ```json { "properties": { "amount": "1500.00", "closedate": "2019-12-07T16:50:06.678Z", "dealname": "New deal", "pipeline": "default", "dealstage": "contractsent", "hubspot_owner_id": "910901" }, "associations": [ { "to": { "id": 201 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 5 } ] }, { "to": { "id": 301 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 3 } ] } ] } ``` In the `associations` object, you should include the following: | Parameter | Description | | --- | --- | | `to` | The record or activity you want to associate with the deal, specified by its unique `id` value. | | `types` | The type of the association between the deal and the record/activity. Include the `associationCategory`and `associationTypeId`. Default association type IDs are listed [here](/guides/api/crm/associations/associations-v4#association-type-id-values), or you can retrieve the value for custom association types (i.e. labels) via the [associations API](/guides/api/crm/associations/associations-v4#retrieve-association-types). | ## Retrieve deals You can retrieve deals individually or in batches. - To retrieve an individual deal, make a `GET` request to `/crm/v3/objects/deals/{dealId}`. - To request a list of all deals, make a `GET` request to `/crm/v3/objects/deals`. For these endpoints, you can include the following query parameters in the request URL: | Parameter | Description | | --- | --- | | `properties` | A comma separated list of the properties to be returned in the response. If the requested deal doesn't have a value for a property, it will not appear in the response. | | `propertiesWithHistory` | A comma separated list of the current and historical properties to be returned in the response. If the requested deal doesn't have a value for a property, it will not appear in the response. | | `associations` | A comma separated list of objects to retrieve associated IDs for. Any specified associations that don't exist will not be returned in the response. Learn more about the [associations API.](/guides/api/crm/associations/associations-v4) | - To retrieve a batch of specific deals by record ID or a [custom unique identifier property](/guides/api/crm/properties#create-unique-identifier-properties), make a `POST` request to `crm/v3/objects/deals/batch/read`. - The batch endpoint <u>cannot</u> retrieve associations. Learn how to batch read associations with the [associations API](/guides/api/crm/associations/associations-v4). - To retrieve deals by a custom [unique identifier property](/guides/api/crm/properties#create-unique-identifier-properties) rather than the deal ID, include the `idProperty` parameter in the request body to specify the property name. Then, in the `inputs` array, include the values of the unique identifier property rather than the deal ID. For example, to retrieve a batch of deals, your request could look like either of the following: ```json { "properties": ["dealname", "dealstage", "pipeline"], "inputs": [ { "id": "7891023" }, { "id": "987654" } ] } ``` ```json { "properties": ["dealname", "dealstage", "pipeline"], "idProperty": "uniqueordernumber", "inputs": [ { "id": "0001111" }, { "id": "0001112" } ] } ``` To retrieve deals with current and historical values for a specific property, you can include the `propertiesWithHistory` parameter in the request body, as shown below. ```json { "propertiesWithHistory": ["dealstage"], "inputs": [ { "id": "7891023" }, { "id": "987654" } ] } ``` ## Update deals You can update deals individually or in batches. For existing deals, the deal ID is a default unique value that you can use to update the deal via API, but you can also identify deals using [custom unique identifier properties.](/guides/api/crm/properties#create-unique-identifier-properties) - To update an individual deal by its record ID, make a `PATCH` request to `/crm/v3/objects/deals/{dealId}`, and include the data you want to update. - To update multiple deals, make a `POST` request to `/crm/v3/objects/deals/batch/update`. In the request body, include an array with the identifiers for the deals and the properties you want to update. ### Associate existing deals with records or activities To associate a deal with other CRM records or an activity, make a `PUT` request to `/crm/v3/objects/deals/{dealId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}`. To retrieve the `associationTypeId` value, refer to [this list](/guides/api/crm/associations/associations-v4#association-type-id-values) of default values, or make a `GET` request to `/crm/v4/associations/{fromObjectType}/{toObjectType}/labels`. Learn more about associating records with the [associations API](/guides/api/crm/associations/associations-v4). ### Remove an association To remove an association between a deal and a record or activity, make a `DELETE` request to the following URL: `/crm/v3/objects/deals/{dealId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}`. ## Pin an activity on a deal record You can pin an activity on a deal record via API by including the `hs_pinned_engagement_id` parameter in the request. For the value of the parameter, specify the ID of the activity to pin, which can be retrieved via the [engagements APIs](/guides/api/crm/engagements/engagement-details). You can pin one activity per record, and the activity must already be associated with the deal prior to pinning. To set or update a deal's pinned activity, your request could look like: ```json { "properties": { "hs_pinned_engagement_id": 123456789 } } ``` You can also create a deal, associate it with an existing activity, and pin the activity in the same request. For example: ```json { "properties": { "dealname": "New deal", "pipelines": "default", "dealstage": "contractsent", "hs_pinned_engagement_id": 123456789 }, "associations": [ { "to": { "id": 123456789 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 213 } ] } ] } ``` ## Delete deals You can delete deals individually or in batches, which will add the deal to the recycling bin in HubSpot. You can later [restore the deal within HubSpot](https://knowledge.hubspot.com/records/restore-deleted-records). - To [delete an individual deal](/reference/api/crm/objects/deals#delete-%2Fcrm%2Fv3%2Fobjects%2Fdeals%2F%7Bdealid%7D) by its ID, make a `DELETE` request to `/crm/v3/objects/deals/{dealId}`. No request body is needed for this request. - To [batch delete deals](/reference/api/crm/objects/deals#post-%2Fcrm%2Fv3%2Fobjects%2Fdeals%2Fbatch%2Farchive), make a `POST` request to `/crm/v3/objects/deals/batch/archive`. In the request body, include the deal ID values as the `id` inputs, as shown in the example request body below. ```json { "inputs": [ { "id": "123456" }, { "id": "7891011" }, { "id": "12123434" } ] } ``` Learn more about deleting deals in the [reference documentation](/reference/api/crm/objects/deals#delete-%2Fcrm%2Fv3%2Fobjects%2Fdeals%2F%7Bdealid%7D). # Feedback Submissions @ Feedback submission records store information about individual survey submissions. The feedback submissions endpoints allow you to manage this data and sync it between HubSpot and other systems. $ guides & api/crm/objects --- # Feedback Submissions In HubSpot, feedback submissions store information submitted to a feedback survey. Surveys in HubSpot include [Net Promoter Score (NPS)](https://knowledge.hubspot.com/customer-feedback/how-do-i-send-a-customer-loyalty-survey), [Customer Satisfaction (CSAT)](https://knowledge.hubspot.com/customer-feedback/create-and-send-customer-satisfaction-surveys), [Customer Effort Score (CES)](https://knowledge.hubspot.com/customer-feedback/how-do-i-send-a-customer-support-survey), and [custom surveys](https://knowledge.hubspot.com/customer-feedback/create-a-custom-survey). Using the feedback submission endpoints, you can retrieve submission data about your feedback surveys. This API is read-only, so you cannot use it to create, update, or delete feedback submission data in HubSpot. Learn more about objects, properties, and associations APIs in the [Understanding the CRM](/guides/api/crm/understanding-the-crm) guide. ## Retrieve feedback survey submissions To view details about your feedback survey submissions, you can retrieve submission data in bulk for multiple surveys, or for an individual survey. For example, you can use the API to see all survey responses for a specific NPS survey. To retrieve submissions, make a `GET` request to `/crm/v3/objects/feedback_submissions/{feedbackSubmissionId}`. By default, the following properties are returned for each submission: `hs_createdate`, `hs_lastmodifieddate`, and `hs_object_id`, but you also can retrieve additional [properties](https://knowledge.hubspot.com/customer-feedback/feedback-submission-properties). For example, to retrieve survey submissions with the source and sentiment of the submissions, your request URL would look like: `https://api.hubspot.com/crm/v3/objects/feedback_submissions?properties=hs_sentiment,hs_survey_channel`. ## Feedback submission properties Feedback submissions have [default properties](https://knowledge.hubspot.com/customer-feedback/feedback-submission-properties#default-feedback-submission-properties) that contain information about the survey, submission answers, and the date the survey was submitted. You can also [create custom submissions properties](https://knowledge.hubspot.com/customer-feedback/feedback-submission-properties#custom-feedback-submission-properties). Feedback submissions properties <u>cannot</u> be created or edited via API. You can only create properties in the [feedback surveys tool within HubSpot](https://knowledge.hubspot.com/customer-feedback/create-a-custom-survey#survey), and the properties cannot be edited after creation. ## Associations Feedback submissions can be associated with contact and ticket records. Learn how to associate objects with the [associations API.](/guides/api/crm/associations/associations-v4) # CRM API | Goals @ Goals are used to create user-specific quotas for their sales and services teams based on templates provided by HubSpot. The goals endpoints allow you to sync this data between HubSpot and other systems.  $ guides & api/crm/objects --- In HubSpot, goals are used to create user-specific quotas for their sales and services teams based on templates provided by HubSpot. The goals API allow you to retrieve goals data in your HubSpot account. Learn more about [using goals in HubSpot](https://knowledge.hubspot.com/reports/create-goals). ## Retrieve goals You can retrieve an individual goal or all goals in your account. - To request all goals, make a `GET` request to `/crm/v3/objects/goal_targets`. - To retrieve an individual goal, make a `GET` request to `/crm/v3/objects/goal_targets/{goalTargetId}/`. - To retrieve goals that meet a specific set of criteria, you can make a `POST` request to the search endpoint and include filters in the request body. Learn more about [searching the CRM](/guides/api/crm/search). For example, to retrieve a goal with an ID of `44027423340`, the request URL would be the following: `https://api.hubapi.com/crm/v3/objects/goal_targets/44027423340/` The response will include a few default properties, including the create date, last modified date. ```json // Example response { "id": "87504620389", "properties": { "hs_createdate": "2021-11-30T22:18:49.923Z", "hs_lastmodifieddate": "2023-12-11T19:21:32.851Z", "hs_object_id": "87504620389" }, "createdAt": "2021-11-30T22:18:49.923Z", "updatedAt": "2023-12-11T19:21:32.851Z", "archived": false } ``` To return specific properties, include a `properties` query parameter in the request URL along with comma-separated property names. Learn more about [user properties below](#goal-properties). For example, making a `GET` request to the following URL would result in the response below: `crm/v3/objects/users?properties=hs_job_title,hs_additional_phone` ```json // Example response { "id": "87504620389", "properties": { "hs_createdate": "2021-11-30T22:18:49.923Z", "hs_lastmodifieddate": "2023-12-11T19:21:32.851Z", "hs_object_id": "87504620389" }, "createdAt": "2021-11-30T22:18:49.923Z", "updatedAt": "2023-12-11T19:21:32.851Z", "archived": false } ``` ## Goals properties When making a `GET` request to the Goals API, you can also request specific goal properties: - **hs_goal_name:** This is a string that denotes the name of a goal. - **hs_target_amount:** Number that denotes the goal target value. - **hs_start_datetime:** Goal's start date as a UTC timestamp. - **hs_end_datetime:** Goal's end date as a UTC timestamp. - **hs_created_by_user_id:** HubSpot UserId of the person who created the goal, not the one assigned to a goal. For example, if you wanted to include all properties listed above, the request URL may resemble the following: `https://api.hubapi.com/crm/v3/objects/goal_targets/44027423340?properties=hs_goal_name,hs_target_amount,hs_start_datetime,hs_end_datetime,hs_created_by_user_id` The response may look similar to the JSON excerpt below: ```json // Example response for GET request to /crm/v4/objects/goal_targets/{goal_target_id}/ { "id": "44027423340", "properties": { "hs_created_by_user_id": "885536", "hs_createdate": "2023-02-15T15:53:07.080Z", "hs_end_datetime": "2024-01-01T00:00:00Z", "hs_goal_name": "Revenue Goal 2023", "hs_lastmodifieddate": "2023-02-16T10:02:21.131Z", "hs_object_id": "44027423340", "hs_start_datetime": "2023-12-01T00:00:00Z", "hs_target_amount": "2000.00" }, "createdAt": "2023-02-15T15:53:07.080Z", "updatedAt": "2023-02-16T10:02:21.131Z", "archived": false } ``` # CRM API | Leads @ Leads are contacts or companies that are potential customers who have shown interest in your products or services. The leads endpoints allow you to manage this data and sync it between HubSpot and other systems. $ guides & api/crm/objects --- In HubSpot, [leads](https://knowledge.hubspot.com/object-settings/sync-lead-ownership-and-activities) are contacts or companies that are potential customers who have shown interest in your products or services. The leads endpoints allow you to create and manage lead records in your HubSpot account, as well as sync lead data between HubSpot and other systems. Before using the API, be sure leads have been [set up in your account](https://knowledge.hubspot.com/object-settings/sync-lead-ownership-and-activities). Learn more about objects, records, properties, and associations APIs in the [Understanding the CRM](/guides/api/crm/understanding-the-crm) guide. For more general information about objects and records in HubSpot, [learn how to manage your CRM database](https://knowledge.hubspot.com/contacts-user-guide). ## Create leads To create new leads, make a `POST` request to `/crm/v3/objects/leads`. In the request body, include any details about the lead in a `properties` object. Your new lead: - Must have a lead name, specified using the `hs_lead_name` property. - Must be associated with an existing contact. - Should only be assigned to a user with a [seat](https://knowledge.hubspot.com/account-management/manage-seats). (Leads can only be worked via the workspace). ### Properties Lead details are stored in lead properties. There are [default HubSpot lead properties](https://knowledge.hubspot.com/properties/hubspots-default-lead-properties), but you can also [create custom lead properties](https://knowledge.hubspot.com/properties/create-and-edit-properties). To view all available properties, you can retrieve a list of your account's lead properties by making a `GET` request to `/crm/v3/properties/leads`. Learn more about the [properties API](/guides/api/crm/properties). See the table below for some common properties for leads: | PROPERTY | DESCRIPTION | | --- | --- | | `hs_lead_name` | The full name of the lead. | | `hs_lead_type` | A dropdown list of lead types. You can edit or add new types in your [lead property settings.](https://knowledge.hubspot.com/properties/create-and-edit-properties#view-and-edit-properties) | | `hs_lead_label` | The current status of the lead. You can edit or add new labels in your [lead property settings.](https://knowledge.hubspot.com/properties/create-and-edit-properties#view-and-edit-properties) | ### Associations When creating a new lead you must associate the lead with [existing records](https://knowledge.hubspot.com/records/associate-records) in an associations object provided in the request body. In the associations object, you should include the following: | Parameter | Description | | --- | --- | | `to` | The record you want to associate with the lead, specified by its unique `id` value. | | `types` | The type of the association between the lead and the record/activity. Include the `associationCategory`and `associationTypeId`. Default association type IDs are listed [here](/guides/api/crm/associations#association-type-id-values), or you can retrieve the value for custom association types (i.e. labels) via the [associations API](/guides/api/crm/associations#retrieve-association-types). | For example, to create a warm lead named “Jane Doe” who has a type of _New Business_, your request body would resemble the following: ```json { "associations": [ { "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 578 } ], "to": { "id": "12345" } } ], "properties": { "hs_lead_name": "Jane Doe", "hs_lead_type": "NEW BUSINESS", "hs_lead_label": "WARM" } } ``` ## Retrieve leads You can retrieve leads individually or in batches. - To retrieve an individual lead, make a `GET` request to `/crm/v3/objects/leads/{leadsId}`. - To request a list of all leads, make a `GET` request to `/crm/v3/objects/leads`. For these endpoints, you can include the following query parameters in the request URL: | Parameter | Description | | --- | --- | | `properties` | A comma separated list of the properties to be returned in the response. If the requested lead doesn't have a value for a property, it will not appear in the response. | | `propertiesWithHistory` | A comma separated list of the current and historical properties to be returned in the response. If the requested lead doesn't have a value for a property, it will not appear in the response. | | `associations` | A comma separated list of objects to retrieve associated IDs for. Any specified associations that don't exist will not be returned in the response. Learn more about the [associations API.](/guides/api/crm/associations) | - To retrieve a batch of specific leads by ID, make a `POST` request to `crm/v3/objects/leads/batch/read`. The batch endpoint <u>can't</u> retrieve associations. Learn how to batch read associations with the [associations API](/guides/api/crm/associations). For the batch read endpoint, you can use the optional `Property` parameter to retrieve leads by `leadID` or a custom [unique identifier property](/guides/api/crm/properties#create-unique-identifier-properties). By default, the `id` values in the request refer to the record ID (`hs_object_id`), so the `idProperty` parameter isn't required when retrieving by record ID. If you're using a custom unique value property to retrieve leads, you must include the `idProperty` parameter. ## Update leads You can update leads individually or in batches. For existing leads, the leads ID is a unique value, so you can use `leadsId` to update leads via the API. To update an individual lead by its lead ID, make a `PATCH` request to `/crm/v3/objects/leads/{leadsId}`, and include the data you want to update in the request body. ### Associate existing leads with records To associate a lead with other CRM records or an activity, make a `PUT` request to `/crm/v3/objects/leads/{leadsId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}`. To retrieve the `associationTypeId` value, refer to [this list](/guides/api/crm/associations#association-type-id-values) of default values, or make a `GET` request to `/crm/v4/associations/{fromObjectType}/{toObjectType}/labels`. Learn more about the [associations API.](/guides/api/crm/associations) ### Remove an association To remove an association between a lead and a record or activity, make a `DELETE` request to the following URL: `/crm/v3/objects/leads/{leadId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}`. If you remove all primary associations to the lead, the lead will automatically be deleted. ## Delete leads You can delete leads individually or in batches, which will add the lead to the recycling bin in HubSpot. You can later [restore the lead within HubSpot](https://knowledge.hubspot.com/records/restore-deleted-records). To delete an individual lead by its ID, make a `DELETE` request to `/crm/v3/objects/leads/{leadId}`. Learn more about deleting leads in the [reference documentation](/reference/api/crm/objects/leads#delete-%2Fcrm%2Fv3%2Fobjects%2Fleads%2F%7Bleadsid%7D). ## Limits Batch operations for creating, updating, and archiving are limited to batches of 100. # CRM API | Tickets @ A ticket represents a customer request for support. The tickets endpoints allow you to manage this data and sync it between HubSpot and other systems.  $ guides & api/crm/objects --- In HubSpot, tickets represents customer requests for help. Tickets are tracked through your support process in [pipeline statuses](https://knowledge.hubspot.com/object-settings/set-up-and-customize-pipelines) until they're closed. The tickets endpoints allow you to manage create and manage ticket records, as well as sync ticket data between HubSpot and other systems. Learn more about objects, records, properties, and associations APIs in the [Understanding the CRM](/guides/api/crm/understanding-the-crm) guide. For more general information about objects and records in HubSpot, [learn how to manage your CRM database](https://knowledge.hubspot.com/get-started/manage-your-crm-database). ## Create tickets To create new tickets, make a `POST` request to `/crm/v3/objects/tickets`. In your request, include your ticket data in a properties object. You can also add an associations object to associate your new ticket with existing records (e.g., contacts, companies), or activities (e.g., meetings, notes). ### Properties Ticket details are stored in ticket properties. There are [default HubSpot ticket properties](https://knowledge.hubspot.com/properties/hubspots-default-ticket-properties), but you can also [create custom properties](https://knowledge.hubspot.com/properties/create-and-edit-properties). When creating a new ticket, you should include the following properties in your request: `subject` (the ticket's name), `hs_pipeline_stage` (the ticket's status) and if you have multiple pipelines, `hs_pipeline`. If a pipeline isn't specified, the default pipeline will be used. To view all available properties, you can retrieve a list of your account's ticket properties by making a `GET` request to `/crm/v3/properties/tickets`. Learn more about the the [properties API](/guides/api/crm/properties). You must use the internal ID of a ticket status or pipeline when creating a ticket via the API. The internal ID is a number, which will also be returned when you retrieve tickets via the API. You can find a ticket status or pipeline's internal ID in your [ticket pipeline settings.](https://knowledge.hubspot.com/object-settings/set-up-and-customize-pipelines) For example, to create a new ticket, your request may look similar to the following: ```json ///Example request body { "properties": { "hs_pipeline": "0", "hs_pipeline_stage": "1", "hs_ticket_priority": "HIGH", "subject": "troubleshoot report" } } ``` ### Associations When creating a new ticket, you can also associate the ticket with [existing records](https://knowledge.hubspot.com/records/associate-records) or [activities](https://knowledge.hubspot.com/records/associate-activities-with-records) by including an associations object. For example, to associate a new ticket with an existing contact and company, your request would look like the following: ```json ///Example request body { "properties": { "hs_pipeline": "0", "hs_pipeline_stage": "1", "hs_ticket_priority": "HIGH", "subject": "troubleshoot report" }, "associations": [ { "to": { "id": 201 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 16 } ] }, { "to": { "id": 301 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 26 } ] } ] } ``` In the associations object, you should include the following: | Parameter | Description | | --- | --- | | `to` | The record or activity you want to associate with the ticket, specified by its unique `id` value. | | `types` | The type of the association between the ticket and the record/activity. Include the `associationCategory`and `associationTypeId`. Default association type IDs are listed [here](/guides/api/crm/associations/associations-v4#association-type-id-values), or you can retrieve the value for custom association types (i.e. labels) via the [associations API](/guides/api/crm/associations/associations-v4#retrieve-association-types). | ## Retrieve tickets You can retrieve tickets individually or in batches. - To retrieve an individual ticket, make a `GET` request to `/crm/v3/objects/tickets/{ticketId}`. - To request a list of all tickets, make a `GET` request to `/crm/v3/objects/tickets`. For these endpoints, you can include the following query parameters in the request URL: | Parameter | Description | | --- | --- | | `properties` | A comma separated list of the properties to be returned in the response. If the requested ticket doesn't have a value for a property, it will not appear in the response. | | `propertiesWithHistory` | A comma separated list of the current and historical properties to be returned in the response. If the requested ticket doesn't have a value for a property, it will not appear in the response. | | `associations` | A comma separated list of objects to retrieve associated IDs for. Any specified associations that don't exist will not be returned in the response. Learn more about the [associations API.](/guides/api/crm/associations/associations-v4) | - To retrieve a batch of specific tickets by record ID or a [custom unique identifier property](/guides/api/crm/properties#create-unique-identifier-properties), make a `POST` request to `crm/v3/objects/tickets/batch/read`. The batch endpoint <u>cannot</u> retrieve associations. Learn how to batch read associations with the [associations API](/guides/api/crm/associations/associations-v4). For the batch read endpoint, you can also use the optional `idProperty` parameter to retrieve tickets by a custom [unique identifier property](/guides/api/crm/properties#create-unique-identifier-properties). By default, the `id` values in the request refer to the record ID (`hs_object_id`), so the `idProperty` parameter is not required when retrieving by record ID. To use a custom unique value property to retrieve tickets, you must include the `idProperty` parameter. For example, to retrieve a batch of tickets, your request could look like either of the following: ```json ///Example request body with record ID { "properties": ["subject", "hs_pipeline_stage", "hs_pipeline"], "inputs": [ { "id": "4444888856" }, { "id": "666699988" } ] } ``` ```json ///Example request body with a unique value property { "properties": ["subject", "hs_pipeline_stage", "hs_pipeline"], "idProperty": "uniquepropertyexample", "inputs": [ { "id": "abc" }, { "id": "def" } ] } ``` To retrieve tickets with current and historical values for a property, your request could look like: ```json ///Example request body with record ID (current and historical values) { "propertiesWithHistory": ["hs_pipeline_stage"], "inputs": [ { "id": "4444888856" }, { "id": "666699988" } ] } ``` ## Update tickets You can update tickets individually or in batches. For existing tickets, the record ID is a default unique value that you can use to update the ticket via API, but you can also identify and update tickets using [custom unique identifier properties.](/guides/api/crm/properties#create-unique-identifier-properties) - To update an individual ticket by its record ID, make a `PATCH` request to `/crm/v3/objects/tickets/{ticketId}`, and include the data you want to update. - To update multiple tickets, make a `POST` request to `/crm/v3/objects/tickets/batch/update`. In the request body, include an array with the identifiers for the tickets and the properties you want to update. ### Associate existing tickets with records or activities To associate a ticket with other CRM records or an activity, make a `PUT` request to `/crm/v3/objects/tickets/{ticketId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}`. To retrieve the `associationTypeId` value, refer to [this list](/guides/api/crm/associations/associations-v4#association-type-id-values) of default values, or make a `GET` request to `/crm/v4/associations/{fromObjectType}/{toObjectType}/labels`. Learn more about the [associations API.](/guides/api/crm/associations/associations-v4) ### Remove an association To remove an association between a ticket and a record or activity, make a `DELETE` request to the following URL: `/crm/v3/objects/tickets/{ticketId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}`. ## Pin an activity on a ticket record You can pin an activity on a ticket record via API by including the `hs_pinned_engagement_id` field in your request. In the field, include the `id` of the activity to pin, which can be retrieved via the [engagements APIs](/guides/api/crm/engagements/engagement-details). You can pin one activity per record, and the activity must already be associated with the ticket prior to pinning. To set or update a ticket's pinned activity, your request could look like: ```json ///Example request body PATCH /crm/v3/objects/tickets/{ticketId} { "properties": { "hs_pinned_engagement_id": 123456789 } } ``` You can also create a ticket, associate it with an existing activity, and pin the activity in the same request. For example: ```json ///Example request body POST /crm/v3/objects/tickets { "properties": { "hs_pipeline": "0", "hs_pipeline_stage": "1", "hs_ticket_priority": "HIGH", "subject": "troubleshoot report", "hs_pinned_engagement_id": 123456789 }, "associations": [ { "to": { "id": 123456789 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 227 } ] } ] } ``` ## Delete tickets You can delete tickets individually or in batches, which will add the ticket to the recycling bin in HubSpot. You can later [restore the ticket within HubSpot](https://knowledge.hubspot.com/records/restore-deleted-records). To delete an individual ticket by its ID, make a `DELETE` request to `/crm/v3/objects/tickets/{ticketId}`. Learn more about deleting tickets in the [reference documentation](/reference/api/crm/objects/tickets#delete-%2Fcrm%2Fv3%2Fobjects%2Ftickets%2F%7Bticketid%7D). # CRM API | Owners @ HubSpot uses owners to assign CRM objects to specific people. These endpoints are used to get a list of available owners for an account. $ guides & api/crm --- HubSpot owners [assign](https://knowledge.hubspot.com/records/how-to-set-a-record-owner) specific users to records, activities, or marketing tasks, and can be used in personalization tokens for your content. Owners are automatically created and updated in HubSpot when new users are added or existing owners are synced from [Salesforce](https://knowledge.hubspot.com/salesforce/salesforce-integration-faq). The owners API endpoints are read-only, so you can use them to retrieve an owner's identifying details, including the owner ID. This identifier can then be used to assign ownership to CRM records in HubSpot, via an integration, or via property change API calls. ## Retrieve a list of owners To retrieve the current owners in your account, make a `GET` request to `/crm/v3/owners`. The response will return each user's name, email, ID values, create/update dates, and if applicable, team information. Two ID values are returned, which are used for different purposes: - `id`: the ID of the owner. This value should be used when retrieving information about a specific owner, and when assigning an owner to a record or activity. - `userId`: the ID of the user. This value can be used to specify users in the [settings API](/guides/api/settings/users/user-provisioning), but will result in an error if it is used to assign ownership. Your response will look similar to the following: ```json ///Example response GET crm/v3/owners { "results": [ { "id": "41629779", "email": "email@hubspot.com", "type": "PERSON", "firstName": "HubSpot", "lastName": "Test Owner", "userId": 9586504, "userIdIncludingInactive": 9586504, "createdAt": "2019-12-25T13:01:35.228Z", "updatedAt": "2023-08-22T13:40:26.790Z", "archived": false, "teams": [ { "id": "368389", "name": "Sales Team", "primary": true } ] }, { "id": "60158084", "email": "email@gmail.com", "type": "PERSON", "firstName": "Test", "lastName": "Email", "userId": 9274996, "userIdIncludingInactive": 9274996, "createdAt": "2021-02-10T17:59:04.891Z", "updatedAt": "2023-02-09T17:41:52.767Z", "archived": false, "teams": [ { "id": "368389", "name": "Sales Team", "primary": true } ] }, { "id": "81538190", "email": "salesmanager@hubspot.com", "type": "PERSON", "firstName": "Sales", "lastName": "Manager Example", "userId": 3892666, "userIdIncludingInactive": 3892666, "createdAt": "2021-05-27T16:55:57.242Z", "updatedAt": "2022-08-02T18:34:35.039Z", "archived": false } ] } ``` You can also retrieve archived owners to view users that were deactivated. To do so, add the `archived` parameter with the value `true`. For archived users, there is still an `id` value, but the `userId` value will be `null`. The user ID is instead stored in the `userIdIncludingInactive` field. For example: ```json ///Example response GET crm/v3/owners/?archived=true { "results": [ { "id": "42103462", "email": "useremail@hubspot.com", "type": "PERSON", "firstName": "", "lastName": "", "userId": null, "userIdIncludingInactive": 9685555, "createdAt": "2020-01-09T20:28:50.080Z", "updatedAt": "2020-01-09T20:28:50.080Z", "archived": true } ] } ``` ## Retrieve information about an individual owner To retrieve a specific owner, make a `GET` request to `/crm/v3/owners/{ownerId}`. You should use the `id` value to specify the owner for which you want more details. The `updatedAt` value in the response changes based on updates to the Owner object itself. It will not be updated for changes to the User object. For example, changing a user's permissions will <u>not</u> update the `updatedAt` value. # CRM API | Pipelines @ The Pipelines endpoints are used to view, create, and manage pipelines in the HubSpot CRM and service tools. $ guides & api/crm --- In HubSpot, a pipeline is where records are tracked through stages. For example, sales pipelines can be used to predict revenue and identify roadblocks or service pipelines can be used to manage ticket statuses and analyze blockers. Depending on your [subscription](https://legal.hubspot.com/hubspot-product-and-services-catalog), you can create multiple pipelines for an object. For example, when working with deals, an account might have one pipeline for _New Sales_ and another for _Contract Renewals_. Pipelines are available for the following [objects](/guides/api/crm/understanding-the-crm): - Deals - Tickets - Appointments - Courses - Listings - Orders - Services - Leads (_**Sales Hub**_ _Professional_ and _Enterprise_ only) - Custom objects (_Enterprise_ only) ## Manage pipelines ### Create a pipeline Accounts with a _Starter_, _Professional_, or _Enterprise_ subscription can create additional pipelines. If your account has a **_Sales Hub_** subscription, you can create [multiple deal pipelines.](https://knowledge.hubspot.com/object-settings/set-up-and-customize-pipelines) If your account has a _**Service Hub**_ subscription, you can create multiple [ticket pipelines](https://knowledge.hubspot.com/object-settings/set-up-and-customize-pipelines). To create a new pipeline, make a `POST` request to `/crm/v3/pipelines/{objectType}`. In the request body, include the following: - `displayOrder`: a number that decides the order the pipeline is displayed within all pipelines of the object. If multiple pipelines are the same number in the order, they'll be listed alphabetically by label. - `label`: the name of the pipeline as it is displayed in HubSpot. - `stages`: inputs to set the stages in the pipeline. For each stage, include the following: - `metadata`: properties related to the stage, optional for all objects except deals. For deals, `probability` is required with a value between `0.0` and `1.0`, `0.0` being _Closed Lost_ and `1.0` being _Closed Won_. For tickets, you can include `ticketState`with a value of either `OPEN` or `CLOSED`. - `displayOrder`: a number that decides the order in which the stage will be displayed. - `label`: the stage name. This must be unique for each stage. Appointment, course, listing, lead, order, and service pipelines can have up to 30 stages. Deal, ticket, and custom object pipelines can have up to 100 stages. For example, to create a new deal pipeline, your request may look like the following: ```json ///Example request body POST crm/v3/pipelines/deals { "displayOrder": 3, "label": "New deal pipeline", "stages": [ { "label": "In Progress", "metadata": { "probability": "0.2" }, "displayOrder": 0 }, { "label": "Contract signed", "metadata": { "probability": "0.8" }, "displayOrder": 1 }, { "label": "Closed Won", "metadata": { "probability": "1.0" }, "displayOrder": 2 }, { "label": "Closed Lost", "metadata": { "probability": "0.0" }, "displayOrder": 3 } ] } ``` ### Replace a pipeline If there's an existing pipeline you want to replace instead of creating a new pipeline, make a `PUT` request to `/crm/v3/pipelines/{objectType}/{pipelineId}` with the `id` of the pipeline to replace. In the request body, include the [fields](#create-pipeline-fields) [required when creating](#create-pipeline-fields) [a new pipeline](#create-pipeline-fields). The information you add in the request body will overwrite the existing pipeline's details. ### Retrieve pipelines - To retrieve all pipelines for an object, make a `GET` request to `/crm/v3/pipelines/{objectType}`. Each pipeline's `id`, `label`, and `displayOrder` values will be returned, along with information about when it was created or updated. You can use the `id` values to retrieve and update individual pipelines. - To retrieve an individual pipeline, make a `GET` request to `/crm/v3/pipelines/{objectType}/{pipelineId}`. ### Update a pipeline To edit a pipeline's details, such as label or display order, make a `PATCH` request to `/crm/v3/pipelines/{objectType}/{pipelineId}`. In the request body, include the properties to update. If you want to edit a pipeline's stages, use the [stage endpoints](#manage-pipeline-stages). ### Delete a pipeline To delete a pipeline, make a `DELETE` request to `/crm/v3/pipelines/{objectType}/{pipelineId}`. To check if there are records in the pipeline, include the `validateReferencesBeforeDelete` parameter with a value of `true`. When the parameter is included, you'll be notified of existing records and cannot delete the pipeline until the records have been deleted or moved to a different pipeline. If there are existing records, your response will look similar to the following: ```json ///404 Bad Request example { "status": "error", "message": "Stage IDs: [renter_viewing, renter_security_deposit_paid, renter_new_lead, renter_closed_won, renter_closed_lost] are being referenced by object IDs: [22901690010]", "correlationId": "1fb9ac01-f574-4919-bf55-2c8c25ac1507", "context": { "stageIds": [ "[renter_viewing, renter_security_deposit_paid, renter_new_lead, renter_closed_won, renter_closed_lost]" ], "objectIds": ["[22901690010]"] }, "category": "VALIDATION_ERROR", "subCategory": "PipelineError.STAGE_ID_IN_USE" } ``` ## Manage pipeline stages ### Create a stage To add a new stage to a pipeline, make a `POST` request to `/crm/v3/pipelines/{objectType}/{pipelineId}/stages`. In the request body, include the following: - `displayOrder`: a number that decides the order of the stage in the pipeline. If multiple stages are the same number in the order, they'll be listed alphabetically by label. - `label`: the name of the stage as it is displayed in HubSpot. - `metadata`: an object that includes properties for the stage, optional for all objects except deals. For deals, `probability` is required with a value between `0.0` and `1.0`, `0.0` being _Closed Lost_ and `1.0` being _Closed Won_. For tickets, you can include `ticketState` with a value of `OPEN` or `CLOSED`. Appointment, course, listing, lead, order, and service pipelines can have up to 30 stages. Deal, ticket, and custom object pipelines can have up to 100 stages. For example, to add a stage called _Contract signed_ as the fifth stage in a pipeline, you request would look like: ```json ///Example request body { "metadata": { "probability": "0.8" }, "displayOrder": 4, "label": "Contract signed" } ``` ### Replace a stage If there's an existing pipeline stage you want to replace instead of creating a new stage, make a `PUT` request to `/crm/v3/pipelines/{objectType}/{pipelineId}/stages/{stageId}` with the `id` of the stage to replace. In the request body, include the [fields](#create-pipeline-fields) [required when creating](#create-stage-fields) [a new stage](#create-stage-fields). The information you add in the request body will overwrite the existing stage's details. ### Retrieve stages - To retrieve all stages in a pipeline, make a `GET` request to `/crm/v3/pipelines/{objectType}/{pipelineId}/stages`. Each stage's `id`, `label`, and `displayOrder` values will be returned, along with information about when it was created or updated. You can use the `id` values to retrieve and update individual stages. - To retrieve an individual stage, make a `GET` request to `/crm/v3/pipelines/{objectType}/{pipelineId}/stages/{stageId}`. ### Update a stage To edit a stage's details, such as label or display order, make a `PATCH` request to `/crm/v3/pipelines/{objectType}/{pipelineId}/stages/{stageId}`. In the request body, include the properties to update. ### Delete a stage To delete a pipeline stage, make a `DELETE` request to `/crm/v3/pipelines/{objectType}/{pipelineId}/stages/{stageId}`. ## Track changes to pipelines and stages You can use the audit endpoints to track changes made to your pipelines and pipeline stages. - To view changes made to a pipeline, make a `GET` request to `/crm/v3/pipelines/{objectType}/{pipelineId}/audit`. - To view changes made to a stage, make a `GET` request to `/crm/v3/pipelines/{objectType}/{pipelineId}/stages/{stageId}/audit`. In the response, updates are listed in reverse chronological order with details about the type of action, when it occurred, and who made the change. For example, when auditing changes to a pipeline, your response would look like the following: ```json /// Example 200 response for GET /crm/v3/pipelines/deals/11348541/audit { "results": [ { "portalId": 123456, "identifier": "123456:11348541", "action": "UPDATE", "timestamp": "2024-10-07T20:58:57.414Z", "message": "Pipeline update", "rawObject": "{\"displayOrder\":1,\"pipelineId\":\"11348541\",\"portalId\":123456,\"objectType\":\"DEAL\",\"objectTypeId\":\"0-3\",\"version\":11,\"label\":\"Partner pipeline\",\"active\":true,\"stages\":[{\"displayOrder\":0,\"portalId\":123456,\"objectTypeId\":\"0-3\",\"pipelineId\":\"11348541\",\"stageId\":\"11348542\",\"label\":\"Appointment scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.2\"},\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":9586504,\"createdAt\":1614359253457,\"updatedAt\":1728334737395,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"internalWriteOnly\":false,\"active\":true,\"isApprovalStage\":null},{\"displayOrder\":1,\"portalId\":123456,\"objectTypeId\":\"0-3\",\"pipelineId\":\"11348541\",\"stageId\":\"11348543\",\"label\":\"Qualified to buy\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.4\"},\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":9586504,\"createdAt\":1614359253457,\"updatedAt\":1728334737395,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"internalWriteOnly\":false,\"active\":true,\"isApprovalStage\":null},{\"displayOrder\":2,\"portalId\":123456,\"objectTypeId\":\"0-3\",\"pipelineId\":\"11348541\",\"stageId\":\"11348544\",\"label\":\"Presentation scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.6\"},\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":9586504,\"createdAt\":1614359253457,\"updatedAt\":1728334737395,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"internalWriteOnly\":false,\"active\":true,\"isApprovalStage\":null},{\"displayOrder\":3,\"portalId\":123456,\"objectTypeId\":\"0-3\",\"pipelineId\":\"11348541\",\"stageId\":\"11348545\",\"label\":\"Decision Maker Bought-In\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":9586504,\"createdAt\":1614359253457,\"updatedAt\":1728334737395,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"internalWriteOnly\":false,\"active\":true,\"isApprovalStage\":null},{\"displayOrder\":4,\"portalId\":123456,\"objectTypeId\":\"0-3\",\"pipelineId\":\"11348541\",\"stageId\":\"11348546\",\"label\":\"Contract sent\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.9\"},\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":9586504,\"createdAt\":1614359253457,\"updatedAt\":1728334737395,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"internalWriteOnly\":false,\"active\":true,\"isApprovalStage\":null},{\"displayOrder\":5,\"portalId\":123456,\"objectTypeId\":\"0-3\",\"pipelineId\":\"11348541\",\"stageId\":\"259717454\",\"label\":\"Contract signed\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.2\"},\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":9586504,\"createdAt\":1728334734115,\"updatedAt\":1728334737395,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"internalWriteOnly\":false,\"active\":true,\"isApprovalStage\":null},{\"displayOrder\":6,\"portalId\":123456,\"objectTypeId\":\"0-3\",\"pipelineId\":\"11348541\",\"stageId\":\"11348547\",\"label\":\"Closed won\",\"metadata\":{\"isClosed\":\"true\",\"probability\":\"1.0\"},\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":9586504,\"createdAt\":1614359253457,\"updatedAt\":1728334737395,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"internalWriteOnly\":false,\"active\":true,\"isApprovalStage\":null},{\"displayOrder\":7,\"portalId\":123456,\"objectTypeId\":\"0-3\",\"pipelineId\":\"11348541\",\"stageId\":\"11348548\",\"label\":\"Closed lost\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":9586504,\"createdAt\":1614359253457,\"updatedAt\":1728334737395,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"internalWriteOnly\":false,\"active\":true,\"isApprovalStage\":null}],\"createdBy\":9586504,\"updatedBy\":9586504,\"createdAt\":1614359253457,\"updatedAt\":1728334737395,\"limitExempt\":false,\"autoCloseDateEnabled\":true,\"clearCloseDateWhenMovingClosedToOpen\":false,\"permission\":null,\"createStageId\":null,\"inactiveDefaultPipeline\":false,\"default\":false}", "fromUserId": 9586504 }, { "portalId": 123456, "identifier": "123456:11348541", "action": "UPDATE", "timestamp": "2024-10-07T20:58:54.143Z", "message": "Pipeline update", "rawObject": "{\"displayOrder\":1,\"pipelineId\":\"11348541\",\"portalId\":123456,\"objectType\":\"DEAL\",\"objectTypeId\":\"0-3\",\"version\":10,\"label\":\"Partner pipeline\",\"active\":true,\"stages\":[{\"displayOrder\":0,\"portalId\":123456,\"objectTypeId\":\"0-3\",\"pipelineId\":\"11348541\",\"stageId\":\"11348542\",\"label\":\"Appointment scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.2\"},\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":9586504,\"createdAt\":1614359253457,\"updatedAt\":1728334734115,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"internalWriteOnly\":false,\"active\":true,\"isApprovalStage\":null},{\"displayOrder\":1,\"portalId\":123456,\"objectTypeId\":\"0-3\",\"pipelineId\":\"11348541\",\"stageId\":\"11348543\",\"label\":\"Qualified to buy\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.4\"},\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":9586504,\"createdAt\":1614359253457,\"updatedAt\":1728334734115,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"internalWriteOnly\":false,\"active\":true,\"isApprovalStage\":null},{\"displayOrder\":2,\"portalId\":123456,\"objectTypeId\":\"0-3\",\"pipelineId\":\"11348541\",\"stageId\":\"11348544\",\"label\":\"Presentation scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.6\"},\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":9586504,\"createdAt\":1614359253457,\"updatedAt\":1728334734115,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"internalWriteOnly\":false,\"active\":true,\"isApprovalStage\":null},{\"displayOrder\":3,\"portalId\":123456,\"objectTypeId\":\"0-3\",\"pipelineId\":\"11348541\",\"stageId\":\"11348545\",\"label\":\"Decision Maker Bought-In\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":9586504,\"createdAt\":1614359253457,\"updatedAt\":1728334734115,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"internalWriteOnly\":false,\"active\":true,\"isApprovalStage\":null},{\"displayOrder\":4,\"portalId\":123456,\"objectTypeId\":\"0-3\",\"pipelineId\":\"11348541\",\"stageId\":\"11348546\",\"label\":\"Contract sent\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.9\"},\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":9586504,\"createdAt\":1614359253457,\"updatedAt\":1728334734115,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"internalWriteOnly\":false,\"active\":true,\"isApprovalStage\":null},{\"displayOrder\":5,\"portalId\":123456,\"objectTypeId\":\"0-3\",\"pipelineId\":\"11348541\",\"stageId\":\"11348547\",\"label\":\"Closed won\",\"metadata\":{\"isClosed\":\"true\",\"probability\":\"1.0\"},\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":9586504,\"createdAt\":1614359253457,\"updatedAt\":1728334734115,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"internalWriteOnly\":false,\"active\":true,\"isApprovalStage\":null},{\"displayOrder\":6,\"portalId\":123456,\"objectTypeId\":\"0-3\",\"pipelineId\":\"11348541\",\"stageId\":\"11348548\",\"label\":\"Closed lost\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":9586504,\"createdAt\":1614359253457,\"updatedAt\":1728334734115,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"internalWriteOnly\":false,\"active\":true,\"isApprovalStage\":null},{\"displayOrder\":7,\"portalId\":123456,\"objectTypeId\":\"0-3\",\"pipelineId\":\"11348541\",\"stageId\":\"259717454\",\"label\":\"Contract signed\",\"metadata\":{\"probability\":\"0.2\",\"isClosed\":\"false\"},\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":9586504,\"createdAt\":1728334734115,\"updatedAt\":1728334734115,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"internalWriteOnly\":false,\"active\":true,\"isApprovalStage\":null}],\"createdBy\":9586504,\"updatedBy\":9586504,\"createdAt\":1614359253457,\"updatedAt\":1728334734115,\"limitExempt\":false,\"autoCloseDateEnabled\":true,\"clearCloseDateWhenMovingClosedToOpen\":false,\"permission\":null,\"createStageId\":null,\"inactiveDefaultPipeline\":false,\"default\":false}", "fromUserId": 9586504 }, { "portalId": 123456, "identifier": "123456:11348541", "action": "UPDATE", "timestamp": "2023-07-27T15:30:01.098Z", "message": "Pipeline update", "rawObject": "{\"displayOrder\":1,\"pipelineId\":\"11348541\",\"portalId\":123456,\"label\":\"Partner pipeline\",\"active\":true,\"stages\":[{\"displayOrder\":0,\"stageId\":\"11348542\",\"label\":\"Appointment scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.2\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":1,\"stageId\":\"11348543\",\"label\":\"Qualified to buy\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.4\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":2,\"stageId\":\"11348544\",\"label\":\"Presentation scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.6\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":3,\"stageId\":\"11348545\",\"label\":\"Decision Maker Bought-In\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":4,\"stageId\":\"11348546\",\"label\":\"Contract sent\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.9\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":5,\"stageId\":\"11348547\",\"label\":\"Closed won\",\"metadata\":{\"isClosed\":\"true\",\"probability\":\"1.0\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":6,\"stageId\":\"11348548\",\"label\":\"Closed lost\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null}],\"version\":8,\"updatedBy\":null}", "fromUserId": 9586504 }, { "portalId": 123456, "identifier": "123456:11348541", "action": "UPDATE", "timestamp": "2023-03-17T17:37:42.613Z", "message": "Pipeline update", "rawObject": "{\"displayOrder\":1,\"pipelineId\":\"11348541\",\"portalId\":123456,\"label\":\"Partner pipeline\",\"active\":true,\"stages\":[{\"displayOrder\":0,\"stageId\":\"11348542\",\"label\":\"Appointment scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.2\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":1,\"stageId\":\"11348543\",\"label\":\"Qualified to buy\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.4\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":2,\"stageId\":\"11348544\",\"label\":\"Presentation scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.6\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":3,\"stageId\":\"11348545\",\"label\":\"Decision Maker Bought-In\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":4,\"stageId\":\"11348546\",\"label\":\"Contract sent\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.9\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":5,\"stageId\":\"11348547\",\"label\":\"Closed won\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":6,\"stageId\":\"11348548\",\"label\":\"Closed lost\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null}],\"version\":7,\"updatedBy\":null}", "fromUserId": 9586504 }, { "portalId": 123456, "identifier": "123456:11348541", "action": "UPDATE", "timestamp": "2023-03-17T17:31:59.276Z", "message": "Pipeline update", "rawObject": "{\"displayOrder\":1,\"pipelineId\":\"11348541\",\"portalId\":123456,\"label\":\"Partner pipeline\",\"active\":true,\"stages\":[{\"displayOrder\":0,\"stageId\":\"11348542\",\"label\":\"Appointment scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.2\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":1,\"stageId\":\"11348543\",\"label\":\"Qualified to buy\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.4\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":2,\"stageId\":\"11348544\",\"label\":\"Presentation scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.6\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":3,\"stageId\":\"11348545\",\"label\":\"Decision Maker Bought-In\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":4,\"stageId\":\"11348546\",\"label\":\"Contract sent\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.9\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":5,\"stageId\":\"11348547\",\"label\":\"Closed won\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":6,\"stageId\":\"11348548\",\"label\":\"Closed lost\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null}],\"version\":6,\"updatedBy\":null}", "fromUserId": 9586504 }, { "portalId": 123456, "identifier": "123456:11348541", "action": "UPDATE", "timestamp": "2023-03-17T17:29:18.824Z", "message": "Pipeline update", "rawObject": "{\"displayOrder\":1,\"pipelineId\":\"11348541\",\"portalId\":123456,\"label\":\"Partner pipeline\",\"active\":true,\"stages\":[{\"displayOrder\":0,\"stageId\":\"11348542\",\"label\":\"Appointment scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.2\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":1,\"stageId\":\"11348543\",\"label\":\"Qualified to buy\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.4\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":2,\"stageId\":\"11348544\",\"label\":\"Presentation scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.6\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":3,\"stageId\":\"11348545\",\"label\":\"Decision Maker Bought-In\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":4,\"stageId\":\"11348546\",\"label\":\"Contract sent\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.9\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":5,\"stageId\":\"11348547\",\"label\":\"Closed won\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":6,\"stageId\":\"11348548\",\"label\":\"Closed lost\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null}],\"version\":5,\"updatedBy\":null}", "fromUserId": 9586504 }, { "portalId": 123456, "identifier": "123456:11348541", "action": "UPDATE", "timestamp": "2022-12-09T20:29:21.004Z", "message": "Pipeline update", "rawObject": "{\"displayOrder\":1,\"pipelineId\":\"11348541\",\"portalId\":123456,\"label\":\"Partner pipeline\",\"active\":true,\"stages\":[{\"displayOrder\":0,\"stageId\":\"11348542\",\"label\":\"Appointment scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.2\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":1,\"stageId\":\"11348543\",\"label\":\"Qualified to buy\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.4\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":2,\"stageId\":\"11348544\",\"label\":\"Presentation scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.6\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":3,\"stageId\":\"11348545\",\"label\":\"Decision Maker Bought-In\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":4,\"stageId\":\"11348546\",\"label\":\"Contract sent\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.9\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":5,\"stageId\":\"11348547\",\"label\":\"Closed won\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":6,\"stageId\":\"11348548\",\"label\":\"Closed lost\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null}],\"version\":null,\"updatedBy\":null}", "fromUserId": 9586504 }, { "portalId": 123456, "identifier": "123456:11348541", "action": "UPDATE", "timestamp": "2022-11-08T14:32:11.024Z", "message": "Pipeline update", "rawObject": "{\"displayOrder\":1,\"pipelineId\":\"11348541\",\"portalId\":123456,\"label\":\"Additional pipeline\",\"active\":true,\"stages\":[{\"displayOrder\":0,\"stageId\":\"11348542\",\"label\":\"Appointment scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.2\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":1,\"stageId\":\"11348543\",\"label\":\"Qualified to buy\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.4\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":2,\"stageId\":\"11348544\",\"label\":\"Presentation scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.6\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":3,\"stageId\":\"11348545\",\"label\":\"Decision Maker Bought-In\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":4,\"stageId\":\"11348546\",\"label\":\"Contract sent\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.9\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":5,\"stageId\":\"11348547\",\"label\":\"Closed won\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":6,\"stageId\":\"11348548\",\"label\":\"Closed lost\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null}],\"version\":null,\"updatedBy\":null}", "fromUserId": 9586504 }, { "portalId": 123456, "identifier": "123456:11348541", "action": "UPDATE", "timestamp": "2022-11-08T14:31:58.571Z", "message": "Pipeline update", "rawObject": "{\"displayOrder\":1,\"pipelineId\":\"11348541\",\"portalId\":123456,\"label\":\"Additional pipeline\",\"active\":true,\"stages\":[{\"displayOrder\":0,\"stageId\":\"11348542\",\"label\":\"Appointment scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.2\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":1,\"stageId\":\"11348543\",\"label\":\"Qualified to buy\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.4\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":2,\"stageId\":\"11348544\",\"label\":\"Presentation scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.6\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":3,\"stageId\":\"11348545\",\"label\":\"Decision Maker Bought-In\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":4,\"stageId\":\"11348546\",\"label\":\"Contract sent\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.9\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":5,\"stageId\":\"11348547\",\"label\":\"Closed won\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null},{\"displayOrder\":6,\"stageId\":\"11348548\",\"label\":\"Closed lost\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"writePermissions\":\"CRM_PERMISSIONS_ENFORCEMENT\",\"isInternalWriteOnly\":null}],\"version\":null,\"updatedBy\":null}", "fromUserId": 9586504 }, { "portalId": 123456, "identifier": "123456:11348541", "action": "UPDATE", "timestamp": "2022-05-11T17:49:38.304Z", "message": "Pipeline update", "rawObject": "{\"displayOrder\":1,\"pipelineId\":\"11348541\",\"portalId\":123456,\"label\":\"Additional pipeline\",\"active\":true,\"stages\":[{\"displayOrder\":0,\"stageId\":\"11348542\",\"label\":\"Appointment scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.2\"},\"updatedBy\":9586504,\"updatedAt\":1652291367982,\"internalWriteOnly\":null},{\"displayOrder\":1,\"stageId\":\"11348543\",\"label\":\"Qualified to buy\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.4\"},\"updatedBy\":9586504,\"updatedAt\":1652291367982,\"internalWriteOnly\":null},{\"displayOrder\":2,\"stageId\":\"11348544\",\"label\":\"Presentation scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.6\"},\"updatedBy\":9586504,\"updatedAt\":1652291367982,\"internalWriteOnly\":null},{\"displayOrder\":3,\"stageId\":\"11348545\",\"label\":\"Decision Maker Bought-In\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":9586504,\"updatedAt\":1652291367982,\"internalWriteOnly\":null},{\"displayOrder\":4,\"stageId\":\"11348546\",\"label\":\"Contract sent\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.9\"},\"updatedBy\":9586504,\"updatedAt\":1652291367982,\"internalWriteOnly\":null},{\"displayOrder\":5,\"stageId\":\"11348547\",\"label\":\"Closed won\",\"metadata\":{\"isClosed\":\"true\",\"probability\":\"0.8\"},\"updatedBy\":9586504,\"updatedAt\":1652291367982,\"internalWriteOnly\":null},{\"displayOrder\":6,\"stageId\":\"11348548\",\"label\":\"Closed lost\",\"metadata\":{\"isClosed\":\"true\",\"probability\":\"0.8\"},\"updatedBy\":9586504,\"updatedAt\":1652291367982,\"internalWriteOnly\":null}],\"version\":1,\"updatedBy\":null}", "fromUserId": 9586504 }, { "portalId": 123456, "identifier": "123456:11348541", "action": "UPDATE", "timestamp": "2022-05-11T17:49:28.001Z", "message": "Pipeline update", "rawObject": "{\"displayOrder\":1,\"pipelineId\":\"11348541\",\"portalId\":123456,\"label\":\"Additional pipeline\",\"active\":true,\"stages\":[{\"displayOrder\":0,\"stageId\":\"11348542\",\"label\":\"Appointment scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.2\"},\"updatedBy\":null,\"updatedAt\":null,\"internalWriteOnly\":null},{\"displayOrder\":1,\"stageId\":\"11348543\",\"label\":\"Qualified to buy\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.4\"},\"updatedBy\":null,\"updatedAt\":null,\"internalWriteOnly\":null},{\"displayOrder\":2,\"stageId\":\"11348544\",\"label\":\"Presentation scheduled\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.6\"},\"updatedBy\":null,\"updatedAt\":null,\"internalWriteOnly\":null},{\"displayOrder\":3,\"stageId\":\"11348545\",\"label\":\"Decision Maker Bought-In\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"internalWriteOnly\":null},{\"displayOrder\":4,\"stageId\":\"11348546\",\"label\":\"Contract sent\",\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.9\"},\"updatedBy\":null,\"updatedAt\":null,\"internalWriteOnly\":null},{\"displayOrder\":5,\"stageId\":\"11348547\",\"label\":\"Closed won\",\"metadata\":{\"isClosed\":\"true\",\"probability\":\"0.8\"},\"updatedBy\":null,\"updatedAt\":null,\"internalWriteOnly\":null},{\"displayOrder\":6,\"stageId\":\"11348548\",\"label\":\"Closed lost\",\"metadata\":{\"isClosed\":\"true\",\"probability\":\"0.0\"},\"updatedBy\":null,\"updatedAt\":null,\"internalWriteOnly\":null}],\"version\":0,\"updatedBy\":null}", "fromUserId": 9586504 }, { "portalId": 123456, "identifier": "123456:11348541", "action": "CREATE", "timestamp": "2021-02-26T17:07:35.013Z", "message": "User defined. pipeline created", "rawObject": "{\"label\":\"Additional pipeline\",\"displayOrder\":1,\"active\":true,\"stages\":[{\"label\":\"Appointment scheduled\",\"displayOrder\":0,\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.2\"},\"portalId\":123456,\"pipelineId\":\"11348541\",\"stageId\":\"11348542\",\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":null,\"createdAt\":1614359253457,\"updatedAt\":null,\"active\":true},{\"label\":\"Qualified to buy\",\"displayOrder\":1,\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.4\"},\"portalId\":123456,\"pipelineId\":\"11348541\",\"stageId\":\"11348543\",\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":null,\"createdAt\":1614359253457,\"updatedAt\":null,\"active\":true},{\"label\":\"Presentation scheduled\",\"displayOrder\":2,\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.6\"},\"portalId\":123456,\"pipelineId\":\"11348541\",\"stageId\":\"11348544\",\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":null,\"createdAt\":1614359253457,\"updatedAt\":null,\"active\":true},{\"label\":\"Decision Maker Bought-In\",\"displayOrder\":3,\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.8\"},\"portalId\":123456,\"pipelineId\":\"11348541\",\"stageId\":\"11348545\",\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":null,\"createdAt\":1614359253457,\"updatedAt\":null,\"active\":true},{\"label\":\"Contract sent\",\"displayOrder\":4,\"metadata\":{\"isClosed\":\"false\",\"probability\":\"0.9\"},\"portalId\":123456,\"pipelineId\":\"11348541\",\"stageId\":\"11348546\",\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":null,\"createdAt\":1614359253457,\"updatedAt\":null,\"active\":true},{\"label\":\"Closed won\",\"displayOrder\":5,\"metadata\":{\"isClosed\":\"true\",\"probability\":\"1.0\"},\"portalId\":123456,\"pipelineId\":\"11348541\",\"stageId\":\"11348547\",\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":null,\"createdAt\":1614359253457,\"updatedAt\":null,\"active\":true},{\"label\":\"Closed lost\",\"displayOrder\":6,\"metadata\":{\"isClosed\":\"true\",\"probability\":\"0.0\"},\"portalId\":123456,\"pipelineId\":\"11348541\",\"stageId\":\"11348548\",\"maxVisibleVersion\":null,\"createdBy\":9586504,\"updatedBy\":null,\"createdAt\":1614359253457,\"updatedAt\":null,\"active\":true}],\"objectType\":\"DEAL\",\"objectTypeId\":\"0-3\",\"pipelineId\":\"11348541\",\"portalId\":123456,\"version\":0,\"createdBy\":9586504,\"updatedBy\":null,\"createdAt\":1614359253457,\"updatedAt\":null,\"limitExempt\":false,\"permission\":null,\"default\":false}", "fromUserId": 9586504 } ] } ``` # CRM API | Properties @ The CRM properties endpoints allow you to manage custom properties as well as view default property details for any object. $ guides & api/crm --- # Properties Use properties to store information on CRM records. HubSpot provides a set of default properties for each CRM object, and you can also create and manage your own custom properties either [in HubSpot](https://knowledge.hubspot.com/properties/create-and-edit-properties) or using the properties API. When creating properties, it’s important to consider how to architect your data. In many cases, creating custom properties for HubSpot's standard objects is the right course of action. However, there may be times when you'll need to create a separate [custom object](/guides/api/crm/objects/custom-objects) with its own set of properties. ## Default properties CRM objects are defined by a primary `type` and a set of `properties`. Each type has a unique set of standard properties, represented by a map of name-value pairs. Learn more about default properties for different objects: - [Contacts](https://knowledge.hubspot.com/properties/hubspots-default-contact-properties) - [Companies](https://knowledge.hubspot.com/properties/hubspot-crm-default-company-properties) - [Deals](https://knowledge.hubspot.com/properties/hubspots-default-deal-properties) - [Tickets](https://knowledge.hubspot.com/properties/hubspots-default-ticket-properties) - [Activities](https://knowledge.hubspot.com/properties/hubspots-default-activity-properties) (Calls, Emails, Meetings, Notes, Tasks) - [Leads](https://knowledge.hubspot.com/properties/hubspots-default-lead-properties)(_**Sales Hub**_ _Professional_ and _Enterprise_) ## Property groups [Property groups](https://knowledge.hubspot.com/properties/create-and-edit-properties#create-and-edit-property-groups) are used to group related properties. Any grouped properties will appear next to each other on HubSpot records. If your integration creates any custom object properties, a custom property group will make it easy to identify that data. ## Property type and fieldType values When creating or updating properties, both `type` and `fieldType` values are required. The `type` value determines the type of the property, i.e. a string or a number. The `fieldType` property determines how the property will appear in HubSpot or on a form, i.e. as a plain text field, a dropdown menu, or a date picker. In the table below, learn about the available property `type` and corresponding `fieldType` values. | `type` | Description | Valid `fieldType` values | | --- | --- | --- | | `bool` | A field containing binary options (e.g., `Yes` or `No`, `True` or `False`). | `booleancheckbox`, `calculation_equation` | | `enumeration` | A string representing a set of options, with options separated by a semicolon. | `booleancheckbox`, `checkbox`, `radio`, `select`, `calculation_equation` | | `date` | A value representing a specific day, month, and year. Values must be represented in UTC time and can be formatted as [ISO 8601 strings or EPOCH-timestamps in milliseconds](#add-values-to-date-and-datetime-properties) (i.e. midnight UTC). | `date` | | `datetime` | A value representing a specific day, month, year, and time of day. Values must be represented in UTC time and can be formatted as [ISO 8601 strings or UNIX-timestamps in milliseconds](#add-values-to-date-and-datetime-properties). | `date` | | `string` | A plain text string, limited to 65,536 characters. | `file`, `text`, `textarea`, `calculation_equation`, `html`, `phonenumber` | | `number` | A number value containing numeric digits and at most one decimal. | `number`, `calculation_equation` | | `object_coordinates` | A text value used to reference other HubSpot objects, used only for internal properties. Properties of this type cannot be created or edited, and are not visible in HubSpot. | `text` | | `json` | A text value stored as formatted JSON, used only for internal properties. Properties of this type cannot be created or edited, and are not visible in HubSpot. | `text` | Valid values for `fieldType` include: | Fieldtype | Description | | --- | --- | | `booleancheckbox` | An input that will allow users to selected one of either Yes or No. When used in a form, it will be displayed as a single checkbox. Learn how to [add a value to single checkbox properties](#add-values-to-checkbox-type-properties). | | `calculation_equation` | A custom equation that can calculate values based on other property values and/or associations. Learn how to define [calculation properties](#create-calculation-properties). | | `checkbox` | A list of checkboxes that will allow a user to select multiple options from a set of options allowed for the property. Learn how to [format values when updating multiple checkbox properties](#add-values-to-checkbox-type-properties). | | `date` | A date value, displayed as a date picker. | | `file` | Allows for a file to be uploaded on a record or via a form. Stores a file ID. | | `html` | A string, rendered as sanitized html, that enables the use of a rich text editor for the property. | | `number` | A string of numerals or numbers written in decimal or scientific notation. | | `phonenumber` | A plain text string, displayed as a formatted phone number. | | `radio` | An input that will allow users to select one of a set of options allowed for the property. When used in a form, this will be displayed as a set of radio buttons. | | `select` | A dropdown input that will allow users to select one of a set of options allowed for the property. | | `text` | A plain text string, displayed in a single line text input. | | `textarea` | A plain text string, displayed as a multi-line text input. | ## Create a property To create a property, make a `POST` request to `/crm/v3/properties/{objectType}`. In your request body, include the following required fields: - `groupName`: the [property group](https://knowledge.hubspot.com/properties/organize-and-export-properties) the property will be in. - `name`: the internal name of the property (e.g., favorite_food). - `label`: the name of the property as it appears in HubSpot (e.g., Favorite Food). - `type`: the [type](#property-type-and-fieldtype-values) of property. - `fieldType`: the [field type](#property-type-and-fieldtype-values) of the property. For example, to create a contact property called _Favorite Food_, your request would look like: ```json { "groupName": "contactinformation", "name": "favorite_food", "label": "Favorite Food", "type": "string", "fieldType": "text" } ``` ## Create unique identifier properties When a record is created in HubSpot, a unique ID (`hs_object_id`) is automatically generated and should be treated as a string. These IDs are unique only within that object type, so there can be both a contact and company with the same ID. For contacts and companies, there are additional unique identifiers, including a contact's email address (`email`) and a company's domain name (`domain`). In some cases, you want may to create your own unique identifier property so that you can't enter the same value for multiple records. You can have up to ten unique ID properties per object. To create a property requiring unique values via API: - Make a `POST` request to `/crm/v3/properties/{objectType}`. - In your request body, for the `hasUniqueValue` field, set the value to `true`. ```json { "groupName": "dealinformation", "name": "system_a_unique", "label": "Unique ID for System A", "hasUniqueValue": true, "type": "string", "fieldType": "text" } ``` Once you've created your unique ID property, you can use it in an API call to retrieve specific records. For example, to retrieve a deal with a value of `abc` for the `system_a_unique` property, your request URL would be: `/crm/v3/objects/deals/abc?idProperty=system_a_unique`. You can then use this unique identifier property value to identify and update specific records in the same way you could use `hs_object_id`, `email` (contacts), or `domain` (companies). ## Create calculation properties Calculation properties define a property value based on other properties within the same object record. They are defined using a formula, which may include operations like min, max, count, sum, or average. You can use the properties API to read or create calculation properties in your HubSpot account, using a field type of `calculation_equation` and a type of `number`, `bool`, `string`, or `enumeration`. You can define the property's calculation formula with the `calculationFormula` field. Calculation properties created via API <u>cannot</u> be edited within HubSpot. You can only edit these properties via the properties API. ### Calculation property syntax Using `calculationFormula`, you can write your formula with arithmetic operators, comparison operators, logic operators, conditional statements, and other functions. #### Literal syntax - **String literal**: constant strings can be represented with either single quotes (`'constant'`) or double quotes (`"constant"`). - **Number literal**: constant numbers can be any real numbers, and can include point notation. `1005` and `1.5589` are both valid constant numbers. - **Boolean literal**: constant booleans can be `true` or `false`. #### Property syntax - **String property variables:** for an identifier string to be interpreted as a string property, it must be wrapped in the `string` function. For example, `string(var1)`will be interpreted as the value for the string property var1. - **Number property variables**: all identifiers will be interpreted as number property variables. For example, `var1` will be interpreted as the value for the number property var1. - **Boolean property variables**: for an identifier to be interpreted as a bool property, it must be wrapped in the `bool` function. For example, the identifier `bool(var1)` will be interpreted as the value for the boolean property var1. The language used is case sensitive for all types <u>except</u> strings. For example, `If A ThEn B` is exactly the same as `if a then b` but `'a'` is not the same as `'A'`. Spaces, tabs, and new lines will be used for tokenization but will be ignored. #### Operators Operators can be used with literal and property values. For arithmetic operators, you can use prefix notation to multiply, and parenthesis can be used to specify the order of operations. | Operator | Description | Examples | | --- | --- | --- | | `+` | Add numbers or strings. | `property1 + 100` | | `-` | Subtract numbers. | `property1 + 100 - property2` | | `*` | Multiply numbers. | `10property1` \= `10 * property1` | | `/` | Divide numbers. | `property1 * (100 - property2/(50 - property3))` | | `<` | Checks if a value is less than another. Supported by number properties or constants. | `a < 100` | | `>` | Checks if a value is greater than another. Supported by number properties or constants. | `a > 50` | | `<=` | Checks if a value is less than or equal to another. Supported by number properties or constants. | `a <= b` | | `>=` | Checks if a value is greater than or equal to another. Supported by number properties or constants. | `b>= c` | | `=` | Checks if a value is equal to another. Supported by both numbers and strings. | `(a + b - 100c * 150.652) = 150-230b` | | `equals` | Checks if a value is equal to another. Supported by both numbers and strings. | `a + b - 100.2c * 150 equals 150 - 230` | | `!=` | Checks if a value is not equal to another. Supported by both numbers and strings. | `string(property1) != 'test_string'` | | `or` | Checks if either or two values are true. | `a > b or b <= c` | | `and` | Checks if both values are true. | `bool(a) and bool(c)` | | `not` | Checks if none of the values are true. | `not (bool(a) and bool(c))` | #### Functions The following are supported functions: | Function | Description | Examples | | --- | --- | --- | | `max` | Will have between 2 and 100 input numbers, and will return the maximum number out of all the inputs. | `max(a, b, c, 100)` or `max(a, b)` | | `min` | Will have between 2 and 100 input numbers, and will return the minimum number of out all the inputs. | `min(a, b, c, 100)` or `min(a, b)` | | `is_present` | Evaluates whether an expression can be evaluated. | `is_present(bool(a))`\= true if the property is boolean, but `is_present(bool(a))` = false if the property is empty or not boolean. | | `contains` | Has two strings as inputs and will return true if the first input contains the second. | `contains('hello', 'ello')` = `true` while `contains('ello', 'hello')` = false. | | `concatenate` | Joins a list of strings. The list of inputs can go from 2 up to 100. | `concatenate('a', 'b', string(a), string(b))` | There are also two parsing functions: - `number_to_string`: tries to convert the input number expression to a string. - `string_to_number`: tries to convert the input string expression to a number. For example, `"Number of cars: " + num_cars` is not a valid property because you can't add a string with a number, but `"Number of cars: " + number_to_string(num_cars)` is. #### Conditional statements You can also write your formula with conditional statements using `if`, `elseif`, `endif`, and `else`. For example, a conditional statement could look like: `if boolean_expression then statement [elseif expression then statement]* [else statement | endif]` where the `[a]` brackets represent that a is optional, the `a|b` represent that either a or b will work, and `*` means 0 or more. `endif` can be used to finish a conditional statement prematurely, ensuring that the parser can identify which `if` the next `elseif` belongs to. ### Example formulas The following are examples you can use to help define your own calculation formulas: ```json "calculationFormula": "closed - started" ``` A more advanced example with conditionals: ```json "calculationFormula": "if is_present(hs_latest_sequence_enrolled_date) then if is_present(hs_sequences_actively_enrolled_count) an hs_sequences_actively_enrolled_count >= 1 then true else false else ''" ``` ## Retrieve properties You can retrieve information for individual properties or all properties within an object. - To retrieve an individual property, make a `GET` request to `crm/v3/properties/{object}/{propertyName}`. For example, to retrieve the `favorite_food` property, your request URL would be `/crm/v3/properties/contacts/favorite_food`. - To retrieve all properties for an object, make a `GET` request to `/crm/v3/properties/{objectType}`. When retriving all properties, by default only non-sensitive properties are returned. To retrieve sensitive data properties, include the `dataSensitivity` query parameter with the value `sensitive`. Learn more about [managing sensitive data via API](/reference/api/crm/sensitive-data#manage-sensitive-data) (BETA, _Enterprise_ only). ## Update or clear a property's values To update a property value for a record, make a `PATCH` request to `crm/v3/objects/{objectType}/{recordId}`. In your request body, include the properties and their values in an array. Learn more about updating records via the [object APIs](/guides/api/crm/understanding-the-crm). ### Add values to date and datetime properties Time values are represented in ISO 8601 format in responses, but HubSpot APIs will accept either of two formats for date and time values: - **ISO 8601 formatted strings**: depending on the type of data, these will be one of two different formats: - For values that represent a specific date, the complete date format will be used: YYYY-MM-DD (e.g. `2020-02-29`) - For values that represent a specific date and time, the complete date plus hours, minutes, seconds, and a decimal fraction of a second format will be used: YYYY-MM-DDThh:mm:ss.sTZD (e.g. `2020-02-29T03:30:17.000Z`). All times are represented in UTC, so the values will always use the UTC designator "Z." - **UNIX-formatted timestamps in milliseconds**: timestamp values in milliseconds, which are represented in UTC time. For example, the timestamp value `1427997766000` translates to _02 Apr 2015 18:02:46 UTC_, or _April 2nd, 2015, 2:02:46 PM EDT_ (Eastern Daylight Saving Time). There are two types of properties for storing dates (`date` and `datetime`) which also affect how you format values: - `date` properties store the date, <u>not</u> the time. `date` properties display the date they're set to, regardless of the time zone setting of the account or user. For `date` property values, it is recommended to use the ISO 8601 complete date format. If you use the UNIX timestamp format, you must use an EPOCH millisecond timestamp (i.e. the value must be set to midnight UTC for the date). For example, to represent May 1, 2015 in either format: - **IOS 8601**: 2015-05-01 - **UNIX millisecond timestamp**: 1430438400000 - `datetime` properties store <u>both</u> the date and time. Either timestamp format will be accepted. In HubSpot, `datetime` properties are displayed based on the time zone of the user viewing the record, so the value will be converted to the local time zone of the user. ### Add values to checkbox type properties When updating values for a record's checkbox type properties, format the values in the following ways: - **Boolean** **checkbox property**: to display as _Yes_, or checked in HubSpot, your value must be `true`. To display as _No_ or not checked in HubSpot, your value must be `false`. - **Multiple select** **checkbox property**: to add or append values to a multiple checkboxes property, add a semicolon before the first value, and separate the values with semicolons without a space between. If the property has an existing value, the leading semicolon will append the values instead of overwriting the value. For example, a contact has the existing value `DECISION_MAKER` for the `hs_buying_role` property. To add additional values without replacing the existing value, your request would look like this: ```json { "properties": { "hs_buying_role": ";BUDGET_HOLDER;END_USER" } } ``` ### Assign record owners with user properties When assigning users to CRM records via API, your value must be user's owner `id`, which you can find in your [property settings](https://knowledge.hubspot.com/properties/create-and-edit-properties) or via the [owners API](/guides/api/crm/owners). For example, to assign a user as owner of a contact, send a `PATCH` request to `crm/v3/objects/contacts/{contactId}`, with the body `{ "properties":{ "hubspot_owner_id": "41629779"}}`. ### Clear a property value You can clear an object property value via the API by setting the property value to an empty string. For example, to clear the `firstname` from a contact object, send a `PATCH` request to `/crm/v3/objects/contacts/{contactId}` with the body `{ "properties": { "firstname": ""}}`. # CRM API | Property validations @ The property validations endpoints allow you to view validation rules for your properties. $ guides & api/crm --- # Property validations [Property validation rules](https://knowledge.hubspot.com/properties/set-validation-rules-for-properties) determine formatting requirements for text, date picker, and number properties. For example, an Account ID property that can only include numbers. You can use the property validations API to retrieve validation rules so you’re aware of formatting requirements when setting or updating property values. To complete more actions with properties, refer to the [Properties API](/guides/api/crm/properties). ## Retrieve all properties with validation rules for an object To view all properties of an object that have validation rules, make a `GET` request to `/crm/v3/property-validations/{objectTypeId}`. Refer to [this guide](/guides/api/crm/understanding-the-crm#object-type-ids) for a full list of `objectTypeId` values. Properties with validation rules are returned with the following fields: - `propertyName`: the name of the property with validation rules. - `propertyValidationRules`: an array with the rules set for the property. Includes each rule’s `ruleType` (the category for the rule) and `ruleArguments` (specific requirements of the rule). For example, to request contact properties with validation rules, make a `GET` request to `/crm/v3/property-validations/0-1`. In the following response, there are two properties with rules, the `zip` (Postal Code) property which only allows numeric characters and the `age` property which requires a minimum value of 1. ```json { "results": [ { "propertyName": "zip", "propertyValidationRules": [ { "ruleType": "ALPHANUMERIC", "ruleArguments": ["NUMERIC_ONLY"] } ] }, { "propertyName": "age", "propertyValidationRules": [ { "ruleType": "MIN_NUMBER", "ruleArguments": ["1"] } ] } ] } ``` ## Retrieve validation rules for a property To view the validation rules set for a specific property, make a `GET` request to `/crm/v3/property-validations/{objectTypeId}/{propertyName}`. You can refer to [this guide](https://developers.hubspot.com/docs/guides/api/crm/understanding-the-crm#object-type-ids) for a full list of `objectTypeId` values and use the [Properties API](/guides/api/crm/properties) to retrieve `propertyName` values. The property’s rules are returned with the `ruleType` and `ruleArguments` for each. For example, to view validation rules for an Order ID deal property, make a `GET` request to `/crm/v3/property-validations/0-3/order_id`. In the following response, the property has three rules that require the value to contain between one and eight numeric characters. ```json { "results": [ { "ruleType": "ALPHANUMERIC", "ruleArguments": ["NUMERIC_ONLY"] }, { "ruleType": "MAX_LENGTH", "ruleArguments": ["10"] }, { "ruleType": "MIN_LENGTH", "ruleArguments": ["1"] } ] } ``` # CRM API | Search @ The CRM search endpoints make getting data more efficient by allowing developers to filter, sort, and search across any CRM object type. $ guides & api/crm --- # Search Use the CRM search endpoints to filter, sort, and search objects, records, and engagements across your CRM. For example, use the endpoints to get a list of contacts in your account, or a list of all open deals. To use these endpoints from an app, a CRM scope is required. Refer to this [list of available scopes](/guides/apps/authentication/working-with-oauth#scopes) to learn which granular CRM scopes can be used to accomplish your goal. ## Make a search request To search your CRM, make a `POST` request to the object's search endpoint. CRM search endpoints are constructed using the following format: `/crm/v3/objects/{object}/search`. In the request body, you'll include [filters](#filter-search-results) to narrow your search by CRM property values. For example, the code snippet below would retrieve a list of all contacts that have a specific company email address. ```json { "filterGroups": [ { "filters": [ { "propertyName": "email", "operator": "CONTAINS_TOKEN", "value": "*@hubspot.com" } ] } ] } ``` Each object that you search will include a set of [default properties](#objects) that gets returned. For contacts, a search will return `createdate`, `email`, `firstname`, `hs_object_id`, `lastmodifieddate`, and `lastname`. For example, the above request would return the following response: ```json { "total": 2, "results": [ { "id": "100451", "properties": { "createdate": "2024-01-17T19:55:04.281Z", "email": "testperson@hubspot.com", "firstname": "Test", "hs_object_id": "100451", "lastmodifieddate": "2024-09-11T13:27:39.356Z", "lastname": "Person" }, "createdAt": "2024-01-17T19:55:04.281Z", "updatedAt": "2024-09-11T13:27:39.356Z", "archived": false }, { "id": "57156923994", "properties": { "createdate": "2024-09-11T18:21:50.012Z", "email": "emailmaria@hubspot.com", "firstname": "Maria", "hs_object_id": "57156923994", "lastmodifieddate": "2024-10-21T21:36:02.961Z", "lastname": "Johnson (Sample Contact)" }, "createdAt": "2024-09-11T18:21:50.012Z", "updatedAt": "2024-10-21T21:36:02.961Z", "archived": false } ] } ``` To return a specific set of properties, include a `properties` array in the request body. For example: ```json { "filterGroups": [ { "filters": [ { "propertyName": "annualrevenue", "operator": "GT", "value": "10000000" } ] } ], "properties": ["annualrevenue", "name"] } ``` The response for the above request would look like: ```json { "total": 38, "results": [ { "id": "2810868468", "properties": { "annualrevenue": "1000000000", "createdate": "2020-01-09T20:11:27.309Z", "hs_lastmodifieddate": "2024-09-13T20:23:03.333Z", "hs_object_id": "2810868468", "name": "Google" }, "createdAt": "2020-01-09T20:11:27.309Z", "updatedAt": "2024-09-13T20:23:03.333Z", "archived": false }, { "id": "2823023532", "properties": { "annualrevenue": "10000000000", "createdate": "2020-01-13T16:21:08.270Z", "hs_lastmodifieddate": "2024-09-13T20:23:03.064Z", "hs_object_id": "2823023532", "name": "Pepsi" }, "createdAt": "2020-01-13T16:21:08.270Z", "updatedAt": "2024-09-13T20:23:03.064Z", "archived": false }, { "id": "5281147580", "properties": { "annualrevenue": "50000000", "createdate": "2021-02-01T21:17:12.250Z", "hs_lastmodifieddate": "2024-09-13T20:23:03.332Z", "hs_object_id": "5281147580", "name": "CORKCICLE" }, "createdAt": "2021-02-01T21:17:12.250Z", "updatedAt": "2024-09-13T20:23:03.332Z", "archived": false }, { "id": "5281147581", "properties": { "annualrevenue": "1000000000", "createdate": "2021-02-01T21:17:12.250Z", "hs_lastmodifieddate": "2024-09-13T20:23:03.064Z", "hs_object_id": "5281147581", "name": "Ulta Beauty" }, "createdAt": "2021-02-01T21:17:12.250Z", "updatedAt": "2024-09-13T20:23:03.064Z", "archived": false }, { "id": "5281147583", "properties": { "annualrevenue": "50000000", "createdate": "2021-02-01T21:17:12.251Z", "hs_lastmodifieddate": "2024-09-13T20:23:03.332Z", "hs_object_id": "5281147583", "name": "Narvar" }, "createdAt": "2021-02-01T21:17:12.251Z", "updatedAt": "2024-09-13T20:23:03.332Z", "archived": false }, { "id": "5281496154", "properties": { "annualrevenue": "1000000000", "createdate": "2021-02-01T21:17:12.267Z", "hs_lastmodifieddate": "2024-09-13T20:23:03.332Z", "hs_object_id": "5281496154", "name": "Etsy Inc" }, "createdAt": "2021-02-01T21:17:12.267Z", "updatedAt": "2024-09-13T20:23:03.332Z", "archived": false }, { "id": "5281496155", "properties": { "annualrevenue": "1000000000", "createdate": "2021-02-01T21:17:12.267Z", "hs_lastmodifieddate": "2024-09-13T20:23:03.069Z", "hs_object_id": "5281496155", "name": "grubhub" }, "createdAt": "2021-02-01T21:17:12.267Z", "updatedAt": "2024-09-13T20:23:03.069Z", "archived": false }, { "id": "5281496157", "properties": { "annualrevenue": "1000000000", "createdate": "2021-02-01T21:17:12.267Z", "hs_lastmodifieddate": "2024-09-13T20:23:03.332Z", "hs_object_id": "5281496157", "name": "discover" }, "createdAt": "2021-02-01T21:17:12.267Z", "updatedAt": "2024-09-13T20:23:03.332Z", "archived": false }, { "id": "5281496158", "properties": { "annualrevenue": "50000000", "createdate": "2021-02-01T21:17:12.268Z", "hs_lastmodifieddate": "2024-09-13T20:23:03.064Z", "hs_object_id": "5281496158", "name": "Soludos" }, "createdAt": "2021-02-01T21:17:12.268Z", "updatedAt": "2024-09-13T20:23:03.064Z", "archived": false }, { "id": "5281499282", "properties": { "annualrevenue": "1000000000", "createdate": "2021-02-01T21:17:12.285Z", "hs_lastmodifieddate": "2024-09-13T20:23:03.066Z", "hs_object_id": "5281499282", "name": "AEO Management Co." }, "createdAt": "2021-02-01T21:17:12.285Z", "updatedAt": "2024-09-13T20:23:03.066Z", "archived": false } ], "paging": { "next": { "after": "10" } } } ``` ## Searchable CRM objects and engagements ### Objects The tables below contain the object search endpoints, the objects they refer to, and the properties that are returned by default. | Search endpoint | Object | Default returned properties | | --- | --- | --- | | `/crm/v3/objects/carts/search` | Carts | `createdate`, `hs_lastmodifieddate`, `hs_object_id` | | `/crm/v3/objects/companies/search` | Companies | `name`, `domain`, `createdate`,`hs_lastmodifieddate`, `hs_object_id` | | `/crm/v3/objects/contacts/search` | Contacts | `firstname`,`lastname`,`email`,`lastmodifieddate`,`hs_object_id`, `createdate` | | `/crm/v3/objects/deals/search` | Deals | `dealname`, `amount`, `closedate,``pipeline`,`dealstage`, `createdate`, `hs_lastmodifieddate`, `hs_object_id` | | `/crm/v3/objects/deal_split/search` | Deal splits | `hs_createdate`, `hs_lastmodifieddate`, `hs_object_id` | | `/crm/v3/objects/discounts/search` | Discounts | `createdate`, `hs_lastmodifieddate`, `hs_object_id` | | `/crm/v3/objects/feedback_submissions/search` | Feedback submissions | `hs_createdate`,`hs_lastmodifieddate`,`hs_object_id` | | `/crm/v3/objects/fees/search` | Fees | `createdate`, `hs_lastmodifieddate`, `hs_object_id` | | `/crm/v3/objects/invoices/search` | Invoices | `createdate`, `hs_lastmodifieddate`, `hs_object_id` | | `/crm/v3/objects/leads/search` | Leads | `createdate`, `hs_lastmodifieddate`, `hs_object_id` | | `/crm/v3/objects/line_items/search` | Line items | `quantity`, `amount`, `price`, `createdate`, `hs_lastmodifieddate`, `hs_object_id` | | `/crm/v3/objects/orders/search` | Orders | `createdate`, `hs_lastmodifieddate`, `hs_object_id` | | `/crm/v3/objects/commerce_payments/search` | Payments | `createdate`, `hs_lastmodifieddate`, `hs_object_id` | | `/crm/v3/objects/products/search` | Products | `name`, `description` ,`price`, `createdate`, `hs_lastmodifieddate`, `hs_object_id` | | `/crm/v3/objects/quotes/search` | Quotes | `hs_expiration_date`, `hs_public_url_key`, `hs_status`,`hs_title`, `hs_createdate`, `hs_lastmodifieddate`,`hs_object_id` | | `/crm/v3/objects/subscriptions/search` | Subscriptions (Commerce) | `hs_createdate`, `hs_lastmodifieddate`, `hs_object_id` | | `/crm/v3/objects/taxes/search` | Taxes | `createdate`, `hs_lastmodifieddate`, `hs_object_id` | | `/crm/v3/objects/tickets/search` | Tickets | `content`, `hs_pipeline`, `hs_pipeline_stage`,`hs_ticket_category`, `hs_ticket_priority`, `subject`,`createdate`, `hs_lastmodifieddate`, `hs_object_id` | ### Engagements The table below contains the engagement search endpoints, the engagements they refer to, and the properties that are returned by default. | Search endpoint | Engagement | Default returned properties | | --- | --- | --- | | `/crm/v3/objects/calls/search` | Calls | `hs_createdate`,`hs_lastmodifieddate`,`hs_object_id` | | `/crm/v3/objects/emails/search` | Emails | `hs_createdate`,`hs_lastmodifieddate`,`hs_object_id` | | `/crm/v3/objects/meetings/search` | Meetings | `hs_createdate`,`hs_lastmodifieddate`,`hs_object_id` | | `/crm/v3/objects/notes/search` | Notes | `hs_createdate`,`hs_lastmodifieddate`,`hs_object_id` | | `/crm/v3/objects/tasks/search` | Tasks | `hs_createdate`,`hs_lastmodifieddate`,`hs_object_id` | ## Search default searchable properties Search all default text properties in records of the specified object to find all records that have a value containing the specified string. By default, the results will be returned in order of object creation (oldest first), but you can override this with [sorting](#sorting). For example, the request below searches for all contacts with a default text property value containing the letter `X`. ```shell curl https://api.hubapi.com/crm/v3/objects/contacts/search \ --request POST \ --header "Content-Type: application/json" \ --header "authorization: Bearer YOUR_ACCESS_TOKEN" \ --data '{ "query": "x" }' ``` Below are the properties that are searched by default through the above method: | Search endpoint | Object | Default searchable properties | | --- | --- | --- | | `/crm/v3/objects/calls/search` | Calls | `hs_call_title`, `hs_body_preview` | | `/crm/v3/objects/companies/search` | Companies | `website`, `phone`, `name`, `domain` | | `/crm/v3/objects/contacts/search` | Contacts | `firstname`,`lastname`,`email`,`phone`,`hs_additional_emails`, `fax`, `mobilephone`, `company`, `hs_marketable_until_renewal` | | `/crm/v3/objects/{objectType}/search` | Custom objects | Up to 20 selected properties. | | `/crm/v3/objects/deals/search` | Deals | `dealname`,`pipeline`,`dealstage`, `description`, `dealtype` | | `/crm/v3/objects/emails/search` | Emails | `hs_email_subject` | | `/crm/v3/objects/feedback_submissions/search` | Feedback submissions | `hs_submission_name`, `hs_content` | | `/crm/v3/objects/meetings/search` | Meetings | `hs_meeting_title`, `hs_meeting_body` | | `/crm/v3/objects/notes/search` | Notes | `hs_note_body` | | `/crm/v3/objects/products/search` | Products | `name`, `description` ,`price`, `hs_sku` | | `/crm/v3/objects/quotes/search` | Quotes | `hs_sender_firstname`, `hs_sender_lastname`, `hs_proposal_slug`, `hs_title`, `hs_sender_company_name`, `hs_sender_email`, `hs_quote_number`, `hs_public_url_key` | | `/crm/v3/objects/tasks/search` | Tasks | `hs_task_body`, `hs_task_subject` | | `/crm/v3/objects/tickets/search` | Tickets | `subject`, `content`, `hs_pipeline_stage`, `hs_ticket_category`, `hs_ticket_id` | ## Filter search results Use filters in the request body to limit the results to only records with matching property values. For example, the request below searches for all contacts with a first name of _Alice._ When filtering search results for calls, conversations, emails, meetings, notes, or tasks, the property `hs_body_preview_html` is not supported. For emails, the properties `hs_email_html` and `hs_body_preview` are also not supported. ```shell curl https://api.hubapi.com/crm/v3/objects/contacts/search \ --request POST \ --header "Content-Type: application/json" \ --header "authorization: Bearer YOUR_ACCESS_TOKEN" \ --data '{ "filterGroups":[ { "filters":[ { "propertyName": "firstname", "operator": "EQ", "value": "Alice" } ] } ] }' ``` To include multiple filter criteria, you can group `filters` within **`filterGroups`**: - To apply _AND_ logic, include a comma-separated list of conditions within one set of `filters`. - To apply _OR_ logic, include multiple `filters` within a `filterGroup`.You can include a maximum of five `filterGroups` with up to 6 `filters` in each group, with a maximum of 18 filters in total. If you've included too many groups or filters, you'll receive a `VALIDATION_ERROR` error response. For example, the request below searches for contacts with the first name `Alice` AND a last name other than `Smith`_,_ OR contacts that don't have a value for the property `email`. ```shell curl https://api.hubapi.com/crm/v3/objects/contacts/search \ --request POST \ --header "Content-Type: application/json" \ --header "authorization: Bearer YOUR_ACCESS_TOKEN" \ --data '{ "filterGroups": [ { "filters": [ { "propertyName": "firstname", "operator": "EQ", "value": "Alice" }, { "propertyName": "lastname", "operator": "NEQ", "value": "Smith" } ] }, { "filters": [ { "propertyName": "email", "operator": "NOT_HAS_PROPERTY" } ] } ] }' ``` You can use operators in filters to specify which records should be returned. Values in filters are case-insensitive, with the following two exceptions: - When filtering for an enumeration property, search is case-sensitive for all filter operators. - When filtering for a string property using `IN` and `NOT_IN` operators, the searched values must be lowercase.Below are the available filter operators: | Operator | Description | | --- | --- | | `LT` | Less than the specified value. | | `LTE` | Less than or equal to the specified value. | | `GT` | Greater than the specified value. | | `GTE` | Greater than or equal to the specified value. | | `EQ` | Equal to the specified value. | | `NEQ` | Not equal to the specified value. | | `BETWEEN` | Within the specified range. In your request, use key-value pairs to set `highValue` and `value`. Refer to the example below the table. | | `IN` | Included within the specified list. Searches by exact match. In your request, include the list values in a `values` array. When searching a string property with this operator, values must be lowercase. Refer to the example below the table. | | `NOT_IN` | Not included within the specified list. In your request, include the list values in a `values` array. When searching a string property with this operator, values must be lowercase. | | `HAS_PROPERTY` | Has a value for the specified property. | | `NOT_HAS_PROPERTY` | Doesn't have a value for the specified property. | | `CONTAINS_TOKEN` | Contains a token. In your request, you can use wildcards (\*) to complete a partial search. For example, use the value `*@hubspot.com` to retrieve contacts with a HubSpot email address. | | `NOT_CONTAINS_TOKEN` | Doesn't contain a token. | For example, you can use the `BETWEEN` operator to search for all tasks that were last modified within a specific time range: ```shell curl https://api.hubapi.com/crm/v3/objects/tasks/search \ --request POST \ --header "Content-Type: application/json" \ --header "authorization: Bearer YOUR_ACCESS_TOKEN" \ --data '{ "filterGroups":[{ "filters":[ { "propertyName":"hs_lastmodifieddate", "operator":"BETWEEN", "highValue": "1642672800000", "value":"1579514400000" } ] }] }' ``` For another example, you can use the `IN` operator to search for companies that have specified values selected in a dropdown property. ```shell curl https://api.hubapi.com/crm/v3/objects/companies/search \ --request POST \ --header "Content-Type: application/json" \ --header "authorization: Bearer YOUR_ACCESS_TOKEN" \ --data '{ "filterGroups":[ { "filters":[ { "propertyName":"enumeration_property", "operator":"IN", "values": ["value_1", "value_2"] } ] } ], "properties": ["annualrevenue", "enumeration_property", "name"] }' ``` ## Search through associations Search for records that are associated with other specific records by using the pseudo-property `associations.{objectType}`. For example, the request below searches for all tickets associated with a contact that has the contact ID of `123`: ```shell curl https://api.hubapi.com/crm/v3/objects/tickets/search \ --request POST \ --header "Content-Type: application/json" \ --header "authorization: Bearer YOUR_ACCESS_TOKEN" \ --data '{ "filters": [ { "propertyName": "associations.contact", "operator": "EQ", "value": "123" } ] }' ``` The option to search through custom object associations is not currently supported via search endpoints. To find custom object associations, you can use the [associations API](/guides/api/crm/associations/associations-v4). ## Sort search results Use a sorting rule in the request body to list results in ascending or descending order. Only one sorting rule can be applied to any search. For example, the request below sorts returned contacts with most recently created first: ```shell curl https://api.hubapi.com/crm/v3/objects/contacts/search \ --request POST \ --header "Content-Type: application/json" \ --header "authorization: Bearer YOUR_ACCESS_TOKEN" \ --data '{ "sorts": [ { "propertyName": "createdate", "direction": "DESCENDING" } ] }' ``` ## Paging through results By default, the search endpoints will return pages of 10 records at a time. This can be changed by setting the `limit` parameter in the request body. The maximum number of supported objects per page is 200. For example, the request below would return pages containing 20 results each. ```shell curl https://api.hubapi.com/crm/v3/objects/contacts/search \ --request POST \ --header "Content-Type: application/json" \ --header "authorization: Bearer YOUR_ACCESS_TOKEN" \ --data '{ "limit": 20 }' ``` To access the next page of results, you must pass an **`after`** parameter provided in the **`paging.next.after`** property of the previous response. If the **`paging.next.after`** property isn’t provided, there are no additional results to display. You must format the value in the `after` parameter as an integer. For example, the request below would return the next page of results: ```shell curl https://api.hubapi.com/crm/v3/objects/contacts/search \ --request POST \ --header "Content-Type: application/json" \ --header "authorization: Bearer YOUR_ACCESS_TOKEN" \ --data '{ "after": "20" }' ``` ## Limitations - It may take a few moments for newly created or updated CRM objects to appear in search results. - Archived CRM objects won’t appear in any search results. - The search endpoints are [rate limited](/guides/apps/api-usage/usage-details) to <u>five</u> requests per second. - The maximum number of supported objects per page is 200. - A query can contain a maximum of 3,000 characters. If the body of your request exceeds 3,000 characters, a 400 error will be returned. - The search endpoints are limited to 10,000 total results for any given query. Attempting to page beyond 10,000 will result in a 400 error. - When [filtering](#filter-search-results), you can include a maximum of 5 `filterGroups` with up to 6 `filters` in each group, with a maximum of 18 filters in total. - When searching for phone numbers, HubSpot uses special calculated properties to standardize the format. These properties all start with `hs_searchable_calculated_*`. As a part of this standardization, HubSpot only uses the area code and local number. You should refrain from including the country code in your search or filter criteria. # Understanding the CRM APIs @ HubSpot’s CRM is a system for managing customer relationships and data. Learn about CRM objects and the related CRM APIs. $ guides & api/crm --- # Understanding the CRM APIs The foundation of your HubSpot account is a database of your business relationships and processes, called the CRM (Customer Relationship Management). To manage this data, HubSpot accounts include objects, which represent types of relationships or processes. Individual instances of objects, called records, represent the individual entities under each object type (e.g., John Smith is a contact). To store data in each record, you'll use properties (e.g., the email property) and to represent the relationships between individual entities, you can associate records with one another (e.g., associate John Smith with a company Smith & Co.). Further, CRM records can also store information about interactions through associated engagements/activities, such as emails, calls, and meetings. Below, learn about CRM objects, records, properties, associations, pipelines, and searching the CRM. To learn more about managing your CRM database from within HubSpot, check out [HubSpot's Knowledge Base](https://knowledge.hubspot.com/get-started/manage-your-crm-database). ## Object APIs The object APIs provide access to records and activities. For supported objects, you can use object endpoints and replace the `{objectTypeId}` in the request URL with the desired object. For example, to create contacts, you'd make a `POST` request to `crm/v3/objects/0-1` and to create courses, the request would be to `crm/v3/objects/0-410`. Refer to [this article](/guides/api/crm/using-object-apis) for more information about using the object endpoints for various objects. Some objects have limited API functionality. For more details, click the link to an object's endpoints reference documentation in the table below. If an object listed doesn't have its own doc, you can refer to the [objects API](https://developers.hubspot.com/docs/reference/api/crm/objects/objects) doc and substitute the `{objectTypeId}` in each endpoint to your desired object. ### Object type IDs When using CRM and other APIs, you'll need to use the `objectTypeId` field, which is a unique numerical value assigned to each object. For example, to retrieve records, you'd make a `GET` request to `/crm/v3/objects/{objectTypeId}`, or when creating a property for an object, you'd make a `POST` request to `/crm/v3/properties/{objectTypeId}`.The object type ID values are listed in the table below: | Type ID | Object | Description | | --- | --- | --- | | `0-2` | Companies | Stores information about a business or organization. View the [companies API](/reference/api/crm/objects/companies) | | `0-1` | Contacts | Stores information about an individual person. View the [contacts API](/reference/api/crm/objects/contacts) | | `0-3` | Deals | Represent sales opportunities and transactions, tracked through pipeline stages. View the [deals API](/reference/api/crm/objects/deals) | | `0-5` | Tickets | Represent customer requests for help or support, tracked through pipeline statuses. View the [tickets API](/reference/api/crm/objects/tickets) | | `0-421` | Appointments | Represent encounters or services scheduled for an individual. View the [objects API](https://developers.hubspot.com/docs/reference/api/crm/objects/objects) | | `0-48` | Calls | A type of activity that represents phone call interactions associated with your records. View the [calls API](/reference/api/crm/engagements/calls) | | `0-18` | Communications | A type of activity that represents SMS, LinkedIn, and WhatsApp message interactions associated with your records. View the [communications API](/reference/api/crm/engagements/communications) | | `0-410` | Courses | Represent structured programs or series of lessons, trainings, or educational modules. View the [objects API](https://developers.hubspot.com/docs/reference/api/crm/objects/objects) | | `2-XXX` | Custom objects | Stores data that doesn't fit in with existing objects. To find the `objectTypeId` for a custom object, make a `GET` request to `/crm/v3/schemas`. View the [objects API](https://developers.hubspot.com/docs/reference/api/crm/objects/objects) | | `0-49` | Emails | A type of activity that represents one-to-one email interactions associated with your records. View the [email API](/reference/api/crm/engagements/email) | | `0-19` | Feedback submissions | Stores information submitted to a feedback survey. Feedback submissions are associated with contact records. View the [feedback submissions API](/reference/api/crm/objects/feedback-submissions) | | `0-53` | Invoices | Represent the invoices sent for sales transactions. Invoices can be associated with contacts, companies, deals, line items, discounts, fees, and taxes. View the [invoices API](/reference/api/crm/commerce/invoices) | | `0-136` | Leads | Represent potential customers who have shown interest in your products or services. View the [leads API](/reference/api/crm/objects/leads) | | `0-8` | Line items | Represent individual products and services sold in a deal. Line items can be created from existing products in your product library, or can be created standalone. View the [line items API](/reference/api/crm/objects/line-items) | | `0-420` | Listings | Represent properties or units to be bought, sold, or rented. View the [objects API](https://developers.hubspot.com/docs/reference/api/crm/objects/objects) | | `0-54` | Marketing events | Represent events related to your marketing efforts, specifically including events from connected integrations. You can specify whether or not a contact attended, registered for, or cancelled attending a marketing event. View the [marketing events API](/reference/api/marketing/marketing-events) | | `0-47` | Meetings | A type of activity that represents meeting interactions associated with your records. View the [meetings API](/reference/api/crm/engagements/meetings) | | `0-46` | Notes | A type of activity that represents notes associated with your records. View the [notes API](/reference/api/crm/engagements/notes) | | `0-123` | Orders | Represent ecommerce purchases in HubSpot. View the [orders API](/reference/api/crm/commerce/orders) | | `0-101` | Payments | The payments made by buyers through invoices, payment links, and quotes. Payments can be associated with contacts, companies, deals, invoices, quotes, line items, subscriptions, discounts, fees, and taxes. View the [payments API](/reference/api/crm/commerce/payments) | | `0-116` | Postal mail | A type of activity that represents physical mail interactions associated with your records. View the [postal mail API](/reference/api/crm/engagements/postal-mail) | | `0-7` | Products | Represent goods or services for sale. Products can't be associated with other objects, but you can create line items based on products and associate those with deals and quotes. View the [products API](/reference/api/crm/objects/products) | | `0-14` | Quotes | Represent pricing information shared with potential buyers. Quotes must be [associated](https://developers.hubspot.com/docs/guides/api/crm/commerce/quotes#adding-associations) with a deal, line item, and quote template (`0-64`), but can also be associated with contacts, companies, discounts, fees, and taxes. View the [quotes API](/reference/api/crm/commerce/quotes) | | `0-162` | Services | Represent intangible offerings provided to customers. Examples include onboarding and consulting, repairs and maintenance, and personal care. View the [objects API](https://developers.hubspot.com/docs/reference/api/crm/objects/objects) | | `0-69` | Subscriptions | Represent recurring payments scheduled through payment links and quotes. Invoices can be associated with contacts, companies, deals, quotes, line items, payments, discounts, fees, and taxes. View the [subscriptions API](/reference/api/crm/commerce/subscriptions) | | `0-27` | Tasks | A type of activity that represents to-dos associated with your records. View the [tasks API](/reference/api/crm/engagements/tasks) | | `0-115` | Users | Represent the users in your HubSpot account. Users cannot be associated with other objects, but can be retrieved and updated via API. View the [user details API](/reference/api/settings/users/user-details) | You can always use the numerical type ID value, but in some cases, you can also use the object's name for contacts, companies, deals, tickets, or notes. For example: - When starting an import with the [imports API](/guides/api/crm/imports), the `columnObjectTypeId` specifies which object the data in your file belongs to. To import data for contacts, your value for `columnObjectTypeId` could be `contact` or `0-1`. - When using the [associations API](/guides/api/crm/associations/associations-v4), the `fromObjectType` and `toObjectType` values specify the objects and the direction of the association. To view association types for contacts to companies, your `GET` request URL could be `crm/v4/associations/contact/company/labels` or `crm/v4/associations/0-1/0-2/labels`. ### Unique identifiers and record IDs A unique identifier is a value that differentiates one record in the CRM from another, even if they have otherwise identical information. For example, a database might have records for two people named John Smith. To avoid accidentally sending money to the wrong John Smith, each record is assigned a number as their _Record ID_. When a record is created in HubSpot, its _Record ID_ (`hs_object_id`) is automatically generated and should be treated as a string. Record IDs are unique within an object, so there can be both a contact and company with the same ID. For contacts and companies, there are additional unique identifiers, including a contact's `email` and a company's `domain` name. You can also [create custom unique identifier properties](https://developers.hubspot.com/docs/guides/api/crm/properties#create-unique-identifier-properties). In the CRM APIs, you'll use unique identifier values to identify and manage specific records. You can always use a record's `hs_object_id` value, but can also use custom unique identifier properties for certain endpoints, specified by the `idProperty` parameter. For example, to edit a contact, you could make a `PATCH` request to `/crm/v3/objects/0-1/{contactId}` or `/crm/v3/objects/0-1/{contactEmail}?idProperty=email`.Learn more about how HubSpot handles deduplication in the [Knowledge Base](https://knowledge.hubspot.com/records/deduplication-of-records). ## Associations API In HubSpot, you can show how objects are related to one another by associating their records. For example, you can associate multiple contacts with a company, and then associate the company and relevant contacts with a deal. When using the [associations API endpoints](/guides/api/crm/associations/associations-v4), you can substitute objects for `{toObjectTypeId}` and `{fromObjectTypeId}` in the request URLs and request bodies. Before associating records across objects, to understand which objects can be associated to one another, you can [retrieve association types.](/guides/api/crm/associations-v4#retrieve-association-labels) For example, contacts can be associated with most objects, while Invoices can only be associated with contacts, companies, deals, line items, discounts, fees, and taxes. Depending on your subscription, you can describe the specific relationship types between records using [association labels](https://knowledge.hubspot.com/object-settings/create-and-use-association-labels), and your account may have additional custom objects, which can be associated with the other standard objects. Learn more about object relationships and managing associations using the [associations endpoints](/guides/api/crm/associations/associations-v4). If you have access to a HubSpot account, you can also review your account's unique object relationships by navigating to [the data model tool.](https://knowledge.hubspot.com/data-management/view-a-model-of-your-crm-object-and-activity-relationships) ## Properties API Information about records are stored in fields called properties, which are then organized into [groups](https://knowledge.hubspot.com/properties/create-and-edit-properties). HubSpot provides a set of default properties for each object. In addition to each object’s default properties, you can store custom data by [creating custom properties](https://knowledge.hubspot.com/properties/create-and-edit-properties). When using the properties API, you can substitute objects in the endpoints to create and manage an object's properties. For example, `/crm/v3/properties/0-1` for contact properties or `/crm/v3/properties/0-5` for ticket properties. Learn more about using the properties API in [this article](/guides/api/crm/properties). ## Search API To filter and sort records and activities based on their properties and associations, you can use the search API. When using the search endpoints, substitute the `{objectTypeId}` value for the object within which you want to search. For example, to search calls, you'd make a POST request to `/crm/v3/objects/0-48/search`. Learn more about how to use CRM search API in [this article](/guides/api/crm/search). ## Pipelines API In HubSpot, you can use pipelines to track records through stages in your processes. For example, you can track deals through a sales processes or tickets through support statuses. Using the pipelines API, you can create, retrieve, edit, and delete pipelines and pipeline stages.Learn which objects have pipelines and how to use pipelines API in [this article](/guides/api/crm/pipelines). # Using Object APIs @ Learn how to use the object API endpoints to create and manage records or activities. $ guides & api/crm --- # Using Object APIs The object APIs enable you to create and manage records and activities within HubSpot. The object APIs have the same basic functionality for all supported objects, which allows you to substitute the `objectTypeId` of an object in the endpoints. For example, to create contacts, you'd make a `POST` request to `crm/v3/objects/0-1` and to create courses, the request would be to `crm/v3/objects/0-410`. In this article, learn the basics of how to use the object APIs. For certain objects, you can refer to object-specific guides with examples and other details about unique functionality: [Companies](/guides/api/crm/objects/companies), [Contacts](/guides/api/crm/objects/contacts), [Deals](/guides/api/crm/objects/deals), [Tickets](/guides/api/crm/objects/tickets), [Calls](/guides/api/crm/engagements/calls), [Communications](/guides/api/crm/engagements/communications), [Custom objects](/guides/api/crm/objects/custom-objects), [Emails](/guides/api/crm/engagements/email), [Feedback submissions](/guides/api/crm/objects/feedback-submissions), [Invoices](/guides/api/crm/commerce/invoices), [Leads](/guides/api/crm/objects/leads), [Marketing events](/guides/api/marketing/marketing-events), [Meetings](/guides/api/crm/engagements/meetings), [Notes](/guides/api/crm/engagements/notes), [Orders](https://developers.hubspot.com/docs/guides/api/crm/commerce/orders), [Payments](/guides/api/crm/commerce/payments), [Postal mail](/guides/api/crm/engagements/postal-mail), [Products](/guides/api/crm/commerce/products), [Quotes](/guides/api/crm/commerce/quotes), [Subscriptions](/guides/api/crm/commerce/subscriptions), [Tasks](/guides/api/crm/engagements/tasks), [Users](/guides/api/settings/users/user-details). New objects (e.g., appointments, courses, listings, services) must be activated in the HubSpot account before you can manage their records via API. Learn how to check if they're activated via the [object library API](/guides/api/crm/object-library) and [how to activate them in HubSpot](https://knowledge.hubspot.com/data-management/data-model-templates#view-the-object-library). ## Retrieve records You can retrieve records individually or in batches. - To retrieve an individual record, make a `GET` request to `/crm/v3/objects/{objectTypeId}/{recordId}`. - To request a list of all records for an object, make a `GET` request to `/crm/v3/objects/{objectTypeId}`. - To retrieve a batch of specific records by record ID or a [custom unique identifier property](/guides/api/crm/properties#create-unique-identifier-properties), make a `POST` request to `crm/v3/objects/{objectTypeId}/batch/read` and include the `id` values of the records in the request body. The batch endpoint <u>cannot</u> retrieve associations. Learn how to batch read associations with the [associations API](/guides/api/crm/associations/associations-v4). For these endpoints, you can include the following query parameters in the request URL: | Parameter | Description | | --- | --- | | `properties` | A comma separated list of the properties to be returned in the response. If the requested record doesn't have a value for a property, it will not appear in the response. | | `propertiesWithHistory` | A comma separated list of the current and historical properties to be returned in the response. If the requested record doesn't have a value for a property, it will not appear in the response. | | `associations` | A comma separated list of objects to retrieve associated IDs for. Any specified associations that don't exist will not be returned in the response. Learn more about the [associations API.](/guides/api/crm/associations/associations-v4)**Note**: this parameter is <u>not</u> supported in the batch read endpoint. | | `idProperty` | Indicates the unique identifier property used to identify records. You only need to use this parameter if retrieving by a [custom unique identifier property](/guides/api/crm/properties#create-unique-identifier-properties). | For example, to retrieve a meeting with the meeting's text body and starting time, you'd make a `GET` request to `/crm/v3/objects/0-47/{meetingId}?properties=hs_meeting_body,hs_timestamp` and your response would look similar to the following: ```json ///Example response // GET crm/v3/objects/0-47/65059681027?properties=hs_meeting_body,hs_timestamp { "id": "65059681027", "properties": { "hs_createdate": "2024-11-20T20:12:09.236Z", "hs_lastmodifieddate": "2024-11-20T20:12:10.610Z", ", "hs_object_id": "65059681027", "hs_timestamp": "2024-11-20T20:12:03.054Z" }, "createdAt": "2024-11-20T20:12:09.236Z", "updatedAt": "2024-11-20T20:12:10.610Z", "archived": false } ``` To retrieve a batch of deals, your request could look like either of the following: ```json ///Example request body // GET crm/v3/objects/0-3/batch/read { "properties": ["dealname", "dealstage", "pipeline"], "inputs": [ { "id": "7891023" }, { "id": "987654" } ] } ``` ```json ///Example request body // GET crm/v3/objects/0-3/batch/read?idProperty=uniqueordernumber { "properties": ["dealname", "dealstage", "pipeline"], "idProperty": "uniqueordernumber", "inputs": [ { "id": "0001111" }, { "id": "0001112" } ] } ``` ## Create records - To create a record for an object, make a `POST` request to `crm/v3/objects/{objectTypeId}`. - To create multiple records, make a `POST` request to `/crm/v3/objects/{objectTypeId}/batch/create`. In your request, include data for each record in a `properties` object. You can also add an `associations` object to associate your new record with existing records (e.g., companies, deals), or activities (e.g., meetings, notes). If you want to create and update records at the same time, learn how to [upsert records](#upsert-records) below. ### Properties Record details are stored in properties. To view all available properties for an object, you can retrieve a list of your account's properties by making a `GET` request to `/crm/v3/properties/{objectTypeId}`. Learn more about the [properties API](/guides/api/crm/properties) and [how to format property values.](/guides/api/crm/properties#update-or-clear-a-property-s-values) The table below includes the objects for which records can be created via API and the properties required for creation. | Type ID | Object | Required properties to create | | --- | --- | --- | | `0-2` | Companies | At least one of `domain` (recommended) or `name` | | `0-1` | Contacts | At least one of of `email` (recommended), `firstname`, or `lastname` | | `0-3` | Deals | `dealname`, `dealstage` and `pipeline` | | `0-5` | Tickets | `subject` (the ticket's name), `hs_pipeline_stage` (the ticket's status), and `hs_pipeline` | | `0-421` | Appointments | `hs_appointment_start` | | `0-48` | Calls | `hs_timestamp` | | `0-18` | Communications | `hs_timestamp` and `hs_communication_channel_type`<br /> | | `0-410` | Courses | `hs_course_name` | | `2-XXX` | Custom objects | The [required properties](/guides/api/crm/objects/custom-objects#properties) specified in your object's [schema](/reference/api/crm/objects/schemas). | | `0-49` | Emails | `hs_timestamp` and `hs_email_direction` | | `0-136` | Leads | `hs_associated_contact_email`, `hs_associated_contact_lastname`, `hs_lead_name`, `hs_associated_company_domain`, `hs_associated_contact_firstname`, and `hs_associated_company_name` | | `0-8` | Line items | [Recommended properties](/guides/api/crm/commerce/line-items) | | `0-420` | Listings | `hs_name` | | `0-54` | Marketing events | `hs_event_description` and `hs_event_name` | | `0-47` | Meetings | `hs_timestamp` | | `0-46` | Notes | `hs_timestamp` | | `0-123` | Orders | `hs_order_name` | | `0-116` | Postal mail | `hs_timestamp` | | `0-7` | Products | `hs_sku`, `hs_folder`, `price`, `name`, and `description` | | `0-14` | Quotes | `hs_title` and `hs_expiration_date` | | `0-162` | Services | `hs_name`, `hs_pipeline`, and `hs_pipeline_stage` | | `0-69` | Subscriptions | `hs_name` | | `0-27` | Tasks | `hs_timestamp` | | `0-115` | Users | `hs_internal_user_id` and `hs_email` | For example, to create a new ticket without associations, your request may look similar to the following: ```json ///Example request body // POST /crm/v3/objects/0-1 { "properties": { "hs_pipeline": "0", "hs_pipeline_stage": "1", "hs_ticket_priority": "HIGH", "subject": "troubleshoot report" } } ``` For example, to create multiple contacts without associations, your request may look similar to the following: ```json ///Example request body // POST crm/v3/objects/0-1/batch/create { "inputs": [ { "properties": { "email": "testone@test.com", "firstname": "Test", "lastname": "PersonOne", "favorite_food": "Pizza" } }, { "properties": { "email": "testtwo@test.com", "firstname": "Test", "lastname": "PersonTwo", "favorite_food": "Ice cream" } } ] } ``` ### Associations When creating a new record, you can also associate it with [existing records](https://knowledge.hubspot.com/records/associate-records) or [activities](https://knowledge.hubspot.com/records/associate-activities-with-records) by including an `associations` object. In the `associations` object, you should include the following: | Parameter | Description | | --- | --- | | `to` | The record or activity you want to associate with the record, specified by its unique `id` value. | | `types` | The type of the association between the record and the record/activity. Include the `associationCategory` and `associationTypeId`. Default association type IDs are listed [here](/guides/api/crm/associations/associations-v4#association-type-id-values), or you can retrieve the value for custom association types (i.e. labels) via the [associations API](/guides/api/crm/associations/associations-v4#retrieve-association-types). | For example, to create a new contact and associate with an existing company and email, your request would look like the following: ```json ///Example request body // POST /crm/v3/objects/0-1 { "properties": { "email": "example@hubspot.com", "firstname": "Jane", "lastname": "Doe", "phone": "(555) 555-5555", "company": "HubSpot", "website": "hubspot.com", "lifecyclestage": "marketingqualifiedlead" }, "associations": [ { "to": { "id": 123456 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 279 } ] }, { "to": { "id": 556677 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 197 } ] } ] } ``` For example, to create multiple deals and associate them with existing companies and meetings, your request would look similar to the following: For batch create actions, you can enable multi-status errors which tell you which records were successfully created and which were not. Learn more about [setting up multi-status error handling](/reference/api/other-resources/error-handling#multi-status-errors). ```json ///Example request body // POST /crm/v3/objects/0-3/batch/create { "inputs": [ { "associations": [ { "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 5 } ], "to": { "id": "23125848331" } }, { "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 211 } ], "to": { "id": "65059681027" } } ], "properties": { "dealname": "New deal 1", "dealstage": "qualifiedtobuy", "pipeline": "default" } }, { "associations": [ { "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 5 } ], "to": { "id": "23125848331" } }, { "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 211 } ], "to": { "id": "65059681027" } } ], "properties": { "dealname": "New deal 2", "dealstage": "qualifiedtobuy", "pipeline": "default" } } ] } ``` ## Update records You can update records individually or in batches. For existing records, the Record ID is a default [unique value](/guides/api/crm/understanding-the-crm#unique-identifiers-and-record-ids) that you can use to update the record via API, but you can also identify records using the `idProperty` parameter with a [custom unique identifier property](/guides/api/crm/properties#create-unique-identifier-properties). If you want to create and update records at the same time, learn how to [upsert records](#upsert-records). - To update an individual record, make a `PATCH` request to `/crm/v3/objects/{objectTypeId}/{recordId}`, and include the data you want to update. - To update multiple records, make a `POST` request to `/crm/v3/objects/{objectTypeId}/batch/update`. In the request body, include an array with the identifiers for the records and the properties you want to update. If you've [merged records](https://knowledge.hubspot.com/records/merge-records), you can use the previous record ID values stored in the `hs_merged_object_ids` field to update a record using the basic update endpoint. However, these values are not supported when updating records using the batch update endpoint. </Alert> For example, to update a contact, your request would look similar to the following: ```json // Example request body with record ID // PATCH /crm/v3/objects/0-1/123456789 { "properties": { "favorite_food": "burger", "jobtitle": "Manager", "lifecyclestage": "Customer" } } ``` ## Upsert records You can also batch create and update records at the same time using the upsert endpoint. For this endpoint, you can use a [custom unique identifier property](/guides/api/crm/properties#create-unique-identifier-properties) or `email` for contacts. Following the request, if the records already exist, they'll be updated and if the records don't exist, they'll be created. To upsert records, make a `POST` request to `/crm/v3/objects/{objectTypeId}/batch/upsert`. In your request body, include the `idProperty` parameter to identify the unique identifier property you're using. Include that property's value as the `id` ​and add the other properties you want to set or update. Partial upserts are not supported when using `email` as the `idProperty` for contacts. To complete a partial upsert, use a custom unique identifier property as the `idProperty` instead. For example, to upsert contacts to set the phone number property using `email` as the identifier, your request would look similar to the following: ```json // Example request body with email // POST /crm/v3/objects/0-1/batch/upsert { "inputs": [ { "properties": { "phone": "5555555555" }, "id": "luke@lukesdiner.com", "idProperty": "email" }, { "properties": { "phone": "7777777777" }, "id": "lorelai@thedragonfly.com", "idProperty": "email" }, { "properties": { "phone": "1111111111" }, "id": "michel@thedragonfly.com", "idProperty": "email" } ] } ``` ## Update associations Once records are created, you can update their associations using the [associations API](/guides/api/crm/associations/associations-v4). - To associate a record with other records or an activity, make a `PUT` request to `/crm/v3/objects/{objectTypeId}/{fromRecordId}/associations/{toObjectTypeId}/{toRecordId}/{associationTypeId}`. - To remove an association between a record and a record or activity, make a `DELETE` request to the following URL: `/crm/v3/objects/{objectTypeId}/{fromRecordId}/associations/{toObjectTypeId}/{toRecordId}/{associationTypeId}`. To retrieve the `associationTypeId` value, refer to [this list](/guides/api/crm/associations/associations-v4#association-type-id-values) of default values, or make a `GET` request to `/crm/v4/associations/{fromObjectTypeId}/{toObjectTypeId}/labels`. ## Pin an activity on a record You can [pin an activity](https://knowledge.hubspot.com/records/pin-an-activity-on-a-record) on a record by including the `hs_pinned_engagement_id` field in your create, update, or upsert request. In the field, include the `id` of the activity to pin, which can be retrieved by making a `GET` request to `/crm/v3/objects/{objectTypeId}/{recordId}` for calls, communications, emails, meetings, notes, postal mail, or tasks. You can pin one activity per record, and the activity must already be associated with the record prior to pinning. For example, to set or update an existing deal's pinned activity, your request could look like: ```json ///Example request body // PATCH /crm/v3/objects/0-3/{recordId} { "properties": { "hs_pinned_engagement_id": 123456789 } } ``` To create a new deal, associate it with an activity, and pin that activity, you request would look like: ```json // Example request body // POST /crm/v3/objects/0-3 { "properties": { "dealname": "New Deal", "pipeline": "default", "dealstage": "appointmentscheduled", "hs_pinned_engagement_id": 123456789 }, "associations": [ { "to": { "id": 123456789 }, "types": [ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 205 } ] } ] } ``` ## Delete records You can delete records individually or in batches, which will add the record to the recycling bin in HubSpot. You can [restore the record within HubSpot](https://knowledge.hubspot.com/records/restore-deleted-records) for up to 90 days after deletion. - To delete an individual record by its record ID, make a `DELETE` request to `/crm/v3/objects/{objectTypeId}/{recordId}`. - To delete multiple records, make a `POST` request to `/crm/v3/objects/{objectTypeId}/batch/archive` and include the record ID values as the `id` inputs in your request body. For example, to delete multiple appointments, your request would look like: ```json // Example request body // POST /crm/v3/objects/0-421/batch/archive { "inputs": [ { "id": "123456" }, { "id": "7891011" }, { "id": "12123434" } ] } ``` # CMS API | File Manager @ The files endpoints are used to get and manage data in your file manager . $ guides & api/library --- # File Manager Use HubSpot’s files tool to manage and store files in HubSpot. Files hosted in HubSpot can be uploaded and used in both HubSpot and external content. They can also be attached to records using the [engagements API](/guides/api/crm/engagements/engagement-details). If your company is building its website using HubSpot's CMS, you can use the files API to upload and store assets in HubSpot. These files can then be served and shared through the HubSpot CMS. You can access the files tool from [within HubSpot](https://knowledge.hubspot.com/files/upload-files-to-use-in-your-hubspot-content) or via the files API. Below, learn about the files API and how to upload and delete files. For a full list of files API endpoints, check out the [reference documentation](/reference/api/library/files/v3). ## Upload a file Files can be uploaded using a multipart/form-data `POST` request to `files/v3/files` with the following fields. While a specific folder ID is not required at upload, it's recommend to upload files into a folder and not the root directory. Folder requirements at upload are subject to change in the future. | Field | Description | | --- | --- | | `file` | The file to upload (required). | | `options` | A JSON object that controls the file's privacy and indexability, and contains two fields: `access`, which is required, and `ttl`, which specifies a time period after which the file will be automatically deleted.If you're using the `ttl` field:<ul><li>The minimum period that must be set is 1 day.</li><li>The maximum period that can be set is 1 year.</li><li>After the set period, the file will be permanently deleted. After deletion, the file cannot be recovered or restored.</li></ul><br /> | | `folderId` | The ID of the folder that the file will be uploaded to. Either this field <u>or</u> `folderPath` must be provided in your request (but <u>not</u> both). | | `folderPath` | The path of the folder that the file will be uploaded to. Either this field <u>or</u> `folderId` must be provided in your request (but <u>not</u> both). | | `fileName` | The name of the file. If no name is specified, a name will be generated from the file's content. | | `charsetHunch` | Character set encoding for the uploaded file. If not provided, it will be derived from the file. | As an example, if you wanted to upload a file with the following criteria to your HubSpot account: - **File name:** `cat.png` - **Destination folder in the HubSpot file manager:** `/library/cat_archive` - **File accessibility in HubSpot:** privately accessible The following headers and request body would need to be part of your request: ```shell curl --request POST \ --url 'https://api.hubapi.com/files/v3/files?=' \ --header 'Authorization: Bearer pat-na1-00000000-0000-0000-0000-000000000000' \ --header 'Content-type: multipart/form-data' \ --form file=@/Users/person/Downloads/cat.png \ --form 'options={"access": "PRIVATE"}' \ --form folderPath=/library/cat_archive ``` The resulting response will include the `id` and `parentFolderId` of the uploaded file, which you can use to retrieve the file via a GET request. ```json // 201 Response from successful file upload { "id": "122692044085", "createdAt": "2023-06-28T17:56:45.393Z", "updatedAt": "2023-06-28T17:56:45.393Z", "archived": false, "parentFolderId": "122692510820", "name": "cat", "path": "/library/cat_archive/cat.png", "size": 24574, "height": 219, "width": 225, "encoding": "png", "type": "IMG", "extension": "png", "defaultHostingUrl": "https://12345.fs1.hubspotusercontent-na1.net/hubfs/12345/library/cat_archive/cat.png", "url": "https://12345.fs1.hubspotusercontent-na1.net/hubfs/12345/library/cat_archive/cat.png", "isUsableInContent": true, "access": "PRIVATE" } ``` ## Check a file's upload status If you're importing a file from a URL to your file manager using a `POST` request to `files/v3/files/import-from-url/async`, you can review the upload status of the file. To do so, use a `GET` request to `files/v3/files/import-from-url/async/tasks/{taskId}/status`. After making this request, you will receive one of the following replies: - `PENDING`: the file is in the queue to be uploaded. The import process has not yet started. - `PROCESSING`: the file is in the process of being uploaded. - `CANCELED`: the upload has been canceled and the file will not be uploaded. To import the file to your HubSpot account, you will need to upload the file again. - `COMPLETE`: the file has been uploaded to the files tool successfully. The uploaded file will appear in your files tool. ## View a file's details To review the details of a file that's been uploaded to the files tool, make a `GET` request to `files/v3/files/{fileId}`. This will return the file with details such as name, height and width, encoding, the URL, and more. For example, to retrieve the details of a file: If a file is set to private, the returned URL will result in a 404 error. To get a viewable URL of the file, you can make a `GET` request to `/files/v3/files/{fileId}/signed-url`. When making this request, you can include `property` parameters to return specific properties such as height and width. ## Delete a file To delete a file, make a `DELETE` request to `files/v3/files/{fileId}`. This will mark the file as deleted and make the content of the file inaccessible. To permanently delete a file, make a `DELETE` request to `files/v3/files/{fileId}/gdpr-delete`. This will permanently delete the file’s content and metadata within 7 days. If a file is not GDPR deleted, its contents will remain on HubSpot's servers in a private state where no one can access it. To ensure file contents are fully deleted, use the GDPR delete functionality. ## Create a folder To create a folder, make a `POST` request to `files/v3/folders`. When making the request, you can include the below fields. | Field | Required | Description | | --- | --- | --- | | `name` | Yes | Name of the folder you want to create. | | `parentFolderId` | No | To create the folder within an existing folder, include this field with the existing folder's ID. `parentFolderId` and `parentFolderPath` cannot be set at the same time. | | `parentFolderPath` | No | To create the folder within an existing folder, include this field with the existing folder's path. `parentFolderId` and `parentFolderPath` cannot be set at the same time. | ```json //Example request body of POST request to /files/v3/folders { "name": "myNewFolder", "parentFolderId": 12345 } ``` ## Changes in v3 If you’ve been using the previous version of this API, v3 has the following changes: - All files uploaded through the API will be visible in the files dashboard and the files picker. Hidden files cannot be created. However, private files and non-indexable files can still be created. - Listing files will not return hidden or deleted files. However, a much broader range of filters can be applied. Hidden files can still be fetched by ID, but require a new scope: `files_ui_hidden.read.` - Multiple files cannot be uploaded with a single request. - Folder update actions like moving and renaming are now asynchronous. Each request will return a token that can be used to check the status of the folder edit. - Endpoints that create or replace files require you to provide access levels for the files. These access levels are: - `PUBLIC_INDEXABLE`**:** file is publicly accessible by anyone who has the URL. Search engines can index the file. - `PUBLIC_NOT_INDEXABLE`**:** file is publicly accessible by anyone who has the URL. The X-Robots-Tag: noindex header will be sent whenever the file is retrieved, instructing search engines not to index the file. - **`PRIVATE`:** file is not publicly accessible. Requires a signed URL to display content. Search engines cannot index the file. - Endpoints that create files allow for a level of duplicate detections as part of the file’s upload options. - `ENTIRE_PORTAL`**:** search for a duplicate file in the account. - `EXACT_FOLDER`**:** search for a duplicate file in the provided folder. - `NONE`**:** do not run any duplicate validation. - `REJECT`**:** reject the upload if a duplicate is found. - `RETURN_EXISTING`**:** if a duplicate file is found, do not upload a new file and return the found duplicate instead. - Duplicate detection works on a `duplicateValidationScope`, which affects how we search for a duplicate. - This also requires a `duplicateValidationStrategy`, which dictates what happens if a duplicate is found. # Library | Meetings @ Use the meetings API to retrieve information about meetings created through a scheduling page. You can also book a meeting with a scheduling page through the API. $ guides & api/library --- # Meetings Use the meetings API to retrieve information about meetings created [through a scheduling page](https://knowledge.hubspot.com/meetings-tool/create-and-edit-scheduling-pages). You can also book a meeting with a scheduling page through the API. Learn more about using the [API to create, update, and delete all types of meeting engagements](/guides/api/crm/engagements/meetings). With the meetings API, you can: - Get a list of meeting links. - Get booking information for a meeting link. - Get the next availability page for a meeting link. This could be useful if you have an app that can get a list of meeting links or book meetings. You could also use the meetings API to create an interface for your customers to book meetings with your team. The following features aren’t supported when using the Meetings API: - UTM, HubSpot UTK, and content tracking. - Reschedule through this API. - Send in [CAPTCHA tokens](https://knowledge.hubspot.com/meetings-tool/create-and-edit-scheduling-pages#form). - Create meetings with [payment](https://knowledge.hubspot.com/meetings-tool/create-and-edit-scheduling-pages#scheduling). - Send in an IP address. The sections below provides a walkthrough of how to use the v3 endpoints. For a full reference of the available endpoints and their required fields, check out the [endpoints reference documentation](/reference/api/library/meetings). ## List meeting scheduling pages To get a list of meetings scheduling pages, make a `GET` request to `/scheduler/v3/meetings/meeting-links`. Use the amount of meeting links you want returned as the limit in the request URL and use your [userID](/guides/api/settings/users/user-details), name, and type as a query parameter. To get and filter a list of meeting links, supply the filters you’d like to apply as query parameters (e.g., limit, type, and name), and provide your user ID as the `organizerUserId` query parameter. For example, if your user ID is `1234567`, you’d make a `GET` request to `/scheduler/v3/meetings/meeting-links?limit=1&name=menelson&organzerUserId=1234567&type=PERSONAL_LINK` The response for fetching a list of meeting scheduling pages would resemble the following: ```json //Example response body { "total": 1, "results": [ { "id": "3779484", "slug": "menelson", "link": "https://meetings.hubspotqa.com/menelson", "name": "Sales Pro Demo", "type": "PERSONAL_LINK", "organizerUserId": "2195101", "userIdsOfLinkMembers": ["2195101"], "defaultLink": true, "createdAt": "2024-09-13T18:05:08.797Z", "updatedAt": "2024-09-13T18:43:35.116Z" } ] } ``` The details of each response field are outlined in the table below: | Field | Description | | --- | --- | | `id` | The ID of the activity. | | `slug` | The end of the meeting link’s URL. | | `link` | The full meeting URL. | | `name` | The title of the meeting. | | `type` | Indicates if the meeting is a one-on-one (`PERSONAL_LINK`), group (`GROUP_CALENDAR`), or round-robin (`ROUND_ROBIN_CALENDAR`) meeting link. | | `organizerUserId` | The user ID of the user who organized the meeting. | | `userIdsOfLinkMembers` | The user IDs of the team members on the group or round robin meeting. | | `defaultLink` | Indicates if the meeting is the user’s default meeting link. | ## List booking information To get details about the initial information necessary for a meeting scheduler, make a `GET` request to `/scheduler/v3/meetings/meeting-links/book/{slug}`. Use your meeting link path as the slug and use your timezone as a query parameter. For example, to get details about initial information necessary for meeting scheduler, make a `GET` request to `/scheduler/v3/meetings/meeting-links/book/menelson&timezone=America%2FNew_York`. The response for fetching booking information would resemble the following: The details of each response field are outlined in the table below: ```json //Example response body { "total": 1, "results": [ { "id": "3779484", "slug": "menelson", "link": "https://meetings.hubspotqa.com/menelson", "name": "Sales Pro Demo", "type": "PERSONAL_LINK", "organizerUserId": "2195101", "userIdsOfLinkMembers": [ "2195101" ], "defaultLink": true, "createdAt": "2024-09-13T18:05:08.797Z", "updatedAt": "2024-09-13T18:43:35.116Z" } ] }{ "linkId": "3779484", "isOffline": false, "customParams": { "legalConsentEnabled": false, "ownerPrioritized": false, "formFields": [], "displayInfo": { "companyAvatar": "https://99261937.fs1.hubspotusercontentqa-na1.net/hubfs/99261937/1c2f7f64-2687-49da-b931-7d87da9e3d1a.png", "publicDisplayAvatarOption": "PROFILE_IMAGE" }, "guestSettings": { "canAddGuests": false, "maxGuestCount": 10 }, "meetingBufferTime": 900000, "availability": { "MON_FRI": [ { "start": 540, "end": 1020 } ] }, "startTimeIncrementMinutes": "FIFTEEN", "weeksToAdvertise": 2, "durations": [ 900000, 1800000, 3600000 ], "welcomeScreenInfo": { "useCompanyLogo": false, "showWelcomeScreen": false } }, "linkType": "PERSONAL_LINK", "allUsersBusyTimes": [ { "isOffline": false, "meetingsUser": { "id": "2101913", "userId": "2195101", "isSalesStarter": false, "userProfile": { "firstName": "Melinda", "lastName": "Nelson", "email": "menelson@hubspot.com", "fullName": "Melinda Nelson" }, "calendarProvider": "GOOGLE" }, "busyTimes": [ { "start": 1726664400000, "end": 1726668000000 }, } ], "brandingMetadata": { "logoWidth": 0, "logoHeight": 0, "showMarketingAd": false, "showSalesAd": false, "logoUrl": "", "logoAltText": "", "primaryColor": "#3574E3", "secondaryColor": null, "accentColor": null, "accent2Color": null, "companyName": null, "companyDomain": "joekurien.com", "companyAddressLine1": null, "companyAddressLine2": null, "companyCity": null, "companyState": null, "companyZip": null, "companyCountry": null, "companyAvatar": "https://api-na1.hubapiqa.com/avatars/v1/signed-uris/1CkoKEQgEEg1qb2VrdXJpZW4uY29tGNr_-4cGIKCGNioaYnJhbmRpbmc6YXBpOndlYjp1cy1lYXN0LTEyDzE2My4xMTYuMTM1LjExOBIZAPjT6vbwOch01nPRCSQyux1GWlQKiewhnA" }, "linkAvailability": { "linkAvailabilityByDuration": { "900000": { "meetingDurationMillis": 900000, "availabilities": [ { "startMillisUtc": 1726672500000, "endMillisUtc": 1726673400000 }, "1800000": { "meetingDurationMillis": 1800000, "availabilities": [ { "startMillisUtc": 1726672500000, "endMillisUtc": 1726674300000 }, ``` ## List availability To get the next availability page for a meeting, make a `GET` request to `/scheduler/v3/meetings/meeting-links/book/availability-page/{slug}`. Use your meeting link path as the slug and your timezone as a query parameter. For example, to get make a the next availability page for a meeting, make a GET request to `https://api.hubspot.com/scheduler/v3/meetings/meeting-links/book/menelson&timezone=America%2FNew_York `The response for fetching the availability page for a meeting would resemble the following: ```json //Example response body { "linkAvailability": { "linkAvailabilityByDuration": { "1800000": { "meetingDurationMillis": 1800000, "availabilities": [ { "startMillisUtc": 1725282000000, "endMillisUtc": 1725283800000 }, { "startMillisUtc": 1725282900000, "endMillisUtc": 1725284700000 }, { "startMillisUtc": 1726259400000, "endMillisUtc": 1726261200000 } ] } }, "hasMore": false }, "allUsersBusyTimes": [ { "isOffline": false, "meetingsUser": { "id": "1821619", "userId": "1234567", "isSalesStarter": true } } ] } ``` ## Book a meeting To book a meeting using a meeting link, make a `POST` request to `/scheduler/v3/meetings/meeting-links/book/`. Use your timezone as a query parameter. For example to book a meeting, make a `POST` request to `/scheduler/v3/meetings/meeting-links/book?timezone=America%2FNew_York `The request body should include the following information. Any information that's required in your meeting registration must be included in the request body: ```json //Request body sample { "slug": menelson "firstName": Melinda "lastName": Nelson "email": menelson@hubspot.com "startTime": 1726059600000, "duration": 1800000, "guestEmails": [], "timezone": "America/New_York", "locale": "en-us", ], "likelyAvailableUserIds": [] } ``` ```json //Response body sample { "calendarEventId": "6q0nonv2c1a73b1nqctf5rmo6g", "start": "2024-09-19T18:45:00Z", "duration": 1800000, "contactId": "1706743198", "bookingTimezone": "America/New_York", "locale": "en-us", "guestEmails": [], "subject": "Event Subject", "location": "Location Example", "isOffline": false, "end": "2024-09-19T19:15:00Z" } ``` # Marketing API | Campaigns @ These endpoints are used to create and manage campaigns in your HubSpot account. $ guides & api/marketing --- ## Campaigns Use the [campaigns tool](https://knowledge.hubspot.com/campaigns/understand-campaigns) to campaigns to create, manage, and report on a single marketing campaign using multiple assets in one place. For example, you can create a campaign using marketing emails and social posts and raise awareness of a new product or an event. To use the campaigns tool and its corresponding endpoints, you'll need a **_Marketing Hub_** _Professional_ or _Enterprise_ subscription. You can use campaign endpoints to create, read, update, and delete marketing campaign data. For example, you can use these endpoints to create and manage HubSpot campaigns from your external applications. You can also transfer campaign data to external data warehouses for analytics. You can also create, read, update, and delete multiple campaigns with the associated batch endpoints, check out the [reference documentation](/reference/api/marketing/campaigns) for a full list of endpoints and the associated parameters available. ## Scope requirements The following scopes are required to use the campaigns API, based on the endpoints you're using: - `marketing.campaigns.read`: provides access to view details about marketing campaigns and their associated assets. - `marketing.campaigns.write`: grants access to create, delete, and modify marketing campaigns. - `marketing.campaigns.revenue.read`: provides access to view revenue details and deal amounts attributed to a marketing campaign. ## Create a campaign To create a campaign, make a `POST` request to `/marketing/v3/campaigns`. Create a campaign with the given properties and return the campaign object, including the `campaignGuid` and created properties. For example, to create a new campaign, you would make a `POST` request with the following request body. ```json //Example create request body { "properties": { "hs_name": "Inbound", "hs_start_date": "2024-10-30", "hs_notes": "Campaign notes" } } ``` The response will include a `campaignGuid`, a unique identifier for the campaign. This will be formatted as a UUID. In the following example, the `campaignGuid` is `edb9b6c3-d2e2-4ca8-8396-832262aed0d4`. ```json //Example create response body { "id": "edb9b6c3-d2e2-4ca8-8396-832262aed0d4", "properties": { "hs_name": "Inbound", "hs_start_date": "2024-10-30", "hs_notes": "Campaign notes" }, "createdAt": "2024-10-30T03:30:17.883Z", "updatedAt": "2024-12-07T16:50:06.678Z" } ``` ## Retrieve a campaign To retrieve the details of an existing campaign, make a `GET` request to `/marketing/v3/campaigns/{campaignGuid}`. This endpoint will return the campaign information. Depending on the query parameters used, this can also be used to return information about the assets and the corresponding assets' metrics. When using this endpoint, you can use the following query parameters. - `properties`**:** a comma-separated list of the properties to be returned in the response. For example, `hs_name`, `hs_campaign_status`, `hs_notes`. If any of the specified properties has an empty value on the requested object, it will be ignored and not returned in response. If the parameter is empty, the response will include an empty properties map. - `startDate`**:** the start date to fetch asset metrics, formatted as YYYY-MM-DD. This is used to fetch the metrics associated with the assets for a specified period. If no date is specified, no asset metrics will be retrieved. - `endDate`**:** the end date to fetch asset metrics, formatted as YYYY-MM-DD. This is used to fetch the metrics associated with the assets for a specified period. If no date is specified, no asset metrics will be retrieved. For example, if you made a `POST` request with `/marketing/v3/campaigns/edb9b6c3-d2e2-4ca8-8396-832262aed0d4?properties=hs_name,hs_start_date`, this would result in the following response body: ```json //Example get response body { "id": "edb9b6c3-d2e2-4ca8-8396-832262aed0d4", "properties": { "hs_name": "Inbound", "hs_start_date": "2024-10-30" }, "assets": { "EMAIL": { "paging": { "next": { "link": "?after=NTI1Cg%3D%3D", "after": "NTI1Cg%3D%3D" }, "results": [ { "id": "832", "name": "My email" } ] } } }, "createdAt": "2024-10-30T03:30:17.883Z", "updatedAt": "2024-12-07T16:50:06.678Z" } ``` ## List assets To retrieve the assets associated with a campaign make a `GET` request to `/marketing/v3/campaigns/{campaignId}/assets/{assetType}`. This endpoint lists all assets of the campaign by asset type. The `assetType` parameter is required, and each request can only fetch assets of a single type. The following are required: - `campaignGuid`**:** the UUID of the campaign. - `assetType`**:** the type of asset to be fetched. You can use the following query parameters with the endpoint: - `after`**:** a cursor for pagination. If provided, the results will start after the given cursor. For example, you can use the value NTI1Cg%3D%3D. - `limit`**:** define the maximum number of results to return. The allowed values range from 1 to 100. If no limit is specified, it will be set to 10 assets by default. - `startDate`**:** the start date to fetch asset metrics, formatted as YYYY-MM-DD. This is used to fetch the metrics associated with the assets for a specified period. If no date is specified, no asset metrics will be retrieved. - `endDate`**:** the end date to fetch asset metrics, formatted as YYYY-MM-DD. This is used to fetch the metrics associated with the assets for a specified period. If no date is specified, no asset metrics will be retrieved. The following is a list of available asset types and metrics related to the assets that are returned in response: | Asset | Asset type | Metrics | | --- | --- | --- | | Ad campaigns | `AD_CAMPAIGN` | No metrics available. | | Blog posts | `BLOG_POST` | CONTACTS_FIRST_TOUCH, CONTACTS_LAST_TOUCH, SUBMISSIONS, and VIEWS | | Social posts | `SOCIAL_BROADCAST` | FACEBOOK_CLICKS, LINKEDIN_CLICKS, and TWITTER_CLICKS | | CTAs | `WEB_INTERACTIVE` | CLICKS and VIEWS | | CTAs (Legacy) | `CTA` | CLICKS, SUBMISSIONS, and VIEWS | | External website pages | `EXTERNAL_WEB_URL` | VIEWS | | Forms | `FORM` | CONVERSION_RATE, SUBMISSIONS, and VIEWS | | Landing pages | `LANDING_PAGE` | CONTACTS_FIRST_TOUCH, CONTACTS_LAST_TOUCH, CUSTOMERS, SUBMISSIONS, and VIEWS | | Emails | `MARKETING_EMAIL` | CLICKS, OPEN, and SENT | | Marketing events | `MARKETING_EVENT` | ATTENDEES, CANCELLATIONS, and REGISTRATIONS | | Static lists | `OBJECT_LIST` | CONTACTS | | Website pages | `SITE_PAGE` | CONTACTS_FIRST_TOUCH, CONTACTS_LAST_TOUCH, CUSTOMERS, SUBMISSIONS, and VIEWS. | | Workflows | `AUTOMATION_PLATFORM_FLOW` | CURRENTLY_ENROLLED and STARTED | | Marketing SMS | `MARKETING_SMS` | SENT, DELIVERED, and UNIQUE_CLICKS | ## Manage asset associations To associate an asset with a campaign make a `PUT` request to `/marketing/v3/campaigns/{campaignGuid}/assets/{assetType}/{assetId}`. To remove an asset from a campaign make a `DELETE` request to `/marketing/v3/campaigns/{campaignGuid}/assets/{assetType}/{assetId}`. You can use the endpoints above to create and remove associations for the following asset types. For other asset types, learn how to [associate assets and content with a campaign](https://knowledge.hubspot.com/campaigns/associate-assets-and-content-with-a-campaign) in HubSpot. - `FORM` - `OBJECT_LIST` - `EXTERNAL_WEB_URL` ## Update campaign To perform a partial update of a campaign identified by the specified campaignGuid, make a `PATCH` request to `/marketing/v3/campaigns/{campaignGuid}`. You can use the following properties to update your campaign. The provided property values will be overwritten. If your request includes read-only or non-existent properties, you will encounter a `400 error`. Learn more about which properties can be used. ```json //Example update request { "properties": { "hs_name": "Inbound 2024" } } ``` ```json //Example update response { "id": "edb9b6c3-d2e2-4ca8-8396-832262aed0d4", "properties": { "hs_name": "Inbound 2024" }, "createdAt": "2024-10-30T03:30:17.883Z", "updatedAt": "2024-12-07T16:50:06.678Z" } ``` ## Delete a campaign To delete a campaign, make a `DELETE` request to `/marketing/v3/campaigns?`. This call will always return a `204 No Content` response, regardless of whether the campaignGuid provided corresponds to an existing campaign or not. ## Search for campaigns To search for campaigns based on query parameters, make a `GET` request to `/marketing/v3/campaigns/{campaignGuid}`. When using this endpoint, you can use the following query parameters. - `name`**:** return all campaigns whose names contain the specified substring. This allows partial matching of campaign names, returning all campaigns that include the given substring in their name. If this parameter is not provided, the search will return all campaigns. - `sort`**:** the field by which to sort the results. Allowed values are hs_name, createdAt, updatedAt. An optional '-' before the property name will denote descending order. For example, you can use -CREATED_AT to sort your campaigns from newest to oldest. If this is not specified, the list of campaigns will be sorted in alphabetical order by campaign name. - `after`**:** a cursor for pagination. If provided, the results will start after the given cursor. For example, you can use the value NTI1Cg%3D%3D. - `limit`**:** define the maximum number of results to return. The allowed values range from 1 to 100. If no limit is specified, it will be set to 50 campaigns by default. - `properties:` a comma-separated list of the properties to be returned in the response. For example, `hs_name`, `hs_campaign_status`, `hs_notes`. If any of the specified properties has an empty value on the requested object, it will be ignored and not returned in response. If the parameter is empty, the response will include an empty properties map. ```json //Example reponse body // for GET request to https://api.hubapi.com/marketing/v3/campaigns?properties=hs_name&limit=2 { "total": 14, "results": [ { "id": "972e2774-7409-43c2-a8b9-581732dff5a7", "properties": { "hs_name": "Inbound 2023" }, "createdAt": "2023-09-07T10:18:06.320Z", "updatedAt": "2023-09-07T10:18:06.320Z" }, { "id": "703bc2f0-f000-4840-b6ae-b2d74bc09fa0", "properties": { "hs_name": "Inbound 2024" }, "createdAt": "2024-06-25T10:57:26.232Z", "updatedAt": "2024-06-25T10:57:26.232Z" } ], "paging": { "next": { "after": "Mg%3D%3D", "link": "https://api.hubspotqa.com/marketing/v3/campaigns? properties=hs_name&limit=2&after=Mg%3D%3D" } } } ``` ## Campaign properties When using the _properties_ query parameter for your campaign endpoints, you can use the following properties: | Property Name | Read Only | Format | Description | | --- | --- | --- | --- | | `hs_name` | read & write | string | The campaign's name. This should be unique, with a max size of 256 characters. | | `hs_start_date` | read & write | date (YYYY-MM-DD) | The campaign's start date. | | `hs_end_date` | read & write | date (YYYY-MM-DD) | The campaign's end date. | | `hs_notes` | read & write | string | Any notes about the campaign. | | `hs_audience` | read & write | string | The campaign's audience. | | `hs_goal` | read & write | string | The campaign's goal. | | `hs_currency_code` | read & write | ISO currency code | The currency code used for the campaign. | | `hs_campaign_status` | read & write | predefined values | The status of the campaign. This includes `planned`, `in_progress`, `active`, `paused`, and `completed`. | | `hs_owner` | read | user id | The user id of the user that owns the campaign | | `hs_color_hex` | read | color id | The color of the campaign in hex color code. | | `hs_created_by_user_id` | read | user id | The user id of the user that originally created the campaign | | `hs_object_id` | read | string | The internal CRM object id. | | `hs_budget_items_sum_amount` | read | monetary value | The sum of all budget items. Learn more about managing your [campaign budget](https://knowledge.hubspot.com/campaigns/manage-your-campaign-budget). | | `hs_spend_items_sum_amount` | read | monetary value | The sum of all spend items. Learn more about managing your [campaign budget](https://knowledge.hubspot.com/campaigns/manage-your-campaign-budget). | # Marketing API | Marketing Email @ Learn how to create, update, and retrieve data for your emails using the Marketing Emails v3 API. $ guides & api/marketing/emails --- # Marketing Email You can use the Marketing Emails API to programmatically create, update, and get details about marketing emails. You can also query details about the post-send statistics of a specific email or set of emails. These statistics should match what you can access [in the app on the _Performance_ page](https://knowledge.hubspot.com/marketing-email/analyze-your-marketing-email-campaign-performance) of a sent email and will be returned under the stats object in your JSON response. The Marketing Email API cannot be used to create or retrieve data for sales emails that are created and sent via the contact record. To get details for sales emails, use the Engagements API. - To programmatically send transactional emails to contacts, use the [Single-send API](/guides/api/marketing/emails/transactional-emails#single-send-api). - To use the `/publish` and `/unpublish` endpoints, you must have a _**Marketing Hub**_ _Enterprise_ account or the [transactional email add-on](https://www.hubspot.com/products/marketing/transactional-email). - Starting on October 31st, 2024, as part of the Marketing Email API migration to use ILS lists, the `contactLists` parameter is no longer supported. Instead, you should provide the [ILS list IDs](https://knowledge.hubspot.com/lists/lists-faq#ils-list) using the `contactIlsLists` parameter when creating an email. Learn more on the [HubSpot Developer Changelog](https://developers.hubspot.com/changelog/marketing-email-api-ils-migration). ## Create a marketing email To create an email, make a `POST` request to `/marketing/v3/emails` and include the following fields in the body of your request: ```json // Example request body for POST request to /marketing/v3/emails { "name": "A new marketing email", "subject": "Behold the latest version of our newsletter!", "templatePath": "@hubspot/email/dnd/welcome.html" } ``` This endpoint supports a `businessUnitId` field, which you should include in the request body if you purchased the [Brands add-on](https://knowledge.hubspot.com/branding/manage-brands-with-business-unit). Be aware that business units is the former name for [brands](https://knowledge.hubspot.com/branding/manage-brands-with-business-unit), but this change does <u>not</u> impact the API. The field name you specify in the request body should still be `businessUnitId`. You can get a list of brands in your account using the [brands API](/guides/api/settings/business-units-api). ## Retrieve a marketing email You can retrieve existing emails in your account individually or in batches: - To retrieve an individual email, make a `GET` request to `/marketing/v3/emails/{emailId}` - To retrieve a list of all emails, make a `GET` request to `/marketing/v3/emails`, and include any filters as query parameters in your request (e.g., add `createdAfter` and a date in ISO8601 format to get all emails created after a specific date). Click the [reference documentation](/reference/api/marketing/emails/marketing-emails) for a full list of endpoints and the associated parameters available. # Marketing API | Single send API @ An overview of the marketing single-send API in HubSpot. $ guides & api/marketing/emails --- # Marketing Single Send API The single-send API allows you to send template emails created in the HubSpot [marketing email tool](https://knowledge.hubspot.com/marketing-email/create-marketing-emails-in-the-drag-and-drop-email-editor) using a JSON-formatted POST request. All contacts receiving marketing content must be [set as marketing](https://knowledge.hubspot.com/records/set-contacts-as-marketing). Any marketing emails sent through the single-send API will automatically be associated with contact records based on their email address, and update non-marketing contacts and set them to marketing contacts . If there's no contact with a matching email address, a new contact record with that email will be created, and the contact will be set as marketing. ## Requirements To use the marketing single send API, the following requirements must be met: - You must have a _**Marketing Hub** Enterprise_ account. - The [private app](/guides/apps/private-apps/overview) or [public app](/guides/apps/public-apps/overview) you're using to make API requests has been granted the `marketing-email` scope. ## Create an email and send it via the single-send API First, [set up your email in HubSpot](https://knowledge.hubspot.com/marketing-email/how-to-use-transactional-email-in-hubspot). After you create the email, you can set the recipient details, including any contact or custom properties set up in the email template, in the body of the API request. Before you can make the API request, you'll need the ID of the email: - If you leave the email drafted without publishing it, you can get the email ID from the URL when you're in the email editor. The ID is the final numeric value before the final slash character (`/`) in the URL (e.g., `https://app.hubspot.com/email/{PORTAL_ID}/edit/{EMAIL_ID}/settings`). - If you publish your email, you can copy the email ID from the email details page. HubSpot does not save the HTML/JSON sent through this API. You can review the email template from the recipient contact's timeline, but if you want to keep a record of the email's contents, it's recommended to add a BCC to the email. To send an email with the Single-Send API, make a `POST` request to `/marketing/v4/email/single-send`. The response contains the following fields: - `requestedAt`: the timestamp of when the send was requested. - `statusId`: an identifier that can be used to [query the status of the send](#query-the-status-of-an-email-send). - `status`: the status of the send request. Includes `PENDING`, `PROCESSING`, `CANCELED`, and `COMPLETE`. ### Request properties The request body must be a JSON-formatted object with the following properties: - `emailId` - `message` - `contactProperties` - `customProperties` #### emailId The `emailId` field contains the email's content ID, which can be found in HubSpot's email tool. #### message The message field is a JSON object containing anything that you want to override. At the minimum, you must include the `to` field. Message object fields: - `to`: the recipient of the email - `from`: the "From" header for the email. You can define a from name with the following format: `"from":"Sender Name <sender@hubspot.com>"` - `sendId`: the ID of a particular send. Only one email with a given `sendId` will be sent per account, so you can include this field to prevent duplicate email sends. - `replyTo`: a JSON list of "Reply-To" header values for the email. - `cc`: a JSON list of email addresses to send as Cc. - `bcc`: a JSON list of email addresses to send as Bcc. #### contactProperties The `contactProperties` field is a JSON map of contact property values. Each contact property value contains a `name` and `value`. Each property will be set on the contact record and will be visible in the template under: Use these properties when you want to set a contact property while you’re sending the email. For example, when sending a receipt you may want to set a `last_paid_date` property, as the sending of the receipt will have information about the last payment. ```json { "emailId": 4126643121, "message": { "to": "jdoe@hubspot.com" "sendId": "6" }, "contactProperties": { "last_paid_date": "2022-03-01", "firstname": "jane" } } ``` #### customProperties The `customProperties` field is a JSON map of key-value properties. These properties are generally related to the email itself, not the contact receiving the email. They will not appear in the [web page version of the email](https://knowledge.hubspot.com/marketing-email/create-a-web-version-of-your-marketing-email), or in the view of the email from the contact's timeline. These properties are also not stored in HubSpot and will only be included in the sent email. Each key in the `customProperties` field can be referenced in the template using a [HubL expression](/reference/cms/hubl/overview) for fields contained within the `custom` variable (e.g., `{{ custom.NAME_OF_PROPERTY }}` ). For example, if your email template references two properties, `purchaseUrl` and `productName`, you could provide the associated values for these properties with the following request body: ```json { "emailId": 4126643121, "message": { "to": "jdoe@hubspot.com" "sendId": "6" }, "customProperties": { "purchaseUrl": "https://example.com/link-to-product", "productName": "vanilla" } } ``` You can then reference these properties in your email template: ```hubl <!doctype html> <html> <p> Congrats on purchasing some of the best ice cream around. </p> <a href={{custom.purchaseUrl}}>{{custom.productName}}</a> </html> ``` The `customProperties` field only supports arrays when used with [programmable email content](/guides/cms/content/data-driven-content/emails-with-programmable-content). In your email template, you can reference the items defined in your `customProperties` field by using a [HubL expression](/reference/cms/hubl/overview) (e.g., using a [for loop](/reference/cms/hubl/loops) to render each item in a list). For example, if the `customProperties` you included in your request body was structured like the following JSON snippet below: ```json { "emailId": 4126643122, "message": { "to": "jdoe@hubspot.com" "sendId": "7" }, "customProperties": { "exampleArray": [ {"firstKey": "someValue", "secondKey": "anotherValue"}, {"firstKey": "value1", "secondKey": "value2" } ] } } ``` You could then reference the values for each item in `exampleArray` with the following HubL code: ```hubl <!doctype html> <html> <p> Thanks for your recent purchase! Here are the details of the items you'll be receiving: </p> <ul> {% for item in custom.exampleArray %} <li>First key: {{ item.firstKey }}, Second key: {{item.secondKey}}</li> {% endfor %} </ul> </html> ``` Once sent, the email would render the contents of the associated programmable email template as follows: ### Query the status of an email send To get the status of the email send, make a `GET` request to `https://api.hubapi.com/marketing/v3/email/send-statuses/{statusId}`. The response contains the following fields: - `sendResult`: an enumeration that represents the email's send status. The possible values are [listed below](#sendresult). - `requestedAt`: the timestamp from when the send was requested. - `startedAt`: the timestamp when the send began processing. - `completedAt`: the timestamp when the send completed. - `statusId`: an identifier that can be used to query the status of the send. - `status`: the status of the send request. Includes `PENDING`, `PROCESSING`, `CANCELED`, and `COMPLETE`. - `eventId`: if sent, the ID and created timestamp of the sent event. #### sendResult The `sendResult` is an enumeration that reflects the result of an email send attempt. Its possible values are: - `SENT`: the email was sent successfully. - `QUEUED`: the email was queued and will send as the queue gets processed. - `PORTAL_SUSPENDED`: due to [Acceptable Use Policy](https://legal.hubspot.com/acceptable-use) violations, the HubSpot customer's email has been suspended. - `INVALID_TO_ADDRESS`: the recipient address is invalid. This error will also occur if you attempt to send an email with any of the following role-based prefixes in the email address: `abuse`, `no-reply`, `noreply`, `root`, `spam`, `security`, `undisclosed-recipients`, `unsubscribe`, `inoc`, `postmaster`, or `privacy`. - `BLOCKED_DOMAIN`: the domain cannot receive emails from HubSpot at this time. - `PREVIOUSLY_BOUNCED`: the recipient has previously bounced, and the sending logic resulted in no send. - `PREVIOUS_SPAM`: the recipient has previously marked similar email as spam. - `INVALID_FROM_ADDRESS`: the _From_ address is invalid. - `MISSING_CONTENT`: the `emailId` is invalid, or the `emailId` corresponds to an email that wasn't set up for Single-Send. - `MISSING_TEMPLATE_PROPERTIES`: there are properties set up in the template that have not been included in the `customProperties` sent in the request. For a full list of the endpoints available with this API, check out the [reference documentation](/reference/api/marketing/emails/single-send-api). # Marketing API | Transactional Emails @ An overview of the Transactional Email feature in HubSpot. $ guides & api/marketing/emails --- # Transactional Email If you have the [transactional email add-on](https://www.hubspot.com/products/marketing/transactional-email), you can send emails over a dedicated IP address for commerce receipts, account updates, terms of service changes, and other essential business transactions. Transactional emails are for relationship-based interactions, unlike [marketing emails](/guides/api/marketing/emails/marketing-emails), which are typically used to promote content. ## Methods for sending transactional email There are three ways to implement transactional email: - [Set up transactional email in-app](https://knowledge.hubspot.com/marketing-email/how-to-use-transactional-email-in-hubspot) - [SMTP API](#smtp-api) - [Single-send API](#single-send-api) | **Method** | **Overview** | **Example use case** | | --- | --- | --- | | **In-app transactional Email** | Create transactional emails using HubSpot's email editor.This provides the same benefits of standard HubSpot emails, such as smart content, personalization and templates.Learn more about [setting up transactional emails in-app](https://knowledge.hubspot.com/marketing-email/how-to-use-transactional-email-in-hubspot). | Send a policy update email to your customers with a link to a new policy page. This is a service update, not a marketing email, so you don't need to include subscription links (e.g CAN-SPAM links). You don't need to use any custom properties or info from external systems. | | **SMTP API** | Send transactional email through your own site or app while also tracking email performance and create contact information within HubSpot. The optional ability to create contact information is based on the smtp token creation.Learn more in [SMTP API section below](#smtp-api) | Send an account signup confirmation email from a separate transactional email system, while also tracking email performance and creating contacts in HubSpot. | | **Single-send API** | A combination of in-app transactional email and SMTP API. Create transactional emails using HubSpot's email editor, and add custom external tokens to your email which you can send to HubSpot via the API. Learn more in the [single-send API section below](#single-send-api). | Send a purchase receipt email to your customer using HubSpot. The email is triggered when the purchase is made, and passes custom values from another system (e.g. purchased item and purchase total). In addition, track the performance of this email in HubSpot. | Any contacts who are CC'd on a transactional email will not be tracked and the email will not appear on the record timeline of the CC'd contact. ## SMTP API Transactional email sent using the SMTP API is automatically triggered by specific criteria, like making a purchase on an e-commerce website. This API integrates with any internal or third-party systems to both trigger the email and incorporate data stored outside of HubSpot (e.g. shipping info or purchase price). The email is sent from your system, but is wrapped with HubSpot tracking codes that allow full [engagement tracking and measurement](https://knowledge.hubspot.com/marketing-email/analyze-your-marketing-email-campaign-performance). To send an email using the SMTP API, you need to use an SMTP API token to get login credentials for the HubSpot SMTP server. Once you log in to the server, you can send the email over SMTP. If you haven't created any SMTP API tokens, you'll first need to [generate a new token](#create-a-new-smtp-api-token). If you've already created SMTP API tokens, learn about the [different methods for getting your tokens through the API](#get-existing-smtp-api-tokens). After getting your token, learn about how to [log in to HubSpot's SMTP server](#log-in). Any domains you use as the _From Address_ of your emails must be connected as an [email sending domain](https://knowledge.hubspot.com/marketing-email/manage-email-authentication-in-hubspot) in HubSpot. You will encounter an error if you send transactional emails through the SMTP API using a domain that isn't authorized to send on behalf of your HubSpot account. : all methods in the SMTP API require an [OAuth token](/guides/apps/authentication/oauth-quickstart-guide) for authentication. If you prefer, all of the methods below for creating, retrieving, deleting, and resetting passwords for SMTP API tokens can be done [within your HubSpot account](https://knowledge.hubspot.com/marketing-email/how-to-use-transactional-email-in-hubspot#send-a-transactional-email-using-the-smtp-api), rather than the API. ### Create a new SMTP API token To create a new SMTP API token, make a `POST` request to `/marketing/v3/transactional/smtp-tokens/`. The request body must be a JSON-formatted object with the following properties: - `createContact`: indicates whether a contact should be created for email recipients. - `campaignName`: a name for the campaign associated with the SMTP API token. The response includes an SMTP token object, which contains: - `id`: username to log into the HubSpot SMTP server. - `createdBy`: email address of the user that sent the token creation request. - `password`: the password for logging in to the HubSpot SMTP server. - `emailCampaignId`: identifier assigned to the campaign provided in the token creation request. - `createdAt`: timestamp generated when a token is created. - `createContact`: indicates whether a contact should be created for email recipients. - `campaignName`: the name of the campaign associated with the token. With your token created, you can [log in to HubSpot's SMTP server](#log-in) using the `id` and `password` values. A token's password can only be retrieved at the time of creation. If you lose the password, or want to set a new password, you'll need to [reset the token's password](#manage-existing-tokens). ### Token deactivation and expiration To protect developers from potential security incidents, HubSpot will monitor any SMTP authentication tokens that are publicly exposed in GitHub repositories and deactivate them. Any detected SMTP tokens will automatically be deactivated, and you will be notified via email and in-app notification so you can generate a new token and update your integrations to replace the revoked token. Learn more about [automatic token deactivation](/guides/apps/authentication/intro-to-auth#automatic-token-deactivation). SMTP API tokens generated through the public API expire after 12 months. Once expired, they're automatically deleted. Tokens created directly [within your HubSpot account](https://knowledge.hubspot.com/marketing-email/how-to-use-transactional-email-in-hubspot#send-a-transactional-email-using-the-smtp-api) do <u>not</u> expire automatically. ### Retrieving SMTP tokens Below are the available methods of getting token data using the API. #### List SMTP API tokens by campaign To get either a list of tokens by campaign name, or get a single token by campaign ID, make a `GET` request to `/marketing/v3/transactional/smtp-tokens`. You'll also need to include either a `campaignName` or `emailCampaignId` parameter with the request. You can find all request details in the [transactional email endpoint reference documentation](/reference/api/marketing/emails/transactional-emails). **Response details** The response contains `results` and `paging` as its top-level fields: - `results`: a collection of `SmtpApiTokenView` containing: - `id`: username to log into the HubSpot SMTP server. - `createdBy`: email address of the user that sent the token creation request. - `emailCampaignId`: identifier assigned to the campaign provided in the token creation request. - `createdAt`: timestamp generated when a token is created. - `createContact`: indicates whether a contact should be created for email recipients. - `campaignName`: the name of the campaign associated with the token. - `paging`: contains a `next.after` field that can be used to request more results. #### Query a single SMTP API token To query a single SMTP API token by ID, make a `GET` request to `/marketing/v3/transactional/smtp-tokens/{tokenId}`. ##### Response details The response includes `SmtpApiTokenView`, which contains: - `id`: username to log into the HubSpot SMTP server. - `createdBy`: email address of the user that sent the token creation request. - `emailCampaignId`: identifier assigned to the campaign provided in the token creation request. - `createdAt`: timestamp generated when a token is created. - `createContact`: indicates whether a contact should be created for email recipients. - `campaignName`: the name of the campaign associated with the token. ### Manage existing tokens After creating tokens, you can reset a password or delete the token using the API. #### Reset password To reset a token password, make a POST request to `/marketing/v3/transactional/smtp-tokens/{tokenId}/password-reset`. The response includes `SmtpApiTokenView`, which contains: - `id`: username to log into the HubSpot SMTP server. - `createdBy`: email address of the user that sent the token creation request. - `emailCampaignId`: identifier assigned to the campaign provided in the token creation request. - `createdAt`: timestamp generated when a token is created. - `createContact`: indicates whether a contact should be created for email recipients. - `campaignName`: the name of the campaign associated with the token. #### Delete a token To delete a single SMTP API token, make a DELETE request to `/marketing/v3/transactional/smtp-tokens/{tokenId}`. The response does not include any content. ### Log in to HubSpot's SMTP server Below are the details for logging in to HubSpot's SMTP server, using the username (`id`) and password provided by your token. - **SMTP Hostname:** - If you're not based in the EU, use `smtp.hubapi.com` for the hostname. - If you're based in the EU, use `smtp-eu1.hubapi.com` for the hostname. - **SMTP Port:** - For STARTTLS, you can use port 25 or 587. - For direct TLS, use port 465. - **SMTP User Name:** provided in the ID field - **SMTP Password:** provided in the password field ## Single-send API The Single-Send API sends template emails created in the HubSpot email tool using a JSON-formatted `POST` request. Any emails sent through this API will be automatically associated with contact records based on email address. If there's no contact with a matching email address, a new contact with that email will be created. If you want to send emails without creating contacts, use the [SMTP API](#SMTP-API). ### Create and publish your email template First, [set up your email in HubSpot](https://knowledge.hubspot.com/marketing-email/how-to-use-transactional-email-in-hubspot). After you create the email, you can set the recipient details, including any contact or custom properties set up in the email template, in the body of the API request. Before you can make the API request, you'll need the ID of the email: - If you leave the email drafted without publishing it, you can get the email ID from the URL when you're in the email editor. The ID is the final numeric value before the final slash character (`/`) in the URL (e.g., `https://app.hubspot.com/email/{PORTAL_ID}/edit/{EMAIL_ID}/settings`). - If you publish your email, you can copy the email ID from the email details page. HubSpot does not save the HTML/JSON sent through this API. You can review the email template from the recipient contact's timeline, but if you want to keep a record of the email's contents, it's recommended to add a BCC to the email. ### Send your email using the single-send API To send an email with the Single-Send API, make a `POST` request to `/marketing/v3/transactional/single-email/send`. The response contains the following fields: - `requestedAt`: the timestamp of when the send was requested. - `statusId`: an identifier that can be used to [query the status of the send](#query-the-status-of-an-email-send). - `status`: the status of the send request. Includes `PENDING`, `PROCESSING`, `CANCELED`, and `COMPLETE`. ### Request properties The request body must be a JSON-formatted object with the following properties: - `emailId` - `message` - `contactProperties` - `customProperties` #### emailId The `emailId` field contains the transactional email's content ID, which can be found in HubSpot's email tool. #### message The message field is a JSON object containing anything that you want to override. At the minimum, you must include the `to` field. Message object fields: - `to`: the recipient of the email - `from`: the "From" header for the email. You can define a from name with the following format: `"from":"Sender Name <sender@hubspot.com>"` - `sendId`: the ID of a particular send. Only one email with a given `sendId` will be sent per account, so you can include this field to prevent duplicate email sends. - `replyTo`: a JSON list of "Reply-To" header values for the email. - `cc`: a JSON list of email addresses to send as Cc. - `bcc`: a JSON list of email addresses to send as Bcc. #### contactProperties The `contactProperties` field is a JSON map of contact property values. Each contact property value contains a `name` and `value`. Each property will be set on the contact record and will be visible in the template under: Use these properties when you want to set a contact property while you’re sending the email. For example, when sending a receipt you may want to set a `last_paid_date` property, as the sending of the receipt will have information about the last payment. ```json { "emailId": 4126643121, "message": { "to": "jdoe@hubspot.com" "sendId": "6" }, "contactProperties": { "last_paid_date": "2022-03-01", "firstname": "jane" } } ``` #### customProperties The `customProperties` field is a JSON map of key-value properties. These properties are generally related to the email itself, not the contact receiving the email. They will not appear in the [web page version of the email](https://knowledge.hubspot.com/marketing-email/create-a-web-version-of-your-marketing-email), or in the view of the email from the contact's timeline. These properties are also not stored in HubSpot and will only be included in the sent email. Each key in the `customProperties` field can be referenced in the template using a [HubL expression](/reference/cms/hubl/overview) for fields contained within the `custom` variable (e.g., `{{ custom.NAME_OF_PROPERTY }}` ). For example, if your email template references two properties, `purchaseUrl` and `productName`, you could provide the associated values for these properties with the following request body: ```json { "emailId": 4126643121, "message": { "to": "jdoe@hubspot.com" "sendId": "6" }, "customProperties": { "purchaseUrl": "https://example.com/link-to-product", "productName": "vanilla" } } ``` You can then reference these properties in your email template: ```hubl <!doctype html> <html> <p> Congrats on purchasing some of the best ice cream around. </p> <a href={{custom.purchaseUrl}}>{{custom.productName}}</a> </html> ``` The `customProperties` field only supports arrays when used with [programmable email content](/guides/cms/content/data-driven-content/emails-with-programmable-content). In your email template, you can reference the items defined in your `customProperties` field by using a [HubL expression](/reference/cms/hubl/overview) (e.g., using a [for loop](/reference/cms/hubl/loops) to render each item in a list). For example, if the `customProperties` you included in your request body was structured like the following JSON snippet below: ```json { "emailId": 4126643122, "message": { "to": "jdoe@hubspot.com" "sendId": "7" }, "customProperties": { "exampleArray": [ {"firstKey": "someValue", "secondKey": "anotherValue"}, {"firstKey": "value1", "secondKey": "value2" } ] } } ``` You could then reference the values for each item in `exampleArray` with the following HubL code: ```hubl <!doctype html> <html> <p> Thanks for your recent purchase! Here are the details of the items you'll be receiving: </p> <ul> {% for item in custom.exampleArray %} <li>First key: {{ item.firstKey }}, Second key: {{item.secondKey}}</li> {% endfor %} </ul> </html> ``` Although you can include objects as items in an array within `customProperties`, accessing nested object fields from directly within `customProperties` is not supported. For example, if you defined a `nested_object` field within `customProperties` that included a `title` property, nothing will render if you attempt to access `customProperties.nested_object.title`. Once sent, the transactional email would render the contents of the associated programmable email template as follows: ### Query the status of an email send To get the status of the email send, make a `GET` request to `https://api.hubapi.com/marketing/v3/email/send-statuses/{statusId}`. The response contains the following fields: - `sendResult`: an enumeration that represents the email's send status. The possible values are [listed below](#sendresult). - `requestedAt`: the timestamp from when the send was requested. - `startedAt`: the timestamp when the send began processing. - `completedAt`: the timestamp when the send completed. - `statusId`: an identifier that can be used to query the status of the send. - `status`: the status of the send request. Includes `PENDING`, `PROCESSING`, `CANCELED`, and `COMPLETE`. - `eventId`: if sent, the ID and created timestamp of the sent event. #### sendResult The `sendResult` is an enumeration that reflects the result of an email send attempt. Its possible values are: - `SENT`: the email was sent successfully. - `QUEUED`: the email was queued and will send as the queue gets processed. - `PORTAL_SUSPENDED`: due to [Acceptable Use Policy](https://legal.hubspot.com/acceptable-use) violations, the HubSpot customer's email has been suspended. - `INVALID_TO_ADDRESS`: the recipient address is invalid. This error will also occur if you attempt to send an email with any of the following role-based prefixes in the email address: `abuse`, `no-reply`, `noreply`, `root`, `spam`, `security`, `undisclosed-recipients`, `unsubscribe`, `inoc`, `postmaster`, or `privacy`. - `BLOCKED_DOMAIN`: the domain cannot receive emails from HubSpot at this time. - `PREVIOUSLY_BOUNCED`: the recipient has previously bounced, and the sending logic resulted in no send. - `PREVIOUS_SPAM`: the recipient has previously marked similar email as spam. - `INVALID_FROM_ADDRESS`: the _From_ address is invalid. - `MISSING_CONTENT`: the `emailId` is invalid, or the `emailId` corresponds to an email that wasn't set up for Single-Send. - `MISSING_TEMPLATE_PROPERTIES`: there are properties set up in the template that have not been included in the `customProperties` sent in the request. # Marketing API | Forms @ These endpoints are used to create and manage forms in your HubSpot account. $ guides & api/marketing/forms --- # Forms Use a HubSpot form to collect lead information about your visitors and contacts. You can use the endpoints outlined here to set up new forms or get details of forms you've previously created. If you're looking to send form submission data, you can use the [Submit data to a form endpoint](/reference/api/marketing/forms/v3-legacy#submit-data-to-a-form-supporting-authentication) instead. The form's type indicates its purpose and is set to `hubspot` by default. You can use the following values for your `formType`: - `hubspot`: these forms offer a variety of field types and styling options and can be used embedded in either HubSpot pages or external pages. These forms can be created and edited using the endpoints described here. You can also create these forms within your HubSpot account, learn more about [creating HubSpot forms](https://knowledge.hubspot.com/forms/create-forms). - `captured`: these forms correspond to HTML forms in external websites. If the non-HubSpot forms tool is enabled and there are submissions to the form on [a tracked page](https://knowledge.hubspot.com/reports/install-the-hubspot-tracking-code), the form is automatically created in HubSpot. Learn more about [using non-HubSpot forms](https://knowledge.hubspot.com/forms/use-non-hubspot-forms). - `flow`: these are pop-up forms that can be used in either HubSpot pages or external pages. Learn more about [HubSpot's pop-up forms tool](https://knowledge.hubspot.com/forms/create-pop-up-forms). - `blog_comment`: these forms are automatically created for HubSpot blog pages to collect comments on blog posts. Learn more about how to further [set up and moderate blog comments](https://knowledge.hubspot.com/blog/set-up-and-moderate-your-blog-comments). # Global form events @ Learn how to use listeners to form global events to define custom behavior when users interact with forms on your site. $ guides & api/marketing/forms --- # Global form events Global form events are a set of event callbacks that you can use to extend form functionality and define custom behavior in response to form events triggered by users on your site. The functionality described in this article only pertains to forms created through the [updated forms editor](https://knowledge.hubspot.com/forms/create-and-edit-forms) in HubSpot's UI. If you need to work with global form events for a [legacy form](https://knowledge.hubspot.com/forms/create-forms), check out this [article instead](/reference/cms/forms#form-events). ## Form events Throughout their lifecycle, HubSpot forms will emit various events based form readiness and user interactions. These events are defined in the table below. You can define listeners for each event by using the `window.addEventListener(EVENT_NAME)` function. | Event name | Description | | --- | --- | | `hs-form-event:on-ready` | The form has finished loading and has been fully rendered. | | `hs-form-event:on-submission:success` | The form has been submitted successfully. | | `hs-form-event:on-submission:failed` | The form submission failed. | | `hs-form-event:on-interaction:navigate` | On a multi-step form, the user has navigated to the previous or next step in the form. | For example, the code snippet below defines an event listener that listens for a successful form submission: ```js window.addEventListener('hs-form-event:on-submission:success', (event) => { console.log('User successfully submitted the form!'); }); ``` ## Event object All global form events are emitted with an event object that's passed to the event handler. This event object includes two nested IDs, the `formId` and the `instanceId`, within the `details` field, which allows you to reference the ID of the form and the specific instance of that form on your page. ```js window.addEventListener('hs-form-event:on-ready', (event) => { const { formId, instanceId } = event.detail; console.log(formId); }); ``` The event object can also be passed as an argument to the `getFormFromEvent` method accessible from the `HubSpotFormsV4` object, which is available on any page where your form is present. This method returns a form instance that can be used to retrieve or populate data in the from. ```js window.addEventListener('hs-form-event:on-submission:success', (event) => { const form = HubSpotFormsV4.getFormFromEvent(event); const conversionId = form.getConversionId(); console.log(conversionId); }); ``` **Please note:** you should define any `hs-form-event:on-ready` listeners on your page before the form embed code is executed. This will ensure that your listeners are set up to listen for the event before it's emitted and avoid race conditions. ## Getting form instances HubSpot provides instance API methods within the `window.HubSpotFormsV4` object. The available methods are detailed in the sections below. ### getForms To retrieve all form instances on a page, call the `getForms` method. The method will return an array of `form` instances. ```js HubSpotFormsV4.getForms(); ``` ### getFormFromEvent To get a specific form instance using an emitted form event, call the `getFormFromEvent` method, using the emitted `event` object as an argument. The method will return a specific `form` instance object. ```js HubSpotFormsV4.getFormFromEvent(event); ``` **Please note:** the `form` instance object includes several fields intended for internal use only, such as `__SECRET_INTERNAL_DO_NOT_USE`. Avoid usage of these fields as they are unstable, and are not designed for public usage. Usage of these fields could break your integration. ## Form instance methods Form instance methods are useful for retrieving and passing data to and from your form. Although these methods can be called at any point in the form's lifecycle, some form data fields can only be retrieved or set before the form is submitted, while others can only be accessed after form submission. ### getFormId Use the `getFormId` method to retrieve the ID of your form. The method will return the ID as a string. ```js HubSpotFormsV4.getForms()[0].getFormId(); HubSpotFormsV4.getFormFromEvent(event).getFormId(); ``` ### getInstanceId Use the `getInstanceId` method to retrieve the ID of the form instance. The method will return the ID as a string. ```js HubSpotFormsV4.getForms()[0].getFormId(); HubSpotFormsV4.getFormFromEvent(event).getInstanceId(); ``` ### getFieldValue Use the `async` `getFieldValue` method to retrieve the value of a field in your form. The method accepts the field's `name` as a string argument, and returns either a string value or an array of string values for multi-checkbox fields. You can also use this method to retrieve the value of a conditionally hidden field. ```js HubSpotFormsV4.getForms()[0] .getFieldValue('0-1/firstname') .then((value) => value); await HubSpotFormsV4.getFormFromEvent(event).getFieldValue('0-1/firstname'); ``` The method will return data that resembles the following: ```json // "John" ``` ### getFormFieldValues Use the `async` `getFormFieldValues` method to retrieve the values of all the fields in your form. This method returns an array of key-value pair objects that contain the field name as a string and the value as either a string or an array of strings for multi-checkbox fields. You can also use this method to retrieve the value of a conditionally hidden field. ```js HubSpotFormsV4.getForms()[0] .getFormFieldValues() .then((values) => values); await HubSpotFormsV4.getFormFromEvent(event).getFormFieldValues(); ``` The method will return data that resembles the following: ```json [ { "name": "0-1/firstname", "value": "Hubspot" }, { "name": "0-1/multicheckbox", "value": ["yes", "no"] } ] ``` **Please note:** the value of a file field will not be returned if you attempt to get the value using either the `getFieldValue` or `getFormFieldValues` methods. ### getConversionId Use the `getConversionId` method to retrieve the ID of a form conversion. The method will return a valid string value when called after the form is successfully submitted. ```js HubSpotFormsV4.getForms()[0].getConversionId(); HubSpotFormsV4.getFormFromEvent(event).getConversionId(); ``` ### getRedirectUrl For forms set up to redirect to a specific URL after submission, you can use the `getRedirectUrl` method to retrieve the redirect URL. This method returns the URL as a string after successful form submission. ```js HubSpotFormsV4.getForms()[0].getRedirectUrl(); HubSpotFormsV4.getFormFromEvent(event).getRedirectUrl(); ``` ### getExtraSubmissionMetadata Use the `getExtraSubmissionMetadata` method to pass additional metadata to the form submission. The method accepts an object with a set of key-value pairs, which will be sent as part of the form submission. ```js HubSpotFormsV4.getForms()[0].setExtraSubmissionMetadata({ pageId: 'page-123' }); HubSpotFormsV4.getFormFromEvent(event).setExtraSubmissionMetadata({ pageId: 'page-123', }); ``` ### setFieldValue You can use the `setFieldValue` method to programmatically update the value of a field on the form. Provide the field's `name` as the first string argument, then the field's `value` as the second argument. ```js HubSpotFormsV4.getForms()[0].setFieldValue('0-1/firstname', 'John'); HubSpotFormsV4.getFormFromEvent(event).setFieldValue('0-1/firstname', 'John'); ``` **Please note:** setting the value of a file field is not supported. The table below provides a list of supported field types and data types accepted for their values: | Field type | Data type | | --- | --- | | Single-line text | string | | Number | string, number | | Single checkbox | boolean | | Multiple checkboxes | string[] | | Dropdown | string | | Phone number | string, number | | Date | string, number (provided as a UNIX timestamp in milliseconds) | | Multi-line text | string | | Radio | string | | Hidden | string[] | # Marketing API | Marketing Events @ An overview of the Marketing Events object in HubSpot. $ guides & api/marketing --- A marketing event is a CRM object, similar to contacts and companies, that enables you to track marketing events, such as a webinar, along with the contacts who registered and attended the event. Below, learn more about working with the marketing event API to integrate marketing events into an app. ## In this article - [Scope requirements](#scope-requirements) - [Differences between internal ID and external ID endpoints](#differences-between-internal-id-and-external-id-endpoints) - [Event management endpoints](#create-and-update-events) - [Event attendance endpoints](#event-attendance-endpoints) - [Participant state endpoints](#participant-state-endpoints) - [List association endpoints](#list-association-endpoints) - [Configure app settings](#configure-app-settings) - [Step 1: Create an API in your app](#step-1-create-an-api-in-your-app) - [Step 2: Provide HubSpot with the URL path to your API](#step-2-provide-hubspot-with-the-url-path-to-your-api) ## Scope requirements To make an API request to one of the marketing event endpoints, the following [scopes](/guides/apps/authentication/working-with-oauth#scopes) are required: - `crm.objects.marketing_events.read`: grants permission to retrieve marketing event and attendance data. - `crm.objects.marketing_events.write`: grants permission to create, delete, or make changes to marketing event information. When authenticating calls made by your app, you can either use a [private app access token](/guides/apps/private-apps/overview) or [OAuth](/guides/api/app-management/oauth-tokens). Learn more about [authentication methods](/guides/apps/authentication/intro-to-auth). For the full list of endpoints available, check out the [reference documentation](/reference/api/marketing/marketing-events). ## Differences between internal ID and external ID endpoints Many endpoints below provide two different ways to identify an event you want to fetch or update. Though the end result for similar endpoints might be the same, they differ mainly in the associated IDs you provide: - **Endpoints using external IDs:** endpoints that require `externalEventId` and `externalAccountId` parameters will only work in the same app that originally created the event. For example, if you created two public apps, called _App A_ and _App B_, and you created a marketing event via the authentication and IDs associated with _App A_, only _App A_ can read, update, or add new participants to the event. If you attempt to access the same event with _App B_ using the same externalEventId and externalAccountId, a 404 error will result. - **Endpoints using an objectId:** endpoints that require an `objectId` can be used to access an event by any app with the associated scopes listed in the section above, regardless of the app that originally created the event. If _App A_ created a marketing event, _App B_ can still read, update, or add participants through `objectId`\-based endpoints. ## Event management endpoints The following sections provide context on common event properties, and detail how to use the various event management endpoints to create, read, update, and archive events. ### Event properties The following properties are available to fetch and update when using the event management endpoints: | Parameter | Type | Description | | --- | --- | --- | | `eventName` | String | The title of your event. | | `eventType` | String | The type of the event (e.g., webinar, tradeshow, etc.). | | `eventOrganizer` | String | The individual or organization that's hosting the event. | | `eventDescription` | String | A description for your event. | | `eventUrl` | String | A URL that users can navigate to where they can learn more details and/or register for your event. | | `eventCancelled` | Boolean | Whether or not the event is cancelled. | | `eventStartTime` | String | An ISO 8601 formatted timestamp of the event's start time. | | `eventEndTime` | String | An ISO 8601 formatted timestamp of the event's end time. | ### Create an event To create a marketing event you can make a `POST` request to `/marketing/v3/marketing-events/events` and provide the `eventName`, `externalEventId`, `externalAccountId`, and `eventOrganizer` in the body of your request. You can optionally provide any additional properties listed in the [table above](#event-properties) in your request. For example, if the `externalAccountId` of your app is `"12345"`, and the `externalEventId` of your event in your app is `"67890"`, you could create a new event called `"Winter webinar"` with a request that would resemble the following: ```json // Example request body for POST request to /marketing/v3/marketing-events { "externalAccountId": "12345", "externalEventId": "67890", "eventName": "Winter webinar", "eventOrganizer": "Snowman Fellowship", "eventCancelled": false, "eventUrl": "https://example.com/holiday-jam", "eventDescription": "Let's get together to plan for the holidays", "eventCompleted": false, "startDateTime": "2024-08-07T12:36:59.286Z", "endDateTime": "2024-08-07T12:36:59.286Z", "customProperties": [ { "eventSeason": "winter" } ] } ``` ### Update event properties using external IDs You can update marketing events by making a `POST` request to `/marketing/v3/marketing-events/events/upsert` endpoint. You can include any `customProperties` along with any other details of your event (including its name, start time, and description). If a marketing event already exists with the specified ID in your request, it will be updated. Otherwise, a new event will be created. For example, the following request would create an event with an ID of `4` named "Virtual cooking class": ```json // Example request body for POST request to /marketing/v3/marketing-events/events/upsert { "inputs": [ { "customProperties": [ { "name": "property1", "value": "1234" } ], "eventName": "Virtual cooking class", "startDateTime": "2023-11-30T17:46:20.461Z", "eventOrganizer": "Chef Joe", "eventDescription": "Join us for a virtual cooking class! Yum." "eventCancelled": false, "externalAccountId": "CookingCo", "externalEventId": "4" } ] } ``` ### Update event properties using its objectId Once an event is created, you can update its properties by making a `PATCH` request to `/marketing/v3/marketing-events/{objectId}`. - To get the `objectId` for a specific marketing event, follow the instructions in [this knowledge base article](https://knowledge.hubspot.com/integrations/use-marketing-events#view-and-analyze-marketing-events) to view the details for an event in your HubSpot account, then locate the ID under the _Record ID_ field. The `objectId` will also be returned in the response when you successfully create an event. - You can also make a `GET` request to the `/marketing/v3/marketing-events` endpoint described in the next section. - If you have the `externalEventId` of an event, you can include it as a path when making a `GET` request to `/marketing/v3/marketing-events/{externalEventId}/identifiers`. The response will include all marketing events along with the relevant identifiers for each event (i.e., the event's `objectId`, its `appInfo`, the `marketingEventName`, and the `externalAccountId`). ### Get event details To get a list of all marketing events along with their properties, make a `GET` request to `/marketing/v3/marketing-events`. If you need to retrieve the details for a specific marketing event by its _Record ID_ in HubSpot, you can provide the ID as the objectId in a `GET` request to `/marketing/v3/marketing-events/{objectId}`. ```json // Example response for GET request to /marketing/v3/marketing-events/{objectId} { "eventName": "Test Marketing Event", "eventType": "test-type", "startDateTime": "2024-05-22T12:29:50.734Z", "endDateTime": "2024-05-25T12:29:50.734Z", "eventOrganizer": "testEventOrganizer", "eventDescription": "testDescription", "eventUrl": "testURL", "eventCancelled": true, "eventCompleted": false, "customProperties": [ { "name": "test_custom_prop", "value": "1" }, { "name": "test_prop", "value": "2" } ], "objectId": "58237132332", "externalEventId": null, "eventStatus": "CANCELLED", "appInfo": { "id": "111", "name": "Zoom" }, "registrants": 1, "attendees": 1, "cancellations": 2, "noShows": 0, "createdAt": "2024-08-07T12:58:40.635Z", "updatedAt": "2024-10-15T13:35:03.353Z" } ``` ### Delete an event To delete a marketing event, make a `DELETE` request to `/marketing/v3/marketing-events/{objectId}` with the event's associated `objectId`. If successful, you'll receive a `204 No Content` response. ### Update multiple events in bulk To update multiple marketing events in bulk, you can make a `POST` request to `/marketing-events/v3/marketing-events/batch/update` and provide the properties you want to update for each event within the inputs array of the request body. For example, if you wanted to update several properties of two marketing events with object IDs of 58237132332 and 54073507364 in a single request, the body of your request would resemble the following: ```json // Example request body for batch request to update marketing events { "inputs": [ { "objectId": "58237132332", "eventCancelled": true, "eventOrganizer": "testEventOrganizer", "eventUrl": "testURL", "eventDescription": "testDescription", "eventName": "Test Marketing Event Update", "eventType": "test-type" }, { "objectId": "54073507364", "eventCancelled": true, "eventOrganizer": "testEventOrganizer", "eventUrl": "testURL", "eventDescription": "testDescription", "eventName": "Test Marketing Event Update 2", "eventType": "test-type" } ] } ``` ## Event attendance endpoints The event attendance state endpoints allow you to record registration activities for a contact, such as whether they registered, attended, or cancelled their registration for your event. For example, you can use this endpoint to record that a HubSpot contact has registered for a marketing event. ### Update attendance using the event objectId If you want to use the `objectId` of a marketing event, you can either use the contact ID of the contact you want to record participation state for, or you can use their email address. - To use a contact's ID, make a POST request to `/marketing/v3/marketing-events/{objectId}/attendance/{subscribeState}/create` then provide the ID of the contact using the `vid` field within the `inputs` array of your request body. For example, the request body below provides an example of updating the attendance data for a contact with an ID of `47733471576` and specifying when the attendee joined and left the event via the `joinedAt` and `leftAt` properties: ```json // Example POST request to /marketing/v3/marketing-events/{objectId}/attendance/{subscriberState}/create { "inputs": [ { "vid": 47733471576, "properties": { "joinedAt": "2024-05-22T13:38:16.500Z", "leftAt": "2024-05-22T15:40:16.500Z" }, "interactionDateTime": 1716382579000 } ] } ``` - To use a contact's email, make a POST request to `/marketing/v3/marketing-events/{objectId}/attendance/{subscribeState}/email-create` then provide the email of the contact using the `email` field within the `inputs` array of your request body. - If you're creating a new contact, you can include the `contactProperties` field within the `inputs` array of your request body to set any associated properties on the newly created contact. Otherwise, if the contact already exists, `contactProperties` provided in the request will <u>not</u> be updated. - For example, the request body below provides an example of updating the attendance data for a contact with an email address of `john@example.com` and specifying when the attendee joined and left the event via the `joinedAt` and `leftAt` fields within the `properties` object of your `inputs` array: ```json // Example POST request to /marketing/v3/marketing-events/{objectId}/attendance/{subscriberState}/create { "inputs": [ { "contactProperties": { "additionalProp1": "string", "additionalProp2": "string" }, "properties": { "joinedAt": "2024-05-22T13:38:16.500Z", "leftAt": "2024-05-22T15:40:16.500Z" }, "email": "john@example.com", "interactionDateTime": 1716382579000 } ] } ``` For either of the approaches above, provide the following values for the corresponding path parameters: - `objectId`: the _Record ID_ of the marketing event in your HubSpot account. Check out the [section above](#differences-between-internal-id-and-external-id-endpoints) for more details on using the objectId of an event versus using its external IDs. - `subscriberState`: an enumeration that matches the new attendance status of the contact: - `REGISTERED`: indicates that the HubSpot contact has registered for the event. - `ATTENDED`: indicates that the HubSpot contact has attended the event. If you're updating a contact's status to ATTENDED, you can also include the `joinedAt` and `leftAt` timestamps as parameters in the request body, specified in the ISO8601 Instant format. - `CANCELLED`: indicates that the HubSpot contact, who had previously registered for the event, has cancelled their registration. ### Update attendance using the external IDs of the event If you were previously using the `/upsert` or `/email-upsert` endpoints to update an attendee's status, you can instead use the alternate endpoints listed below. However, compared to the event attendance endpoints above, using these endpoints will <u>not</u> provide support for the following: - Creating a new contact if it doesn't already exist. - Showing timeline events on the contact record page. - Specifying the `joinedAt` or `leftAt` properties. - Providing a detailed response upon success. If you do use the endpoints that require the `externalEventId` from your app, you can either use the contact IDs or email address of existing contacts: - If you want to use the contact IDs of existing contacts: - Make a `POST` request to `/marketing/v3/marketing-events/attendance/{externalEventId}/{subscriberState}/create`, using the ID of the event from your external application as the `externalEventId`. - In the request body, provide an `inputs` object that includes the following fields: - `interactionDateTime`: the date and time at which the contact subscribed to the event. - `vid`: the contact ID of an existing contact. - If you want to use the email address of one of the event's attendees: - Make a `POST` request to `/marketing/v3/marketing-events/attendance/{externalEventId}/{subscriberState}/email-create`. - In the request body, provide an `inputs` object that includes the following fields: - `interactionDateTime`: the date and time at which the contact subscribed to the event. - `email`: the email address of the attendee as the value of the email field within an inputs - If the email address you include don't match the address of an existing contact, a new contact will be created. For both of the endpoints above, provide the following values for the corresponding path parameters: - `externalEventId`: the ID of the [marketing event](https://knowledge.hubspot.com/integrations/use-marketing-events#view-edit-and-analyze-marketing-events). Check out the [section above](#differences-between-internal-id-and-external-id-endpoints) for more details on using the objectId of an event versus using its external IDs. - `subscriberState`: an enumeration that matches the new attendance status of the contact: - `REGISTERED`: indicates that the HubSpot contact has registered for the event. - `ATTENDED`: indicates that the HubSpot contact has attended the event. If you're updating a contact's status to ATTENDED, you can also include the `joinedAt` and `leftAt` timestamps as parameters in the request body, specified in the ISO8601 Instant format. - `CANCELLED`: indicates that the HubSpot contact, who had previously registered for the event, has cancelled their registration. These APIs are idempotent so long as the ID of the contact and the `interactionDateTime` value in the event has not changed. This enables you to safely set attendance state multiple times without HubSpot creating duplicate events in the marketing events properties. ## Participant state endpoints You can use the participation endpoints to retrieve event participant data for your marketing events. You can query data such as aggregate metrics for a specific event, as well as participant data for a specific contact or event. Review the available participation endpoints below. For a full reference of all available parameters for each endpoint, check out the [reference documentation](/reference/api/marketing/marketing-events). The activity counts in the [marketing events page](https://knowledge.hubspot.com/integrations/use-marketing-events) in your HubSpot account may differ from the corresponding aggregate metrics from the participation counters API endpoint. For example, if a participant registered for an event, then cancelled, then re-registered for the same event, each of those activities will be included in the totals you see in the marketing events UI in your account. If you're using the participant state endpoints below, only the current state of a participant is included in the associated counter for that metric (e.g., `attended`, `registered`, `cancelled`, or `noShows`). ### Read participations for a specific contact To get event participation data for a specific contact, make a `GET` request to `/marketing/v3/marketing-events/participations/contacts/{contactIdentifier}/breakdown`, using's the contact's ID or email address as the `contactIdentifier` path parameter. The response will include a summary of the contact's event participation in the `properties` field: ```json // Example response for GET request for contact participation data { "results": [ { "associations": { "marketingEvent": { "externalAccountId": "4", "marketingEventId": "123", "externalEventId": "456", "name": "Virtual baking workshop" }, "contact": { "firstname": "Jane", "contactId": "156792341", "email": "jdoe@example.com", "lastname": "Doe" } }, "createdAt": "2024-05-21T18:35:04.838Z", "id": "string", "properties": { "occurredAt": "2024-05-22T10:35:04.838Z", "attendancePercentage": "string", "attendanceState": "REGISTERED", "attendanceDurationSeconds": 3600 } } ] } ``` ### Read participation breakdown data To get a breakdown of participation data for a specific event, use your `externalAccountId` and the `externalEventId` of your event to make a `GET` request to `/marketing/v3/marketing-events/participations/{externalAccountId}/{externalEventId}/breakdown`. ### Read participation counters To get an aggregate participation summary for an event, use your `externalAccountId` and the `externalEventId` of your event to make a `GET` request to `/marketing/v3/marketing-events/participations/{externalAccountId}/{externalEventId}`. The response will include the total attendance counts: ```json // Example response for GET request for event participation counters { "attended": 152, "registered": 200, "cancelled": 3, "noShows": 8 } ``` ### Filtering participation breakdown data When fetching breakdown data or event participation data for a specific contact, you can filter the resulting data using the contactIdentifier, state, limit, or after fields as query parameters in your request. | Query parameter | Type | Description | | --- | --- | --- | | `contactIdentifier` | string | The email address or ID of a specific contact | | `state` | Enumeration | The participation state for the event. The possible participation states are:<ul><li>`REGISTERED`: The contact has registered for the event</li><li>`CANCELLED`: The contact's registration has been cancelled.</li><li>`ATTENDED`: The contact attended the event.</li><li>`NO_SHOW`: The contact registered but did not end up attending the event.</li></ul> | | `limit` | Number | Limit the results returned. By default, the limit is set to 10. The valid range is 1-100. | | `after` | Number | Used for paging between results in the response. Consult the provided offset in the previous page of response data to determine the next index of results to return. | ## List association endpoints You can use the endpoints described in the sections below to manage associations between lists and your marketing events. Many of these endpoints require a `listId` as a path parameter, which you can find on the list details page in your HubSpot account: - In your HubSpot account, navigate to **CRM** \> **Lists**. - Click the **name** of a list. - In the top right, click **Details**. - In the right panel, the list ID will appear under _List IDs for API integrations_. You can click **Copy list ID** to copy the ID to the clipboard. As you associate lists with your marketing events, they'll appear on the details page for a marketing event in your HubSpot account: - In your HubSpot account, navigate to **CRM** \> **Contacts**. - In the upper left, click **Contacts** and in the dropdown menu, select **Marketing events**. - Click the **name** of a marketing event. - On the _Performance_ tab, click **Lists** to expand the section, then click the **Lists added through associations** tab. ### Create list association with a marketing event ID To create a new association between a marketing event and an existing list, make a `PUT` request to `/marketing/v3/marketing-events/associations/{marketingEventId}/lists/{listId}`. If successful, you'll receive a `204 No content` response. ### Create list association with external event and account IDs To create a new association between a marketing event and an existing list using the external account ID and the external event ID, make a `PUT` request to `/marketing/v3/marketing-events/associations/{externalAccountId}/{externalEventId}/lists/{listId}`. If successful, you'll receive a `204 No content` response. ### Get lists associated with a marketing event using a marketing event ID To get all lists associated with a marketing event, make a `GET` request to `/marketing/v3/marketing-events/associations/{marketingEventId}/lists`. The response will resemble the following: ```json // Example response for GET request for all associated lists for a marketing event { "total": 1, "results": [ { "listId": "string", "listVersion": 0, "createdAt": "2024-05-10T08:58:35.769Z", "updatedAt": "2024-05-10T08:58:35.769Z", "filtersUpdatedAt": "2024-05-10T08:58:35.769Z", "processingStatus": "string", "createdById": "string", "updatedById": "string", "processingType": "string", "objectTypeId": "string", "name": "string", "size": 0 } ] } ``` ### Get lists associated with a event using external event and account IDs You can also get lists associated with a marketing event using an external account ID and the external event ID, make a `GET` request to `/marketing/v3/marketing-events/associations/{externalAccountId}/{externalEventId}/lists`. ### Delete list association using a marketing event ID To delete a list association for a marketing event using a marketing event ID, make a `DELETE` request to `/marketing/v3/marketing-events/associations/{marketingEventId}/lists/{listId}`. If successful, you'll receive a `204 No content` response. ### Delete list association using external event and account IDs To delete a list association for a marketing event using the external account ID and an external event ID, make a `DELETE` request to `/marketing/v3/marketing-events/associations/{externalAccountId}/{externalEventId}/lists/{listId}`. If successful, you'll receive a `204 No content` response. ## Configure app settings There's some setup required to allow marketing events to sync properly with HubSpot. If you send HubSpot an attendance state change (e.g., a registration or cancellation), HubSpot will first check to see if a Marketing Event exists for the specified event ID. If it doesn't, HubSpot will call the configured endpoint for your app to retrieve the details of the marketing event, then create the event in HubSpot and then publish the attendance state change. This is provided for convenience; however, it's still recommended that you create the Marketing Events yourself via the CRUD methods provided in the [reference documentation](/reference/api/marketing/marketing-events), and don't rely on this functionality to create your marketing events in HubSpot. ### Step 1: Create an API in your app In order to support this, HubSpot requires each app that uses Marketing Events to define an API to fetch information about a specific marketing event. Requirements: - Accepts: - `externalAccountId`: a query parameter that specifies the accountId of the customer in the external app. - `appId`: a query parameter that specifies the ID of the HubSpot application that is requesting the event details. This will be the ID of your app. - `externalEventId`: a path parameter in the URL of the request that specifies the ID of the event in the external app that HubSpot requires details about. - Returns: - A JSON object that provides details for the marketing event, that includes the following fields detailed in the table below: | Field Name | Required | Type | Field Description | | --- | --- | --- | --- | | `eventName` | true | string | The name of the marketing event | | `eventOrganizer` | true | string | The name of the organizer of the marketing event. | | `eventType` | false | string | Describes what type of event this is. For example: `WEBINAR`, `CONFERENCE`, `WORKSHOP` | | `startDateTime` | false | string($date-time) | The start date and time of the marketing event. | | `endDateTime` | false | string($date-time) | The end date and time of the marketing event. | | `eventDescription` | false | string | The description of the marketing event. | | `eventUrl` | false | string | A URL in the external event application where the marketing event. | | `eventCancelled` | false | bool | Indicates if the marketing event has been cancelled. Defaults to `false` | HubSpot will also send a `X-HubSpot-Signature-v3` header that you can use to verify that the request came from HubSpot. Read more about [request signatures](/guides/apps/authentication/validating-requests) for additional details on the signature and how to validate it. ### Step 2: Provide HubSpot with the URL path to your API Now that you've created the API in your app that will return an object that provides the details of a specific marketing event, you will need to provide HubSpot with the URL path to your API by making a `POST` request to `/marketing/v3/marketing-events/{appId}/settings`. This will allow HubSpot to determine how to make requests to your app to get the details of a marketing event. In the body of your `POST` request, specify your URL using the `eventDetailsURL` field. The `eventDetailsURL` must adhere to the following two requirements: - Contain a `%s` character sequence, which HubSpot will use to substitute in the ID of the event (`externalEventId`) as a path parameter. - It must be the full path to the API resource, including the `https://` prefix and the domain name (e.g., `my.event.app`). For example, if you configure an `eventDetailsURL` of `https://my.event.app/events/%s`, and you need to make a request to fetch details of an event with id `1234-event-XYZ`, for the HubSpot app with id `app-101` and account with id `ABC-account-789`, HubSpot will make a `GET` request to: `https://my.event.app/events/1234-event-XYZ?appId=app-101&externalAccountId=ABC-account-789` # Subscriptions preferences v3 @ The subscription preferences endpoints allow you to manage email subscriptions details for contacts in a HubSpot portal $ guides & api/marketing --- # Subscription preferences Subscriptions types represent the legal basis to communicate with your contacts through email. Contacts can [manage their email preferences](https://knowledge.hubspot.com/marketing-email/how-to-send-contacts-an-email-with-an-opt-out-link-so-they-can-manage-their-preferences) so that they're only opted into the emails they want to receive. The subscription preferences API does <u>not</u> currently support retrieving or updating [WhatsApp](https://knowledge.hubspot.com/inbox/collect-consent-for-whatsapp-conversations) subscription information for a contact. ## Get contact subscription status The contact subscription status endpoint allows users to retrieve the subscription statuses for an email address in an account. This endpoint is ideal for when you have an external preferences center or integration and need to know the subscription statuses for any given email address in your account. --- ## Subscribe contact The subscribe contact endpoint allows you to subscribe an email address to any given subscription type in an account, but **will not allow you to resubscribe contacts who have opted out.** The new version of the subscription preferences API, v4, does provide support for resubscribing contacts who previously opted out. Learn more about the v4 API in [this article](/guides/api/marketing/subscriptions). **Example use case:** This endpoint is ideal for when you have an integration or external form that needs to opt contacts into a subscription type. **Note**: The subscribe contact endpoint should only be used to honor requests from contacts who have given you permission to subscribe them. Please [review applicable laws and regulations](https://knowledge.hubspot.com/marketing-email/set-up-email-subscription-types) before subscribing a contact. --- ## Unsubscribe contact The unsubscribe contact endpoint allows allows you to unsubscribe an email address in an account from any given subscription type. **Example use case:** This endpoint is ideal for when you have an integration or external form that needs to opt contacts out of a subscription type. --- ## Get subscription types The subscription info endpoint allows users to retrieve all subscription types in their account. **Example use case:** This endpoint is ideal for when you have an external preferences center, integration, or form and need to know which subscription types exist in your account so you can update the subscription statuses for your contacts accordingly. --- # Subscription preferences v4 @ The v4 subscription preferences endpoints allow you to manage email subscriptions details for contacts in your account. $ guides & api/marketing --- # Subscriptions v4 API Subscription types represent the lawful basis to communicate with your contacts through email. Contacts can [manage their email preferences](https://knowledge.hubspot.com/marketing-email/how-to-send-contacts-an-email-with-an-opt-out-link-so-they-can-manage-their-preferences) so they're only opted in to emails they want to receive. The v4 subscriptions APIs allow you to programmatically subscribe or unsubscribe contacts from your email subscription types, or unsubscribe a contact from all email communication. These APIs also provide support for the [Brands add-on](https://knowledge.hubspot.com/branding/associate-your-assets-with-brands). Some of the endpoints below provide support for a `businessUnitId` field or query parameter, which you can use if you purchased the [Brands add-on](https://knowledge.hubspot.com/branding/manage-your-brands-with-hubspot-brands). Be aware that business units is the former name for [brands](https://knowledge.hubspot.com/branding/manage-your-brands-with-hubspot-brands), but this change does <u>not</u> impact the API. You should continue using any previous reference to `businessUnitId`. You can get a list of brands in your account using the [brands API](/guides/api/settings/business-units-api). ## Scope requirements The following scopes are required to use the v4 subscription API endpoints, based on the endpoint you're using: - `communication_preferences.read`: provides access to fetch subscription type definitions and subscription preferences for a contact. - `communication_preferences.write`: provides access to update subscription preferences for a contact. - `communication_preferences.read_write`: provides access to both fetch and update subscription preferences for a contact and fetch all subscription definitions in your account. The scopes below are required to use the batch subscription endpoints. You must have an _**Marketing Hub** Enterprise_ subscription to authorize these scopes for your app. - `communication_preferences.statuses.batch.read`: provides access to fetch subscription statuses in bulk. - `communication_preferences.statuses.batch.write`: provides access to manage and update subscription statuses in bulk. For a full list of available endpoints and example requests and responses, check out the [reference documentation](https://developers.hubspot.com/docs/reference/api/marketing/subscriptions). ## Get all subscription types To get a list of all email subscription types in your account, make a `GET` request to `/communication-preferences/v4/definitions`. If you have the [Brands add-on](/guides/api/settings/business-units-api), you can filter subscription types by brand by including the `businessUnitId` as a query parameter in your request. The default _Account_ brand will always use `"businessUnitId": 0`. The subscription types will be returned within the `results` field of the response. ```json // Example response for GET request to /communication-preferences/v4/definitions { "status": "COMPLETE", "results": [ { "businessUnitId": 41857, "id": "33583163", "name": "Marketing Information", "description": "Marketing offers and updates.", "purpose": "Marketing", "communicationMethod": "Email", "isActive": true, "isDefault": true, "isInternal": false, "createdAt": "2022-02-09T21:06:59.247Z", "updatedAt": "2022-02-09T21:06:59.247Z" }, { "businessUnitId": 0, "id": "39644612", "name": "New recipe newsletter", "description": "Subscription for new recipes and kitchen updates", "purpose": "Marketing", "communicationMethod": "Email", "isActive": true, "isDefault": false, "isInternal": false, "createdAt": "2022-04-14T20:37:03.073Z", "updatedAt": "2022-04-14T20:37:03.073Z" } ] } ``` You can optionally include the `includeTranslations=true` query parameter in your request to retrieve any subscription translations associated with each definition. For example, if you made a `GET` request to `/communication-preferences/v4/definitions?includeTranslations=true`, the response would resemble the following: ```json // Example response for GET request to /communication-preferences/v4/definitions with subscription translations included { "status": "COMPLETE", "results": [ { "subscriptionTranslations": [ { "subscriptionId": 88249125, "languageCode": "ar", "name": "test", "description": "test", "updatedAt": 1724702359758, "createdAt": 0 } ], "businessUnitId": 0, "id": "88249125", "name": "test", "description": "test", "purpose": "", "communicationMethod": "", "isActive": false, "isDefault": false, "isInternal": false, "createdAt": "2022-12-22T21:06:03.522Z", "updatedAt": "2024-08-26T19:59:39.926Z" } ], "startedAt": "2024-08-30T20:17:36.744Z", "completedAt": "2024-08-30T20:17:36.753Z" } ``` ## Get subscription preferences for a specific contact To get the current subscription preferences for a specific contact, make a `GET` request to `/communication-preferences/v4/statuses/{subscriberIdString}?channel=EMAIL` where the `subscriberIdString` is the email address of the contact. For example, to get the subscription preferences for a contact with an email address of `jdoe@example.com`, you'd make a `GET` request to `/preferences/v4/statuses/jdoe@example.com?channel=EMAIL`. The response will include a full list of the current subscription preferences for the contact in the `results` field. An example response is included below: ```json // Example response for GET request to /communication-preferences/v4/statuses/jdoe@example.com?channel=EMAIL { "status": "SUCCESS", "results": [ { "businessUnitId": 41857, "channel": "EMAIL", "subscriberIdString": "jdoe@example.com", "subscriptionId": 33583163, "status": "NOT_SPECIFIED", "source": "Not specified", "legalBasis": null, "legalBasisExplanation": null, "setStatusSuccessReason": null, "timestamp": "2024-06-05T13:39:29.495Z" }, { "businessUnitId": 0, "channel": "EMAIL", "subscriberIdString": "jdoe@example.com", "subscriptionId": 39644612, "status": "SUBSCRIBED", "source": "Self Service Resubscription", "legalBasis": "CONSENT_WITH_NOTICE", "legalBasisExplanation": "Contact provided explicit consent via form.", "setStatusSuccessReason": null, "timestamp": "2023-02-09T20:13:19.046Z" } ] } ``` Based on whether the contact explicitly opted in or opted out to a given a subscription, they can have the following `status` for a subscription type: - `SUBSCRIBED`: contact opted into the subscription type. - `UNSUBSCRIBED`: contact opted out of the subscription type. - `NOT_SPECIFIED`: contact hasn't provided opt-in preference for the subscription type. Learn more about [opt-in consent for email](https://knowledge.hubspot.com/marketing-email/understand-opt-in-consent-for-email). ## Get contacts who unsubscribed from all email communications Contacts can also opt out of all email communications from your business. To get a list of all contacts who are currently opted out of all email subscription types, make a `POST` request to `/communication-preferences/v4/statuses/batch/unsubscribe-all/read`. If you have the [Brands add-on](https://knowledge.hubspot.com/branding/manage-your-brands-with-hubspot-brands), you'll see the `wideStatusType: "BUSINESS_UNIT_WIDE"` field in the response. Note that the default _Account_ brand will always use `"businessUnitId": 0`. ## Get a specific contact who unsubscribed from all email communications To check whether a specific contact is unsubscribed from all email subscription types, make a `GET` request to `/communication-preferences/v4/statuses/{subscriberIdString}/unsubscribe-all`, where the subscriberIdString is the email address of the contact. If you have the [Brands add-on](https://knowledge.hubspot.com/branding/manage-your-brands-with-hubspot-brands), you'll see the `wideStatusType: "BUSINESS_UNIT_WIDE"` field in the response. Note that the default _Account_ brand will always use `"businessUnitId": 0`. For example, to check whether a contact with an email address of `jdoe@example.com` has opted out of all email communications, you'd make a `GET` request to `/communication-preferences/v4/statuses/jdoe@example.com/unsubscribe-all`. ## Update subscription preferences for a specific contact To update the subscription preferences for a contact, make a `POST` request to `/communication-preferences/v4/statuses/{subscriberIdString}`, where the `subscriberIdString` is the email address of the contact. In the request body, you'll need to include the fields listed the table below: | Parameter | Type | Description | | --- | --- | --- | | `subscriptionId` | Number | The internal ID of the subscription type. You can get a full list of subscription IDs by making a `GET` request to `/communication-preferences/v4/statuses/{subscriberIdString}`. | | `statusState` | string | The opt-in or opt-out state that you want to update the contact's subscription to. The possible values are `SUBSCRIBED`, `UNSUBSCRIBED`, or `NOT_SPECIFIED`. | | `legalBasis` | string | The legal reason for changing the subscriber's status. If you data privacy settings turned on, this field is required, along with the `legalBasisExplanation` field. | | `legalBasisExplanation` | string | An explanation for the legal basis you provided for updating the subscriber status. | | `channel` | string | The channel type for the subscription type. Currently, the only supported channel type is `EMAIL`. | For example, the request body below would subscribe a contact into the subscription associated with the internal ID of `39644612`. You can fetch a list of all subscription types available to get their IDs by making a `GET` request to `/communication-preferences/v4/definitions`. ```json // Example request body for POST request to /communication-preferences/v4/statuses/jdoe@exampl.ecom { "subscriptionId": 39644612, "statusState": "SUBSCRIBED", "legalBasis": "LEGITIMATE_INTEREST_OTHER", "legalBasisExplanation": "Contact mentioned that they mistakenly unsubscribed and they'd like to opt back into our newsletter.", "channel": "EMAIL" } ``` ## Update a contact's "Opted out of all email" status To unsubscribe a contact from all email communication in an account or specific business unit (i.e., "Opted out of all"), make a `POST` request to `/communications-preferences/v4/statuses/{subscriberIdString}/unsubscribe-all`, where the `subscriberIdString` is the email address of the contact. - If you have the [Brands add-on](https://knowledge.hubspot.com/branding/manage-your-brands-with-hubspot-brands), you'll also need to include the `businessUnitId` query parameter in your request. Note that the _Account_ brand will always use `"businessUnitId": 0`. - You can optionally include the `verbose` query parameter to include the details of the updated subscription statuses the contact has unsubscribed from in the response. If you don't use the `verbose` query parameter, the resulting response will be empty. Following a successful `POST` request, the contact will be unsubscribed from all email communication from your account. If you have the Brands add-on, the contact will be unsubscribed from all email from the brand specified in your request, but will still be eligible to receive email from other brands in your account. ## Using batch subscription endpoints If you have an **_Marketing Hub_** _Enterprise_ account, you can use the bulk subscription endpoints detailed below to fetch and manage subscription statuses for multiple contacts in a single API request. ### Get "Opted out of all communication" subscription status for a list of contacts To get a list of the _Opted out of all communication_ statuses for multiple contacts across an account or for a specific business unit, you can make a `POST` request to `/communication-preferences/v4/statuses/batch/unsubscribe-all/read`, and provide the following query parameters: - `businessUnitId`: if you have the [Brands add-on](https://knowledge.hubspot.com/branding/manage-your-brands-with-hubspot-brands), you can include this parameter to specify which business unit your contacts will be opted out of all subscription types from. If you don't provide this query parameter in the URL of your request, then all statuses for the account will be returned across all brands. - `channel`: the communication type to unsubscribe all contacts out of. Currently, the only the supported channel is `EMAIL`. In the body of your request, provide a list of the email addresses for the associated contacts you want to retrieve using the `inputs` field: ```json // Example request body for unsubscribing multiple contacts from all subscriptions in an account or business unit { "inputs": ["test1@hubspot.com"] } ``` For example, if you made a `POST` request to `/communication-preferences/v4/statuses/batch/unsubscribe-all/read?channel=EMAIL`, the resulting response would resemble the following: ```json // Example response body for POST request to /communication-preferences/v4/statuses/batch/unsubscribe-all/read { "status": "COMPLETE", "results": [ { "subscriberIdString": "test1@husbpot.com", "wideStatuses": [ { "businessUnitId": 0, "wideStatusType": "PORTAL_WIDE", "subscriberIdString": "string", "status": "SUBSCRIBED", "channel": "EMAIL", "timestamp": "2024-08-02T21:37:58.597Z" } ] }, { "subscriberIdString": "test2@hubspot.com", "wideStatuses": [ { "businessUnitId": 0, "wideStatusType": "PORTAL_WIDE", "subscriberIdString": "string", "status": "SUBSCRIBED", "channel": "EMAIL", "timestamp": "2024-05-22T12:151:01.145Z" } ] } ], "startedAt": "2024-08-02T19:25:35.063Z", "completedAt": "2024-08-02T19:25:35.114Z" } ``` ### Get specific subscription statuses for multiple contacts To get the subscription statuses of multiple contacts in an account or for a specific business unit, make a `POST` request to `/communication-preferences/v4/statuses/batch/read`. If you have the Brands add-on, you can include the `businessUnitId` query parameter to specify which brand your contacts will be opted out of all subscription types from. In the body of your request, provide a list of the email addresses for the associated contacts you want to opted out of all email communications using the `inputs` field: ```json // Example request body for unsubscribing multiple contacts from all subscriptions in an account or business unit { "inputs": ["test1@hubspot.com"] } ``` For example, if you made a `POST` request to `/communication-preferences/v4/statuses/batch/read?channel=EMAIL`, the resulting response would resemble the following: ```json // Example response body for POST request to /communication-preferences/v4/statuses/batch/read { "status": "COMPLETE", "results": [ { "subscriberIdString": "test@husbpot.com", "statuses": [ { "businessUnitId": 0, "channel": "EMAIL", "subscriberIdString": "test@husbpot.com", "subscriptionId": 88221657, "status": "UNSUBSCRIBED", "source": "Public status API", "legalBasis": null, "legalBasisExplanation": null, "setStatusSuccessReason": null, "timestamp": "2024-08-02T19:28:39.390Z" } ] } ], "startedAt": "2024-08-02T21:50:28.203Z", "completedAt": "2024-08-02T21:50:28.245Z" } ``` ### Update the "Opted out of all email" status for multiple contacts To unsubscribe multiple contacts from all subscription types in an account or for a specific business unit, make a `POST` request to `/communication-preferences/v4/statuses/batch/unsubscribe-all`, and provide the following query parameters in your request: - `businessUnitId`: if you have the [Brands add-on](https://knowledge.hubspot.com/branding/manage-your-brands-with-hubspot-brands), you can include this parameter to specify which brand your contacts will be opted out of all subscription types from. - `channel`: the communication type to unsubscribe all contacts out of. Currently, the only the supported channel is `EMAIL`. - `verbose`: an optional boolean value that controls if the endpoint returns all the subscriptions that were impacted for all contacts. In the body of your request, provide a list of the email addresses for the associated contacts you want to opted out of all email communications using the `inputs` field: ```json // Example request body for unsubscribing multiple contacts from all subscriptions in an account or business unit { "inputs": ["test1@hubspot.com", "test2@hubspot.com"] } ``` For example, if you made a `POST` request to `/communication-preferences/v4/statuses/batch/unsubscribe-all?channel=EMAIL&verbose=true`, the resulting response would resemble the following: ```json // Example response for POST request to /communication-preferences/v4/statuses/batch/unsubscribe-all?channel=EMAIL&verbose=true { "status": "COMPLETE", "results": [ { "subscriberIdString": "test1@husbpot.com", "statuses": [ { "businessUnitId": 0, "channel": "EMAIL", "subscriberIdString": "test@husbpot.com", "subscriptionId": 87914424, "status": "UNSUBSCRIBED", "source": "Public status API", "legalBasis": null, "legalBasisExplanation": null, "setStatusSuccessReason": "UNSUBSCRIBE_FROM_ALL_OCCURRED", "timestamp": "2024-08-02T19:28:39.390Z" } ] }, { "subscriberIdString": "test2@husbpot.com", "statuses": [ { "businessUnitId": 0, "channel": "EMAIL", "subscriberIdString": "test2@husbpot.com", "subscriptionId": 87914424, "status": "UNSUBSCRIBED", "source": "Public status API", "legalBasis": null, "legalBasisExplanation": null, "setStatusSuccessReason": "UNSUBSCRIBE_FROM_ALL_OCCURRED", "timestamp": "2024-08-02T19:28:39.390Z" } ] } ], "startedAt": "2024-08-02T19:25:35.063Z", "completedAt": "2024-08-02T19:25:35.114Z" } ``` ### Unsubscribe multiple contacts from specific subscription types To update the specific subscription types of multiple contacts in an account or for a specific business unit, make a `POST` request to `/communication-preferences/v4/statuses/batch/write`, and provide the details of the subscription updates in the inputs field in the body of your request. For example, the following request body would subscribe the contact with an email address of `test@hubspot.com`[](mailto:test@hubspot.com) to the subscription with an ID of 123: ```json // Example request body for POST request to /communication-preferences/v4/statuses/batch/write { "inputs": [ { "subscriptionId": 123, "statusState": "SUBSCRIBED", "legalBasis": "LEGITIMATE_INTEREST_PQL", "legalBasisExplanation": "string", "channel": "EMAIL", "subscriberIdString": "test@hubspot.com" } ] } ``` For the example request body above, the resulting response would resemble the following: ```json // Example response for POST request to /communication-preferences/v4/statuses/batch/write { "status": "COMPLETE", "results": [ { "businessUnitId": 0, "channel": "EMAIL", "subscriberIdString": "test@husbpot.com", "subscriptionId": 63722844, "status": "UNSUBSCRIBED", "source": "Public status API", "legalBasis": null, "legalBasisExplanation": null, "setStatusSuccessReason": "RESUBSCRIBE_OCCURRED", "timestamp": "2024-08-02T21:46:29.110Z" } ], "startedAt": "2024-08-02T21:46:29.088Z", "completedAt": "2024-08-02T21:46:29.228Z" } ``` # How to use HubSpot's APIs @ The HubSpot API allows developers to build custom apps and integrations. Get started by reading our API overview and creating a developer account. $ guides & api --- # API developer guides & resources HubSpot's APIs, which you can find the [reference documentation here](/reference/api/overview), allow you to build a functional app or integration quickly and easily. Here's an overview of what you'll need to use them. ## Get started To get started building apps and using HubSpot's APIs, check out the following guides that will help you decide what to build and how to build it on HubSpot: - [What to build on HubSpot](/getting-started/what-to-build) - [Tools to help you build](/getting-started/tools-to-help-you-build) - [Account types](/getting-started/account-types) If you're building an integration, you'll likely need to start with building a private or public app, depending on your needs. If you want to offer your app on HubSpot's App Marketplace, you'll need to create a public app. Learn more about the [differences between app types](/guides/apps/overview). ## Authentication Most HubSpot API endpoints support both [OAuth](/guides/apps/authentication/working-with-oauth) and [private app access tokens](/guides/apps/private-apps/overview#make-api-calls-with-your-app-s-access-token), while some app configuration APIs require you to use a developer API key. You can find OAuth and private app access token details in an app's settings page in HubSpot: - **OAuth**: OAuth details can be found on the _Auth_ tab of an app's settings in HubSpot: - Log in to your developer account. - In the left sidebar, navigate to **Apps**. - Click the **name** of an app. - Click the **Auth** tab, then view your app's OAuth details. - **Private app access token:** private app access token details can be found in the app settings page in the account where the app was installed: - Log in to the account where the app was installed. - In the top bar, click the **settings** icon. - In the left sidebar menu, navigate to **Integrations** > **Private apps**. - Click the **name** of the app. - Click the **Auth** tab, then view your app's access token. - **Developer API key:** your developer API key can be found in your developer account: - Log in to your developer account. - In the left sidebar, navigate to **Apps**. - In the upper right, click **Get HubSpot API key**. As of November 30, 2022, HubSpot API Keys were deprecated and are no longer supported. Continued use of HubSpot API Keys is a security risk to your account and data. You should instead authenticate using a private app access token or OAuth. Learn more about [this change](https://developers.hubspot.com/changelog/upcoming-api-key-sunset) and how to [migrate an API key integration](/guides/apps/private-apps/migrate-an-api-key-integration-to-a-private-app) to use a private app instead. ## Usage and limits Learn about our [usage guidelines](/guides/apps/api-usage/usage-details), rate limits, and how to check your API call usage. ## App partners and the App Marketplace If you want to offer your app on HubSpot's App Marketplace, learn more about HubSpot's [app listing requirements](/guides/apps/marketplace/app-marketplace-listing-requirements). When your app is ready to submit for review, learn how to [create your app listing](/guides/apps/marketplace/listing-your-app). ## Learn on the Academy In addition to the documentation you'll find on this site, you can learn more about development on HubSpot through HubSpot Academy videos, courses, and certifications. For example, you may want to start with the [Integrating With HubSpot I: Foundations](https://academy.hubspot.com/courses/integrating-with-hubspot-foundations) course or the [Web App Development](https://academy.hubspot.com/courses/building-your-first-web-app) course. ## Additional resources - [Create a developer account](https://app.hubspot.com/signup-hubspot/developers) - [Set up a developer test account](/getting-started/account-types#developer-test-accounts) to install your app and test API calls - Stay up-to-date by subscribing to the [Changelog](https://developers.hubspot.com/changelog) - Join the conversation or ask questions in HubSpot’s [developer community forums](https://community.hubspot.com/t5/HubSpot-Developers/ct-p/developers) - Become a member of our developer [Slack community](/getting-started/slack/developer-slack) # Account Activity API @ Learn more about the account activity API endpoints. $ guides & api/settings --- # Account activity The account activity API allows you to retrieve login history and security activity related to your HubSpot account. To learn how to export this information from within the HubSpot account, check out [HubSpot's Knowledge Base](https://knowledge.hubspot.com/account-management/view-and-export-account-activity-history). ## Retrieve an audit log of user actions This endpoint is available for _Enterprise_ accounts only. To retrieve an audit log of all user actions, make a `GET` request to `/account-info/v3/activity/audit-logs`. You can include the following query parameters to filter the results: - `actingUserId`: retrieve user-specific logs by specifying a user ID. - `occurredAfter`: retrieve logs starting from a specific timestamp. - `occurredBefore`: retrieve logs up until a specific timestamp. The response will contain an object for each user action included in the audit log. This includes actions related to CRM object creation, property value updates, security activity, and more. Learn more about the data included in audit logs on [HubSpot's Knowledge Base](https://knowledge.hubspot.com/account-management/view-and-export-account-activity-history-in-a-centralized-audit-log#data-included-in-the-centralized-audit-log). ```json { "results": [ { "id": "jAzNiQgaEZUTlJISRtESldLGFNM", "category": "PIPELINE", "subCategory": "DEAL", "action": "UPDATE", "targetObjectId": "Sales Pipeline", "occurredAt": "2025-02-07T20:58:49.529Z", "actingUser": { "userId": 089274502, "userEmail": "mark.scout@lumonindustries.com" } }, { "id": "FQVHSQEHchIJDy0EB14CAhcEFC9PAAAMHEgSBjscGkQHOgoWFlkcXFZBWEZBFUNKUEEZ", "category": "PROPERTY_VALUE", "subCategory": "CONTACT", "action": "UPDATE", "targetObjectId": "97742230563", "occurredAt": "2025-02-07T20:57:49.534Z", "actingUser": { "userId": 089274502, "userEmail": "mark.scout@lumonindustries.com" } }, { "id": "IiI9cjoxLzduNVhVW0UeQAZXFkgVSwoGVFVCFUtYSlFATExEUQ1HGEBUURddEB8=", "category": "CRM_OBJECT", "subCategory": "CONTACT", "action": "CREATE", "targetObjectId": "97742230563", "occurredAt": "2025-02-07T20:57:49.534Z", "actingUser": { "userId": 089274502, "userEmail": "mark.scout@lumonindustries.com" } }, { "id": "IJD5yIDYwIDtjXlBQRlxGFVxUSw==", "category": "CRITICAL_ACTION", "subCategory": "PRIVATE_APP_ACCESS_TOKEN_VIEW", "action": "PERFORM", "targetObjectId": "916900", "occurredAt": "2025-02-07T20:54:39.895Z", "actingUser": { "userId": 089274502, "userEmail": "mark.scout@lumonindustries.com" } }, { "id": "Ij8+eTA9MV8fUxRQlNFHF5RSlJAXX0gMSk7fikwIERFGkBdWkFcRhlbV05Y", "category": "CONTENT", "subCategory": "BLOG", "action": "PUBLISH", "targetObjectId": "92061154330", "occurredAt": "2025-02-07T14:40:40.279Z", "actingUser": { "userId": 089274502, "userEmail": "mark.scout@lumonindustries.com" } } ] } ``` | Parameter | Type | Description | | --- | --- | --- | | `id` | String | The unique ID of the action event. | | `category` | String | The type of user action. | | `subCategory` | String | The subcategory of user action. | | `action` | String | The action performed. | | `targetObjectId` | String | The ID of the object that the user performed the action on. | | `occurredAt` | String | The timestamp of when the action was performed. | | `actingUser` | Object | An object containing the user's ID and email address. | ## Retrieve login activity To retrieve an audit log of all user logins, make a `GET` request to `/account-info/v3/activity/login`. You can retrieve user-specific login history by including the `userId` query parameter followed by the user ID. The response will include an object for each login attempt (successful and unsuccessful) made in the past 90 days, including login attempts to app.hubspot.com and the HubSpot mobile app. ```json { "results": [ { "id": "4862485159", "loginAt": "2025-02-07T14:12:13.544Z", "userId": 089274502, "email": "mark.scout@lumonindustries.com", "loginSucceeded": true, "ipAddress": "123.45.678.90", "location": "new jersey, united states", "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36", "countryCode": "us", "regionCode": "nj" }, { "id": "4861025503", "loginAt": "2025-02-06T19:39:06.355Z", "userId": 089274502, "email": "mark.scout@lumonindustries.com", "loginSucceeded": true, "ipAddress": "123.45.678.90", "location": "new jersey, united states", "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36", "countryCode": "us", "regionCode": "nj" }, { "id": "4825456493", "loginAt": "2025-02-04T14:08:50.356Z", "userId": 089274502, "email": "mark.scout@lumonindustries.com", "loginSucceeded": true, "ipAddress": "123.45.678.90", "location": "new jersey, united states", "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36", "countryCode": "us", "regionCode": "nj" } ] } ``` | Parameter | Type | Description | | --- | --- | --- | | `id` | String | The unique ID of the login event. | | `loginAt` | String | The timestamp of when login was attempted. | | `userId` | Integer | The ID of the user associated with the activity. | | `email` | String | The email address of the user associated with the activity. | | `loginSucceeded` | Boolean | Whether the login attempt was successful. | | `ipAddress` | String | The IP address used for the login attempt. | | `location` | String | The location where the login was attempted. | | `userAgent` | String | User agent information about the device used for login. | | `countryCode` | String | The country code of the location where the login was attempted. | | `regionCode` | String | The region code of the location where the login was attempted. | ## Retrieve security activity To retrieve a history of security-related user activity, make a `GET` request to `/account-info/v3/activity/security`. You can include the following query parameters to filter the results: - `userId`: retrieve user-specific history by specifying a user ID. - `fromTimestamp`: retrieve history starting from a specific datetime. - `toTimestamp`: retrieve history up until a specific datetime. The response will include an object for each activity, including CRM exports, adding and removing users, and installing integrations. Learn more about the data included in security activity history on [HubSpot's Knowledge Base](https://knowledge.hubspot.com/account-management/view-and-export-account-activity-history#security-activity-history). ```json { "results": [ { "id": "534928712", "createdAt": "2025-02-07T20:54:39.895Z", "userId": 089274502, "type": "PRIVATE_APP_ACCESS_TOKEN_VIEW", "actingUser": "mark.scout@lumonindustries.com", "objectId": "916900", "infoUrl": "app.hubspot.com/private-apps/123456/916900/auth", "location": "new jersey, united states", "ipAddress": "123.45.678.90", "countryCode": "us", "regionCode": "nj" }, { "id": "532666822", "createdAt": "2025-01-31T21:29:33.724Z", "userId": 089274502, "type": "INSTALL_INTEGRATION", "actingUser": "mark.scout@lumonindustries.com", "objectId": "7615143", "infoUrl": null, "location": "new jersey, united states", "ipAddress": "123.45.678.90", "countryCode": "us", "regionCode": "nj" }, { "id": "467517668", "createdAt": "2025-01-16T18:11:47.494Z", "userId": 089274502, "type": "EXPORT_DOWNLOAD", "actingUser": "mark.scout@lumonindustries.com", "objectId": "74847993", "infoUrl": "app.hubspot.com/sales-products-settings/123456/importexport", "location": "new jersey, united states", "ipAddress": "123.45.678.90", "countryCode": "us", "regionCode": "nj" } ] } ``` | Parameter | Type | Description | | --- | --- | --- | | `id` | String | The unique ID of the login event. | | `createdAt` | String | The timestamp of when activity occurred. | | `userId` | Integer | The ID of the user associated with the activity. | | `type` | String | The type of activity. | | `actingUser` | String | The email address of the user associated with the activity. | | `objectId` | String | The ID of the object that the activity was performed on. | | `infoUrl` | String | The URL of the page where the activity occurred. | | `location` | String | The location where the login was attempted. | | `ipAddress` | String | The IP address used for the login attempt. | | `userAgent` | String | User agent information about the device used for login. | | `countryCode` | String | The country code of the location where the login was attempted. | | `regionCode` | String | The region code of the location where the login was attempted. | # Account Information API @ HubSpot's account information API endpoints provide information about a given HubSpot account. $ guides & api/settings --- # Account information The following endpoints provide information about a given HubSpot account, including the account settings, and the daily API usage and limits. ## Check daily API usage and limits for a private app Use this endpoint to see how many API calls a private app has made for the current day, and the API usage limits for that account. The current day is measured from midnight to midnight based on the connected account's time zone settings. Read more about HubSpot's [API usage guidelines](/guides/apps/api-usage/usage-details) and the API [request limits](https://developers.hubspot.com/apisbytier) for each HubSpot subscription type. ## Get account details for a HubSpot account Use this endpoint to find the following information for a given HubSpot account: - The account's [portal ID](https://knowledge.hubspot.com/account/manage-multiple-hubspot-accounts#check-your-current-account) (also called the Hub ID). - The [time zone set](https://knowledge.hubspot.com/account/change-your-language-and-region-settings) in the account. - The [company currency and additional currencies](https://knowledge.hubspot.com/settings/add-and-edit-your-account-currencies) added to the account. - The [data hosting location](https://knowledge.hubspot.com/account/hubspot-cloud-infrastructure-and-data-hosting-frequently-asked-questions) for the account. # Brands API @ HubSpot's brands API endpoint provides information about brands tied to a user. $ guides & api/settings --- # Brands (formerly business units) The following endpoint provides information about [brands](https://knowledge.hubspot.com/branding/manage-brands-with-business-unit) tied to a user. This may also include information about [logos](https://knowledge.hubspot.com/branding/edit-your-logo-favicon-and-brand-colors). The API described in this article references business units, which is the former name for [brands](https://knowledge.hubspot.com/branding/manage-brands-with-business-unit). This change does <u>not</u> impact this API. When making requests to the endpoint listed below, the URL path should still include `/business-units/v3/business-units/`, as the the endpoint path itself has not changed. This API currently only supports retrieving brand data and does <u>not</u> support associating assests with a brand, nor creating a new brand. ## Get brands tied to a user To get the brands that a user has access to, you can make a `GET` request to `/business-units/v3/business-units/user/{userId}` The following is an example of what the response body would include: ```json { "logoMetadata": { "logoAltText": "logo sample text", "resizedUrl": "sillystring", "logoUrl": "examplelogourl.com" }, "name": "sample business unit name", "id": "101" } ``` For more information on how to use the brands API, check out the [reference documentation](/reference/api/settings/business-units-api). # Settings API | Currencies @ Learn more about the currencies API endpoints to view and update your account's currencies and exchange rates. $ guides & api/settings --- # Currencies With the currencies API, you can manage the currencies used in your HubSpot account, including setting your account's company currency, creating additional currencies, and updating currency exchange rates. Learn more about [adding and editing currencies within HubSpot.](https://knowledge.hubspot.com/object-settings/add-and-edit-your-account-currencies) ## Supported currencies Only certain currencies are supported for use in HubSpot. To retrieve a list of HubSpot's supported currencies and their codes, make a `GET` request to `/settings/v3/currencies/codes`. You can use any of the returned codes as values for the `currencyCode`, `fromCurrencyCode`, and `toCurrencyCode` properties. ## Add account currencies and set exchange rates Depending on your HubSpot subscription, you can add additional currencies for use in your account, and set their exchange rates compared to your company currency. Learn more about the number of currencies you can have in your account in the [HubSpot Product & Services Catalog.](https://legal.hubspot.com/hubspot-product-and-services-catalog) To add a currency and set the exchange rate, or add a new exchange rate for an existing currency, make a `POST` request to `/settings/v3/currencies/exchange-rates`. In your request, the following fields are required: - `fromCurrencyCode`: the currency code of the currency you want to add to your account. This must be one of HubSpot's supported currency codes. - `conversionRate`: the exchange rate from the additional currency to your company currency. The value must be greater than 0 and can contain up to 6 decimal values (e.g., `1.36`). Any values with more than 6 decimal places will be rounded (e.g., `1.2345678` becomes `1.234568`). You can also include a timestamp using the following field: - `effectiveAt`: the date and time the exchange rate takes effect. Values can be in either [long date](https://www.jarte.com/help_new/date_and_time_formats.html) or [ISO 8601](https://developers.hubspot.com/docs#timestamps) format. For example, your request may look like: ```json { "fromCurrencyCode": "EUR", "conversionRate": "1.3", "effectiveAt": "2023-05-08T14:59:19.813Z" } ``` To add multiple currencies and set their exchange rates in bulk, make a `POST` request to `/settings/v3/currencies/exchange-rates​/batch​/create`. Your request must include the `fromCurrencyCode` and `conversionRate` properties for each currency. For example, your request may look like: ```json { "inputs": [ { "fromCurrencyCode": "USD", "conversionRate": 2.7 }, { "fromCurrencyCode": "EUR", "conversionRate": 1.3 } ] } ``` ## Retrieve account currencies and exchange rates You can view your account's current and historical currencies and exchange rates. To view your account's company currency, make a `GET` request to `/settings/v3/currencies/company-currency`. To view all of your account's current currencies and exchange rates, make a `GET` request to `/settings/v3/currencies/exchange-rates/current`. When you make a successful request, the response will include the IDs of the exchange rates set in your account. To view a specific currency and its exchange rate, make a `GET` request to `/settings/v3/currencies/exchange-rates/{exchangeRateId}`. To view all of your account's currencies and exchange rates, including historical currencies, make a `GET` request to `/settings/v3/currencies/exchange-rates`. You can filter which currencies and exchange rates are returned using the `limit`, `after`, `fromCurrencyCode` and `toCurrencyCode` query parameters in your request. ## Update your account's company currency Your account's [company currency](https://knowledge.hubspot.com/object-settings/add-and-edit-your-account-currencies#company-currency) is the currency used in deal amount totals, deal reports, and when creating new deals in HubSpot. To update your account's company currency, make a `PUT` request to `/settings/v3/currencies/company-currency`. In your request, include the `currencyCode` value for the currency you want to set as your company currency. For example, your request may look like: ```json { "currencyCode": "AED" } ``` For accounts with multiple currencies, the company currency will have an automatically generated self-referencing rate (e.g., EUR/EUR=1.0). This rate is <u>not</u> modifiable via API. ## Update currency exchange rates Once currencies are set up in your HubSpot account, you can update their exchange rates. To update an individual exchange rate, make a `PATCH` request to `/settings/v3/currencies/exchange-rates/{exchangeRateId}`. In your request, include the new exchange rate in the `conversionRate` property. Your request may look like: ```json { "conversionRate": 2 } ``` To update multiple currencies, make a `POST` request to `/settings/v3/currencies/exchange-rates/batch/update`. In your request, include the exchange rate IDs and the new `conversionRate` values. For example: ```json { "inputs": [ { "id": "string", "conversionRate": 1.3 }, { "id": "string", "conversionRate": 2 } ] } ``` ## Hide a currency You can hide a currency so that it's not visible in your HubSpot account. To hide an existing account currency, make a `POST` request to `/settings/v3/currencies/exchange-rates/update-visibility`. In your request, include the following: - `fromCurrencyCode`: the code of the currency you want to hide or show. - `toCurrencyCode`: the code of your company currency. - `visibleInUI`: whether or not the currency and its exchange rate are shown in your HubSpot account. To hide a currency, the value should be `false`. To show a previously hidden currency, set the value should be set to `true`. To hide a currency, your request may look like: ```json { "fromCurrencyCode": "USD", "toCurrencyCode": "AED", "visibleInUI": false } ``` ## Limits and scopes The daily limit for creating exchanges rates is 1,000. This is a total limit which includes rates created both individually and in batches. There are different scopes that a users needs in order to view or edit currency and exchange rate data: - `multi-currency-read`: required to retrieve and read currency data. - `multi-currency-write`: required to edit currency data. ## Manage currencies with automatic exchange rate updates (BETA) If you’ve opted into the _Automatic exchange rate updates_ [beta](https://knowledge.hubspot.com/account-management/opt-your-hubspot-account-into-a-public-beta-feature), you can turn on automatic updates for your currency exchange rates. Once this setting is turned on, you can use the currencies API to check if an account has automatic updates turned on, add new currencies with automatic exchange rate updates, and view currencies for which automatic exchange rate updates are not supported. Learn more about the _Automatic exchange rate updates_ beta on [HubSpot's Knowledge Base](https://knowledge.hubspot.com/account-management/automatic-exchange-rate-updates-beta). The following behavior and endpoints only apply if you’ve opted into the beta and [turned on the _Schedule automatic exchange rate updates_ setting.](https://knowledge.hubspot.com/account-management/automatic-exchange-rate-updates-beta#manage-automatic-exchange-rate-update-settings) ### Behavior changes for existing currency endpoints Once you’ve turned on the automatic exchange rate updates setting, the following behavior is expected: - **Changing company currency** (`PUT` `/company-currency`): If you change your company currency to an [unsupported currency](https://developers.hubspot.com/docs/guides/api/settings/currencies#retrieve-unsupported-currencies), the automatic exchange rate setting will be turned off. - **Fetching exchange rates** (`GET` `/exchange-rates`): When you retrieve all exchange rates, both manual and automatic updates to exchange rates will be returned. Changes to the company currency will not be returned. If you turn the automatic updates setting off after previously having it on, the exchange rate history will return manual updates to exchange rates and the synced rate effective at the time the setting was turned off. - **Manually creating or updating exchange rates** (`POST` `/exchange-rates`, `POST` `/exchange-rates/batch/create`, `PATCH` `/exchange-rates`, and `POST` `/exchange-rates/batch/update`): Once automatic updates are turned on, supported currencies can only be updated by the automatic updates. You'll only be able to manually add new currencies with exchange rates or update existing currency exchange rates for [unsupported currencies](https://developers.hubspot.com/docs/guides/api/settings/currencies#retrieve-unsupported-currencies). ### Check if your account has turned on automatic exchange rate updates To check if automatic exchange rate updates are enabled for your account, make a `GET` request to `settings/v3/currencies/central-fx-rates/information`. If the setting is turned on, the `centralExchangeRatesEnabled` field is returned with the value `true`. ```json { "centralExchangeRatesEnabled": true } ``` ### Add a currency with automatic exchange rate updates To add a currency with automatic updates enabled, make a `POST` request to `settings/v3/currencies/central-fx-rates/add-currency`. In your request, include the `currencyCode` for the new currency. You don’t need to include an exchange rate since the rate is automatically updated, powered by Open Exchange Rates, HubSpot’s third-party provider. For example, to add the US dollar to your account with automatic exchange rate updates: ```json { "currencyCode": "USD" } ``` ### Retrieve unsupported currencies To verify if a currency is supported for automatic exchange rate updates, make a `GET` request to `settings/v3/currencies/central-fx-rates/unsupported-currencies`. In the response, unsupported currencies are returned with their `currencyCode` and `currencyName` values. These currencies will always need manual [exchange rate updates.](https://developers.hubspot.com/docs/guides/api/settings/currencies#update-currency-exchange-rates) # Public permissions @ The public permissions API allows you to get permission data for user access to CRM records in a HubSpot account (BETA).  $ guides & api/settings/users --- # Public permissions You can use this API to check which users have access to specific CRM records in a HubSpot account. Response data will include a list of user IDs that correspond to the users in that account, along with the granular permissions that those users have been granted for the CRM record you provided (e.g., users who can edit or communicate with a given contact). ## HCRN format The public permissions API uses the HCRN format for querying and displaying permissions based on the HubSpot account, CRM record type, and CRM record ID you're requesting. This format always begins with `hcrn:`, followed by the associated IDs you're looking to query for: ```json hcrn:{accountId}:{resourceType}:{resourceTypeId}:{resourceId} ``` | ID | Description | | --- | --- | | `accountId` | The ID of the HubSpot account you want to query permission data from. | | `resourceType` | The category of resource you want to query permissions for. The only value currently supported is `crm-object`. | | `resourceTypeId` | The identifier for this resource type, e.g., an object type ID. You can find a full list of object type IDs [here](/guides/api/crm/understanding-the-crm#object-type-ids). | | `resourceId` | The ID of the specific instance of this resource (e.g., the ID of a specific contact). | You can only currently query for permission to access contacts, companies, deals, and tickets. Learn how to use the HCRN format to query for user permissions in the section below. ## Get user permissions for CRM records To get user permissions for CRM record data in a HubSpot account, make a `GET` request to `/resource-permissions/v3/permitted-users?resource={hrcn}`, where the `hcrn` matches the IDs and types you want to retrieve permission data for. For example, if your HubSpot account ID was `123456` and you wanted to get user permissions for access to a contact with an ID of `64151`, you'd make a `GET` request to `/resource-permissions/v3/permitted-users?resource=hcrn:2086383:crm-object:0-1` The results would resemble the following: ```json { "resources": { "hcrn:123456:crm-object:0-1:64151": { "crm-object:COMMUNICATE": { "action": "crm-object:COMMUNICATE", "permittedUsers": [2620022, 2805887, 2859646] }, "crm-object:EDIT": { "action": "crm-object:EDIT", "permittedUsers": [2620022, 2805887] }, "crm-object:VIEW": { "action": "crm-object:VIEW", "permittedUsers": [ 2620022, 2805887, 2859646, 4511561, 4833320, 5158531, 9586504 ] }, "crm-object:DELETE": { "action": "crm-object:DELETE", "permittedUsers": [2620022] } } } } ``` # Account | Users API @ A user object stores information such as a user's working hours, timezone, additional phone number, and job title. The user endpoints allow you to manage this data and sync it between HubSpot and other systems.  $ guides & api/settings/users --- Use this API to fetch information about users in the account, along with updating their working hours, timezone, additional phone number, and job title properties. This API can be especially useful for syncing HubSpot user data with external workforce management tools. For example, use these endpoints keep a user's working hours in sync with an external scheduling system. Learn more about objects, records, properties, and associations APIs in the [Understanding the CRM](/guides/api/crm/understanding-the-crm) guide. For more general information about objects and records in HubSpot, [learn how to manage your CRM database](https://knowledge.hubspot.com/get-started/manage-your-crm-database). ## Retrieve users Depending on the information you need, there are a few ways to retrieve HubSpot users: - To retrieve all users, make a `GET` request to `/crm/v3/objects/users/`. - To retrieve a specific user, make a `GET` request to the above URL and specify a user ID. For example: `crm/v3/objects/users/207838823235`. - To retrieve a batch of users by ID, make a `POST` request to `/crm/v3/objects/users/batch/read`. - To retrieve users that meet a specific set of criteria, you can make a `POST` request to `/crm/v3/objects/users/search` and include search filters in the request body. Learn more about [searching the CRM](/guides/api/crm/search). In a response for the users API, `id` and `hs_object_id` are the same and represent a user <u>only</u> in the HubSpot account from which the data was requested. This is different than the `id` values in the [user provisioning API](/guides/api/settings/users/user-provisioning) (`hs_internal_user_id`) which refers to a user across all accounts, and in the [owners API](/guides/api/crm/owners) (`hubspot_owner_id`) which refers to a user as an owner of records. For example, the following response returns users, their unique identifiers within the selected HubSpot account, and information about when they were created or modified: ```json // Example response { "results": [ { "id": "207838823235", "properties": { "hs_createdate": "2021-01-10T20:36:06.761Z", "hs_lastmodifieddate": "2023-08-29T18:17:55.697Z", "hs_object_id": "207838823235" }, "createdAt": "2021-01-10T20:36:06.761Z", "updatedAt": "2023-08-29T18:17:55.697Z", "archived": false }, { "id": "207840253600", "properties": { "hs_createdate": "2017-12-22T12:22:12.212Z", "hs_lastmodifieddate": "2023-08-29T18:17:55.697Z", "hs_object_id": "207840253600" }, "createdAt": "2017-12-22T12:22:12.212Z", "updatedAt": "2023-08-29T18:17:55.697Z", "archived": false } ] } ``` To return specific properties, include a `properties` query parameter in the request URL along with comma-separated property names. Learn more about [user properties below](#user-properties). For example, making a `GET` request to the following URL would result in the response below: `crm/v3/objects/users?properties=hs_job_title,hs_additional_phone` ```json // Example response { "results": [ { "id": "207838823235", "properties": { "hs_additional_phone": "+1123456780", "hs_createdate": "2021-01-10T20:36:06.761Z", "hs_job_title": "CEO", "hs_lastmodifieddate": "2023-08-29T18:17:55.697Z", "hs_object_id": "207838823235" }, "createdAt": "2021-01-10T20:36:06.761Z", "updatedAt": "2023-08-29T18:17:55.697Z", "archived": false }, { "id": "207840253600", "properties": { "hs_additional_phone": "+1238675309", "hs_createdate": "2021-01-10T20:36:06.761Z", "hs_job_title": "Vice President", "hs_lastmodifieddate": "2023-08-29T18:17:55.697Z", "hs_object_id": "207838823235" }, "createdAt": "2017-12-22T12:22:12.212Z", "updatedAt": "2023-08-29T18:17:55.697Z", "archived": false } ] } ``` For the batch read endpoint, you can either retrieve users by their ID or by another [unique identifier property](/guides/api/crm/properties#create-unique-identifier-properties) by including an `idProperty` field. For example, to read a batch of users, your request could look like either of the following: ```json ///Example request body with internal ID { "properties": ["hs_job_title", "hs_additional_phone"], "inputs": [ { "id": "207838823235" }, { "id": "207840253600" } ] } ``` ```json ///Example request body with a unique value property { "properties": ["hs_job_title", "hs_additional_phone"], "idProperty": "externalIdProperty", "inputs": [ { "id": "0001111" }, { "id": "0001112" } ] } ``` ## Update users You can update users by ID individually or in batches. - To update an individual user, make a `PATCH` request to `/crm/v3/objects/users/{userId}`. - To update a batch of users, make a `POST` request to `/crm/v3/objects/users/batch/update`, including the user IDs or unique `idProperty` in the request body as shown in the section above. For each endpoint, you'll need to include a request body that contains the properties you want to update. For example, the request body below would update a user's timezone and working hours: ```json ///Example request body { "properties": { "hs_standard_time_zone": "America/Detroit", "hs_working_hours": "[{\"days\":\"SATURDAY\",\"startMinute\":540,\"endMinute\":1020},{\"days\":\"WEDNESDAY\",\"startMinute\":540,\"endMinute\":1020}]" } } ``` Only some properties can be set through this API. See the properties section below for a list of the available propeties. ## User properties To retrieve a list of all available user properties, you can use the properties API by making a `GET` request to `crm/v3/properties/user`. Learn more about using the [properties API](/guides/api/crm/properties). Below are the user properties that can be set through this API. | Parameter | Type | Description | | --- | --- | --- | | `hs_additional_phone` | String | The user's additional phone number. Users can set this in their [user preferences](https://knowledge.hubspot.com/user-management/manage-user-properties-and-preferences#set-user-preferences). | | `hs_availability_status` | String | The user's availability status. The value must be either `"available"` or `"away"`. | | `hs_job_title` | String | The user's job title. Users can set this in their [user preferences](https://knowledge.hubspot.com/user-management/manage-user-properties-and-preferences#set-user-preferences). | | `hs_main_user_language_skill` | String | The user's main language skill. The value must match an existing language skill. Learn more about formatting language skills below. | | `hs_out_of_office_hours` | String | The user's out of office hours. Out of office hours must not overlap. Each out of office hours' start time must be later than the previous start time. | | `hs_secondary_user_language_skill` | String | The user's secondary language skill. The value must match an existing language skill. Learn more about formatting language skills below. | | `hs_standard_time_zone` | String | The user's timezone. Timezone values must use standard [TZ identifiers](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones), such as `"America/New_York"` or `"Europe/Dublin"`. This property must be set before you can set the user's working hours. | | `hs_uncategorized_skills` | String | The user's custom uncategorized skill. This property value must match an existing custom uncatgorized skill in the portal. | | `hs_working_hours` | String | The user's working hours. This property value is formatted as stringified JSON. Learn more about formatting for working hours below. | ### Working hours `hs_working_hours` accepts a stringified JSON value. It consists of an array with an object for each set of working hours. ```json "[{\"days\":\"VALUE\",\"startMinute\":number,\"endMinute\":number}]" ``` | Parameter | Type | Description | | --- | --- | --- | | `days` | Stringified JSON | The days included in a set of working hours. Values include: <ul><li>`MONDAY_TO_FRIDAY`</li><li>`SATURDAY_SUNDAY`</li><li>`EVERY_DAY`</li><li>`MONDAY`</li><li>`TUESDAY`</li><li>`WEDNESDAY`</li><li>`THURSDAY`</li><li>`FRIDAY`</li><li>`SATURDAY`</li><li>`SUNDAY`</li></ul> | | `startMinute` | Number | Working hours start time in minutes. Must be within the range of `0` - `1440`, where `0` represents 12:00AM midnight. For example, a 9:00AM start time would be represented as `540`. | | `endMinute` | Number | Working hours end time in minutes. Follows the same rules as `startMinute`.For example, 5:00PM is represented as `1020`. | - The `hs_standard_time_zone` property must be set before you can set working hours. - Working hours cannot overlap. For example, if a user works Monday through Friday, 9:00AM to 5:00PM, you would format that as follows: ```json "[{\"days\":\"MONDAY_TO_FRIDAY\",\"startMinute\":540,\"endMinute\":1020}]" ``` If a user works Monday 9:00AM to 5:00PM and Saturday 11:00AM to 2:00PM, the array would contain an object to represent each set of working hours: ```json "[{\"days\":\"MONDAY\",\"startMinute\":540,\"endMinute\":1020},{\"days\":\"SATURDAY\",\"startMinute\":660,\"endMinute\":840}]" ``` ### Out of office hours If a user will be unavailable due to scheduled time off, you can set any periods during which they'll be out of office using the `hs_out_of_office_hours` property: - The property accepts an array of date ranges, each specified by a `startTimestamp` and `endTimestamp`. - The date ranges cannot overlap with one another, and the `startTimestamp` of each date range must be later than the previous `startTimestamp`. For example, if you wanted to specify out-of-office hours during October 31st 2024 9:00 AM to 5:00 PM and November 28 2024 9:00 AM to 5:00 PM, you'd specify the following value for the `hs_out_of_office_hours` property for a user: ```json "[{\"startTimestamp\": 17303796000,\"endTimestamp\": 17304084000},{\"startTimestamp\": 17328024000,\"endTimestamp\": 17328312000}]" ``` ### Language skills `hs_main_user_language_skill` or `hs_secondary_user_language_skill` must match an existing language skill. The following JSON array lists all valid options for language skill categories: ```json // Full list of valid options for language skill categories: [ { "label": "Dansk", "value": "da" }, { "label": "Deutsch", "value": "de" }, { "label": "English", "value": "en" }, { "label": "Español", "value": "es" }, { "label": "Français", "value": "fr" }, { "label": "Italiano", "value": "it" }, { "label": "Nederlands", "value": "nl" }, { "label": "Norsk", "value": "no" }, { "label": "Polski", "value": "pl" }, { "label": "Português", "value": "ptbr" }, { "label": "Suomi", "value": "fi" }, { "label": "Svenska", "value": "sv" }, { "label": "中文 - 繁體", "value": "zhtw" }, { "label": "日本語", "value": "ja" } ] ``` # Settings API | User Provisioning @ The User Provisioning endpoints allow you to provision and deprovision HubSpot users programatically. $ guides & api/settings/users --- # User Provisioning Use the user provisioning API to create and manage users in the account, along with their permissions. You can also set user `firstName` and `lastName` properties through this API. To retrieve and update other user information, such as their job title and working hours, use the [users API](/guides/api/settings/users/user-details) instead. ## Specifying a user When specifying a user with the `userId` path parameter, you can either use the user's ID or the user's email. Specifying based on the user's ID is the default behavior but if you want to use the user's email, you can use the query parameter `idProperty` to set that. The following `GET` request is fetching a user with the email _myUser@gmail.com:\_ `https://api.hubspot.com/settings/v3/users/myUser@gmail.com?idProperty=EMAIL` You can set the `idProperty` query parameter in any endpoint that takes in `userId` as a path parameter. ## Permission Sets HubSpot accounts can define permission sets to easily manage multiple users' permissions at once. Once you've created a role and specified certain permissions for it, you can then assign new and existing users the role to grant them the same permissions. Permission sets that have paid seats attached to them can only be modified by applications that have the `billing-write` scope. The following is an example of a role definition for a user: ```json // Example specification of a role definition { "id": "1234", "name": "a new role", "requiresBillingWrite": false } ``` Note that permission sets must be [created in the app](https://knowledge.hubspot.com/settings/create-roles) before attempting to assign them to users. # Breaking Changes on the HubSpot Platform @ What changes qualify as breaking vs non-breaking. Use this to help guide your app development $ guides & apps/api-usage --- # Breaking changes on the HubSpot platform When developing an integration on the HubSpot platform, you'll need to be aware of the types of changes that HubSpot might make to its platform or data within. Below, learn about what HubSpot considers a breaking or non-breaking change, and use these lists to inform decisions you'll make when working with HubSpot's APIs. If a change is listed as a breaking change, we aim to provide you with 90 days notice if we do decide to make that change. This notice period applies to developer products released to general availability. For beta and developer preview products, we’ll continue to update those regularly as we learn more about them. In certain situations, such as those related to security or privacy, we may shorten that 90 day timeframe. As an example of correctly handling non-breaking changes, you can safely write (in pseudo code here) `if err.code === 500 then retry` but you should not write `if err.message === "Uh oh, something went wrong" then retry`. By asking yourself the question "if HubSpot makes a change listed as non-breaking, will this negatively impact the user experience?", you can write code that will be easier to maintain and create a more reliable app. ## REST APIs Below, learn about what changes are breaking for incoming and outgoing RESTful APIs (e.g., contacts API and webhooks). **Breaking changes** - Removing an endpoint - Removing a field from the response (a “field” in this case is a JSON key/value pair) - Removing an option from a field in the request - If a request field could previously accept two String values, and we remove the ability to accept one of those, that would be a breaking change - Adding a new required field, header, or parameter in the request - Renaming a field - Changing the data type of a field - Changing field lengths past industry standard data limits (int, long, etc.) - Changing error response code or category - Changing auth types - Restricting scope - Reduce API limits - Making validation more stringent - Changing the default sort order - Significantly increase the size of a response (10x) **Non-breaking changes** - Changing the error message - Adding a new option to a response (e.g., adding a new possible return value to a string field) - Adding a new optional input parameter - Property labels changing - Labels can be changed by HubSpot, application providers, and even customers. Integrations should not rely on property labels for their functionality. ## HubL Below, learn about what changes are considered breaking for HubL (e.g., the `hubdb_table_rows` function or `escape` filter). **Breaking changes** - Removing a filter, function, variable or tag - Adding a new required argument - Changing the order of an argument - Change the data type of a field in a module/theme field JSON - Reduce the number of times a function can be invoked on a page **Non-breaking changes** - Adding a new optional argument - Adding a new function, filter, variable or tag ## Embedded content Below, learn about what changes are considered breaking for embedded content (e.g., forms and CTAs). **Breaking changes** - Adding or removing class names - Changing the timing or execution of custom form events (e.g. `onFormSubmit`) - Changing the timing or structure of global form events **Non-breaking changes** - Add an optional setting for customizing the behavior of the embed ## JavaScript SDK Below, learn about what changes are considered breaking for JavaScript SDK (e.g., the calling SDK). **Breaking changes** - Removing an operation - Requiring a new argument to a function or field in an object that is passed to the function - Changing order of required parameters - Changing data type returned **Non-breaking changes** - Adding a new function ## Snowflake Data Share Occasionally, there will be changes to the schema that describes the data share. Any change that makes querying a share fail due to changing an existing schema object is considered a breaking change. For example, renaming a table is a breaking change for the [Snowflake Data Share integration](https://knowledge.hubspot.com/reports/query-hubspot-data-in-snowflake) because queries referencing the former name of the table will fail after the rename. In this case, HubSpot will add a new table with the same data to the share so that you can begin using the new name. The old table will continue to exist until the end of the 90 day notice period. Below, learn about what changes are considered breaking for the Snowflake Data Share. **Breaking changes** - Removing or renaming the database schema - Removing or renaming a table or view - Removing or renaming a column - Changing a column's datatype - Column nullability **Non-breaking changes** If a change is listed as a non-breaking change, it will be added to the schema with no notice given. For example, adding a new column to the schema. This means that queries accessing shares should be as specific as possible when referencing schema objects. For example, you should always qualify tables by specifying the database schema they belong to. It also means that you shouldn’t use the `SELECT *` clause in your queries, because that might break when a new column is added. The following are examples of non-breaking changes: - Adding to the database schema - Adding a table or view - Adding a column - Changes to objects and their properties (e.g., the data within the `objects` and `object_properties` tables and views) # HubSpot APIs | Usage guidelines @ HubSpot's API usage guidelines cover our Acceptable Use Policy, API Terms, rate limits, authentication, and security. $ guides & apps/api-usage --- # API usage guidelines HubSpot closely monitors usage of our public APIs to ensure a quality experience for every user. All app and integration developers must comply with the [HubSpot Acceptable Use Policy](https://legal.hubspot.com/acceptable-use) and [API Terms](https://legal.hubspot.com/hs-developer-terms). While HubSpot reserves the right to change or deprecate the APIs over time, updates will always be provided in advance through the [Developer Changelog](https://developers.hubspot.com/changelog). ## Authentication and security For optimal security, all apps must use HubSpot’s [OAuth protocol](/guides/apps/authentication/working-with-oauth) directly, or use your app's access token if you're building a [private app](/guides/apps/private-apps/overview). Apps are responsible for storing time-to-live (TTL) data and refreshing user access tokens in accordance with this protocol. When an access token is generated, it will include an `expires_in` parameter indicating how long it can be used to make API calls before refreshing. `Unauthorized (401)` requests are not a valid indicator that a new access token must be retrieved. ## Checking API usage ### Private apps **To view API usage for a private app:** - In your HubSpot account, click the **settings icon** in the main navigation bar. - In the left sidebar menu, navigate to **Integrations** \> **Private Apps**. - Click the **name** of the private app. - On the app details page, click the **Logs** tab. - Review the API calls listed in the table. You can also use the **search bar**, **filters**, and **date pickers** to further refine the displayed API calls. **Learn more about [checking API usage in private apps](/guides/apps/private-apps/overview#view-api-call-logs).** ### Public apps using OAuth To view API usage for a public app using OAuth: - In your developer account, navigate to **Apps** in the main navigation bar. - Click the **name** of the app. - In the left sidebar menu, navigate to **Monitoring**. - Use the **tabs** to view different types of requests being made to or from the app. While viewing these logs, you can click an **individual request** to view more information. . ## Rate Limits #### Public apps For OAuth apps, each HubSpot account that installs your app is limited to 110 requests every 10 seconds. This excludes the [Search API](/guides/api/crm/search), as noted in the [Other Limits](#other-limits) section below. Limits related to the [API limit increase](https://legal.hubspot.com/hubspot-product-and-services-catalog#Addons) don't apply. #### Private apps The number of calls your private app can make is based on your account subscription and whether you've purchased the [API limit increase](https://legal.hubspot.com/hubspot-product-and-services-catalog#Addons): | | **Product Tier** | **Per 10 Seconds** | **Per Day** | | --- | --- | --- | --- | | Private Apps | (Any Hub) <br/> Free and Starter | 100 / private app | 250,000 / account | | | (Any Hub) <br/> Professional | 190 / private app | 625,000 / account | | | (Any Hub) <br/> Enterprise | 190 / private app | 1,000,000 / account | | Private Apps with [API Limit Increase](https://legal.hubspot.com/hubspot-product-and-services-catalog#Addons) | (Any Hub) <br/> Free, Starter, Professional, and Enterprise | 250 / private app | 1,000,000 / account on top of your base subscription, for each limit increase. You can purchase a maximum of two API limit increases. | Purchasing an API Limit Increase will increase the maximum number of requests you can make to the [associations API](/guides/api/crm/associations/associations-v4) to 1,000,000 daily requests and 200 requests per 10 seconds, but these limits <u>cannot</u> be increased further with an additional API Limit Increase purchase. ## Other limits Some features and APIs have more specific limits that are listed below. ### App and account limits - You can create up to 100 [public apps](/guides/apps/public-apps/overview) per developer account. - You can create up to 20 [private apps](/guides/apps/private-apps/overview) per HubSpot account. - You can create up to 1,000 [webhook subscriptions](/guides/api/app-management/webhooks) per app. - You can create up to 25 CRM extension settings per [public app](/guides/apps/public-apps/overview). ### Timeline event limits [Timeline events](/guides/api/crm/extensions/timeline) are subject to the following limits: - You can create up to 750 timeline event types per app. - You can create up to 500 properties per timeline event type. - Each serialized event instance has the following size limits: - 500 bytes for the event instance ID - 510 KB per property/token - 1 MB in total size for the event instance ## Request limits Keep the following general limits in mind when making requests to HubSpot's API endpoints. Some APIs may also have their own, more specific limits which will be listed on the API reference page under _Limits_. - The [search API](/guides/api/crm/search) endpoints are rate limited to <u>five</u> requests per second per authentication token, and 200 records per page request. Learn more about [search limits](/guides/api/crm/search#limitations). - API requests that are exempt from daily or secondary limits will <u>not</u> be logged in HubSpot. If you want to store these exempted requests, you'll need to log these requests externally. - Batch requests to CRM object endpoints are limited to 100 records per request. For example, you can't batch read more than 100 contacts per request. ## Service Limits Learn more about service limits and pricing [here](https://legal.hubspot.com/hubspot-product-and-services-catalog). ## Error Responses Any app or integration exceeding its rate limits will receive a `429` error response for all subsequent API calls. Requests resulting in an error response shouldn’t exceed 5% of your total daily requests. If you plan on listing your app in the [HubSpot App Marketplace](https://ecosystem.hubspot.com/marketplace/apps), it must stay under this 5% limit to be certified. The `429` response will have the following format: ```json //Example { "status": "error", "message": "You have reached your daily limit.", "errorType": "RATE_LIMIT", "correlationId": "c033cdaa-2c40-4a64-ae48-b4cec88dad24", "policyName": "DAILY", "requestId": "3d3e35b7-0dae-4b9f-a6e3-9c230cbcf8dd" } ``` The `message` and `policyName` will indicate which limit you hit (either daily or secondly). The **daily** limit resets at midnight based on your [time zone setting](https://knowledge.hubspot.com/account-management/change-your-language-and-region-settings). The following table details the rate limit headers included in the response of each API request to HubSpot, subject to the exceptions listed below the table. | **Header** | **Description** | | --- | --- | | `X-HubSpot-RateLimit-Daily` | The number of API requests that are allowed per day. Note that this header is not included in the response to API requests authorized using [OAuth](/guides/apps/authentication/working-with-oauth). | | `X-HubSpot-RateLimit-Daily-Remaining` | The number of API requests still allowed for the current day. Note that this header is not included in the response to API requests authorized using [OAuth](/guides/apps/authentication/working-with-oauth). | | `X-HubSpot-RateLimit-Interval-Milliseconds` | The window of time that the `X-HubSpot-RateLimit-Max` and `X-HubSpot-RateLimit-Remaining` headers apply to.For example, a value of 10000 would be a window of 10 seconds. | | `X-HubSpot-RateLimit-Max` | The number of requests allowed in the window specified in `X-HubSpot-RateLimit-Interval-Milliseconds`.For example, if this header had a value of 100, and the `X-HubSpot-RateLimit-Interval-Milliseconds` header was 10000, the enforced limit would be 100 requests per 10 seconds. | | `X-HubSpot-RateLimit-Remaining` | The number of API requests still allowed for the window specified in `X-HubSpot-RateLimit-Interval-Milliseconds` | - The `X-HubSpot-RateLimit-Secondly` and `X-HubSpot-RateLimit-Secondly-Remaining` headers are still included and will still have accurate data, but the limit referenced by these headers is no longer enforced and these two headers should be considered deprecated. - Responses from the [search API endpoints](/guides/api/crm/search) will <u>not</u> include any of the rate limit headers listed above. You can also check the number of calls used during the current day using [this endpoint](/guides/api/settings/account-information-api). If you're running into the `TEN_SECONDLY_ROLLING` limit, you should throttle the requests that your app is making to stay under that limit. In addition to throttling the requests, or if you're running into the daily limit, check out the suggestions below. If you find that you're still hitting the call limits after looking through these suggestions, please post on HubSpot's [developer forums](https://integrate.hubspot.com/). You should include as many details as possible about the APIs you're using, how you're using them, and which limit you're hitting. ### Use batch APIs and cache results when possible If your site or app uses data from HubSpot on each page load, that data should be cached and loaded from that cache instead of being requested from the HubSpot APIs each time. If you're making repeated calls to get settings from your account for a batch job (such as getting your object properties, owners, or settings for a form), those settings should also be cached when possible. ### Use webhooks to get updated data from HubSpot If you have a HubSpot Marketing Enterprise subscription, you can use webhook actions in workflows to have data for contact records sent to your system. Webhooks can be triggered as an action in any workflow, so you can use any workflow [starting conditions](https://knowledge.hubspot.com/workflows/understand-contact-based-workflow-types) as the criteria to have contact data sent to your system. More details about using webhooks can be found [here](https://knowledge.hubspot.com/workflows/how-do-i-use-webhooks-with-hubspot-workflows) and example webhooks data is [here](/reference/api/automation/create-manage-workflows/v3#create-a-workflow). Webhook calls made via workflows do not count towards the API rate limit. # HubSpot APIs | Authentication methods on HubSpot @ Learn more about using OAuth or private access tokens to make authenticated API requests to HubSpot. $ guides & apps/authentication --- # Authentication methods on HubSpot There are two ways to authenticate calls to HubSpot's APIs: [OAuth](/guides/apps/authentication/working-with-oauth), and [private app](/guides/apps/private-apps/overview) access tokens. Below, learn more about each method and how to include it in your code for authorization. If you were previously using an API key to authenticate, learn how to [migrate to using a private app access token](/guides/apps/private-apps/migrate-an-api-key-integration-to-a-private-app) instead. Integrations designed for multi-customer use or listing on the App Marketplace must be built as an app using HubSpot’s OAuth protocol ## OAuth To make a request using [OAuth](/guides/api/app-management/oauth-tokens) when building a [public app](/guides/apps/public-apps/overview), include the OAuth access token in the authorization header: ```shell /~curl --header "Authorization: Bearer C4d***sVq" https://api.hubapi.com/crm/v3/objects/contacts?limit=10&archived=false ``` ## Private app access tokens Similar to OAuth, to make a request using a [private app](/guides/apps/private-apps/overview) access token, include the token in the authorization header: ```shell /~curl --header "Authorization: Bearer ***-***-*********-****-****-****-************" https://api.hubapi.com/crm/v3/objects/contacts?limit=10&archived=false ``` ## Automatic token deactivation To protect developers from potential security incidents, HubSpot leverages the monitoring and secret scanning capabilities provided by GitHub to detect any HubSpot authentication tokens that are publicly exposed in GitHub repositories. Any detected tokens will automatically be deactivated, and you will be notified via email and in-app notification so you can generate a new token and update your integrations to replace the revoked token. The affected key and token types are listed below: - Developer API keys created within your [app developer account](/getting-started/account-types#app-developer-accounts). - Personal Access Keys used to authenticate commands in the [HubSpot CLI](/guides/cms/tools/local-development-cli). - Private access tokens used to authenticate requests in a [private app](/guides/apps/private-apps/overview). - SMTP tokens used for sending [transactional emails](https://knowledge.hubspot.com/marketing-email/how-to-use-transactional-email-in-hubspot). --- #### Related docs [Working with OAuth](/guides/apps/authentication/working-with-oauth) [OAuth Quickstart Guide](/guides/apps/authentication/oauth-quickstart-guide) [Private Apps](/guides/apps/private-apps/overview) # Working with OAuth | OAuth Quickstart Guide @ Connect to HubSpot with OAuth using our Quickstart Guide and sample node.js app. $ guides & apps/authentication --- # OAuth Quickstart Guide ## Before you get started Before you can start using OAuth with HubSpot, you'll need to have: - [A developer account](https://app.hubspot.com/signup-hubspot/developers) - [An app](/guides/apps/public-apps/overview) associated with your developer account - A HubSpot account\* to install your app in (you can use an existing account or [create a test account](/getting-started/account-types)) **\*You must be a 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 2.0 tokens](#getting-oauth-2.0-tokens): How to authorize your app with users - [Using OAuth 2.0 tokens](#using-oauth-2.0-tokens): How to make queries with a token - [Refreshing OAuth 2.0 tokens](#refreshing-oauth-2.0-tokens): How to use the refresh token provided by HubSpot **Note:** 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, we strongly recommend checking 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 [Getting OAuth 2.0 tokens](/guides/api/app-management/oauth-tokens). - [Get the app on Github](https://github.com/HubSpot/oauth-quickstart-nodejs) ## Getting OAuth 2.0 tokens ### Step 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 below. For more detailed information on this step, read the [reference doc](/guides/apps/authentication/working-with-oauth). | Parameter | Required? | Description | Example | | --- | --- | --- | --- | | `client_id` | Yes | The client ID identifies your app. Find it on your app's settings page. | `7fff1e36-2d40-4ae1-bbb1-5266d59564fb` | | `scope` | Yes | The [scopes](/guides/apps/authentication/working-with-oauth#scopes) your application is requesting, separated by URL-encoded spaces. | `contacts%20social` | | `redirect_uri` | Yes | 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` | No | 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` | No | 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 2.0 process by sending the user to it. ##### Examples Using a server-side redirect: ```js // 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 <a href="https://app.hubspot.com/oauth/authorize?scope=contacts%20social&redirect_uri=https://www.example.com/auth-callback&client_id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx&state=xxxxxxxx" >Install</a > ``` ##### 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). ### Step 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. ### Step 3: Handle the OAuth 2.0 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 app.get('/oauth-callback', async (req, res) => { if (req.query.code) { // Handle the received code } }); ``` ### Step 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/v1/token` with the values shown below. For more detailed information on this step, take a minute to read this [reference doc](/guides/api/app-management/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 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/v1/token', { form: formData }, (err, data) => { // Handle the returned tokens } ``` The body of the token response will be JSON data with the form: ```json { "refresh_token": "6f18f21e-a743-4509-b7fd-1a5e632fffa1", "access_token": "CN2zlYnmLBICAQIYgZXFLyCWp1Yoy_9GMhkAgddk-zDc-H_rOad1X2s6Qv3fmG1spSY0Og0ACgJBAAADAIADAAABQhkAgddk-03q2qdkwdXbYWCoB9g3LA97OJ9I", "expires_in": 1800 } ``` **Note:** The access token will expire after the number of seconds given in the `expires_in` field of the response, currently 30 minutes. For information on getting a new access token, see Refreshing OAuth 2.0 tokens. ## Using OAuth 2.0 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 token as a bearer token in the `Authorization` HTTP header. Specific details can be found in the [reference doc](/guides/api/app-management/oauth-tokens). ##### Example: ```js request.get( 'https://api.hubapi.com/contacts/v1/lists/all/contacts/all?count=1', { headers: { Authorization: `Bearer ${ACCESS_TOKEN}`, 'Content-Type': 'application/json', }, }, (err, data) => { // Handle the API response } ); ``` 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 2.0 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/v1/token` with the values below. For more detailed information on this step, check out the [reference doc](/guides/api/app-management/oauth-tokens). | 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 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/v1/token', { form: formData }, (err, data) => { // Handle the returned tokens } ``` The body of the token response will be JSON data with the form: ```json // Sample response { "refresh_token": "6f18f21e-a743-4509-b7fd-1a5e632fffa1", "access_token": "CN2zlYnmLBICAQIYgZXFLyCWp1Yoy_9GMhkAgddk-zDc-H_rOad1X2s6Qv3fmG1spSY0Og0ACgJBAAADAIADAAABQhkAgddk-03q2qdkwdXbYWCoB9g3LA97OJ9I", "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 docs [Auth methods on HubSpot](/guides/apps/authentication/intro-to-auth) [Working with OAuth](/guides/apps/authentication/working-with-oauth) [Manage tokens](/guides/api/app-management/oauth-tokens) # Scopes @ Learn about the different scopes available for private and public apps in HubSpot. $ guides & apps/authentication --- # Scopes Scopes provide access to a specific set of HubSpot API endpoints and the associated data from a HubSpot account. If you created a private app, you can specify which scopes your app has access to in your [private app settings](/guides/apps/private-apps/overview#create-a-private-app). If you're developing a public app, you'll [configure both required and optional scopes](/guides/apps/public-apps/overview#configure-scopes) that users who install your app will be prompted to authorize via the app's install URL. ## Find required scopes for an endpoint Any scopes required to make a request to a specific endpoint will be listed under the _Requirements_ section in the reference documentation, which can be accessed by navigating to the endpoint reference link at the top of an API guide. Some scopes may list both _Standard_ and _Granular_ scopes. If both scope types are listed, you should opt for using the granular scopes when possible, as they specify more explicit access for your API requests. ## 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** | **Provides access to** | **Required account tier** | | --- | --- | --- | --- | | `cms.domains.read` | List [connected domains](/guides/api/cms/domains) in an account. | CMS API | Any account | | `cms.domains.write` | Create, update, and delete [connected domains](/guides/api/cms/domains). | CMS API | Any account | | `cms.functions.read` | View all [Content Hub serverless functions](/guides/cms/content/data-driven-content/serverless-functions/overview), any related secrets, and function execution results. | CMS API | _**Content Hub** Enterprise_ | | `cms.functions.write` | Grants access to write [Content Hub serverless functions](/guides/cms/content/data-driven-content/serverless-functions/overview) and secrets. | CMS API | _**Content Hub**_ _Enterprise_ | | `cms.knowledge_base.articles.read` | View details about knowledge articles. | CMS API | _**Service Hub**_ _Professional_ or _Enterprise_ | | `cms.knowledge_base.articles.write` | Grants access to update knowledge articles. | CMS API | _**Service Hub** Professional_ or _Enterprise_ | | `cms.knowledge_base.articles.publish` | Grants access to update and publish knowledge articles. | CMS API | _**Service Hub** Professional_ or _Enterprise_ | | `cms.knowledge_base.settings.read` | View general and template knowledge base settings, such as the domain or root URL. | CMS API | _**Service Hub** Professional_ or _Enterprise_ | | `cms.knowledge_base.settings.write` | Grants access to update general and template knowledge base settings. This includes write access to knowledge articles. | CMS API | _**Service Hub** Professional_ or _Enterprise_ | | `cms.membership.access_groups.read` | View [membership access groups](/guides/cms/content/memberships/overview) and their definitions. | CMS API | _**Service Hub**_ or _**Content Hub** Professional_ or _Enterprise_ | | `cms.membership.access_groups.write` | Create, edit, and delete [membership access groups](/guides/cms/content/memberships/overview). | CMS API | **_Service Hub_** or _**Content Hub** Professional_ or _Enterprise_ | | `crm.lists.read` | View details about contact lists. | List endpoints | Any account | | `crm.lists.write` | Create, delete, or make changes to contact lists | List endpoints | Any account | | `crm.objects.appointments.read` | View properties and other details about appointments. | Appointments endpoints | Any account | | `crm.objects.appointments.sensitive.read` | View [Sensitive Data](/reference/api/crm/sensitive-data#sensitive-scopes) properties for appointments. | Appointments endpoints | Any _Enterprise_ account | | `crm.objects.appointments.sensitive.write` | Edit [Sensitive Data](/reference/api/crm/sensitive-data#sensitive-scopes) properties and values for appointments. | Appointments endpoints | Any _Enterprise_ account | | `crm.objects.appointments.write` | Create, delete, or make changes to appointments. | Appointments endpoints | Any account | | `crm.objects.carts.read` | View properties and other details about carts. | Carts endpoints | Any account | | `crm.objects.carts.write` | Create, delete, or make changes to carts. | Carts endpoints | Any account | | `crm.objects.commercepayments.read` | View details about commerce payments. | Commerce payments endpoints | Any _Starter_ account | | `crm.objects.companies.highly_sensitive.read` | View [Highly Sensitive Data](/reference/api/crm/sensitive-data#highly-sensitive-scopes-beta) properties for companies. | Companies endpoints | Any _Enterprise_ account | | `crm.objects.companies.highly_sensitive.write` | Edit [Highly Sensitive Data](/reference/api/crm/sensitive-data#highly-sensitive-scopes-beta) properties and values for companies. | Companies endpoints | Any _Enterprise_ account | | `crm.objects.companies.read` | View properties and other details about companies. | Companies endpoints | Any account | | `crm.objects.companies.sensitive.read` | View [Sensitive Data](/reference/api/crm/sensitive-data#sensitive-scopes) properties for companies. | Companies endpoints | Any _Enterprise_ account | | `crm.objects.companies.sensitive.write` | Edit [Sensitive Data](/reference/api/crm/sensitive-data#sensitive-scopes) properties and values for companies. | Companies endpoints | Any _Enterprise_ account | | `crm.objects.companies.write` | View properties and create, delete, or make changes to companies. | Companies endpoints | Any account | | `crm.objects.contacts.highly_sensitive.read` | View [Highly Sensitive Data](/reference/api/crm/sensitive-data#highly-sensitive-scopes-beta) properties for contacts. | Contacts endpoints | Any _Enterprise_ account | | `crm.objects.contacts.highly_sensitive.write` | Edit [Highly Sensitive Data](/reference/api/crm/sensitive-data#highly-sensitive-scopes-beta) properties and values for contacts. | Contacts endpoints | Any _Enterprise_ account | | `crm.objects.contacts.read` | View properties and other details about contacts. | Contacts endpoints | Any account | | `crm.objects.contacts.sensitive.read` | View [Sensitive Data](/reference/api/crm/sensitive-data#sensitive-scopes) properties for contacts. | Contacts endpoints | Any _Enterprise_ account | | `crm.objects.contacts.sensitive.write` | Edit [Sensitive Data](/reference/api/crm/sensitive-data#sensitive-scopes) properties and values for contacts. | Contacts endpoints | Any _Enterprise_ account | | `crm.objects.contacts.write` | View properties and create, delete, and make changes to contacts. | Contacts endpoints | Any account | | `crm.objects.courses.read` | View details about courses. | Courses endpoints | Any account | | `crm.objects.courses.write` | Create, delete, or make changes to courses. | Courses endpoints | Any account | | `crm.objects.custom.highly_sensitive.read` | View [Highly Sensitive Data](/reference/api/crm/sensitive-data#highly-sensitive-scopes-beta) properties for custom objects. | Custom objects endpoints | Any _Enterprise_ account | | `crm.objects.custom.highly_sensitive.write` | Edit [Highly Sensitive Data](/reference/api/crm/sensitive-data#highly-sensitive-scopes-beta) properties and values for custom objects. | Custom objects endpoints | Any _Enterprise_ account | | `crm.objects.custom.read` | View details about custom objects. | Custom objects endpoints | Any _Enterprise_ account | | `crm.objects.custom.sensitive.read` | View [Sensitive Data](/reference/api/crm/sensitive-data#sensitive-scopes) properties for custom objects. | Custom objects endpoints | Any _Enterprise_ account | | `crm.objects.custom.sensitive.write` | Edit [Sensitive Data](/reference/api/crm/sensitive-data#sensitive-scopes) properties and values for custom objects. | Custom object endpoints | Any _Enterprise_ account | | `crm.objects.custom.write` | Create, delete, or make changes to custom objects. | Custom objects endpoints | Any _Enterprise_ account | | `crm.objects.deals.highly_sensitive.read` | View [Highly Sensitive Data](/reference/api/crm/sensitive-data#highly-sensitive-scopes-beta) properties for deals. | Deals endpoints | Any _Enterprise_ account | | `crm.objects.deals.highly_sensitive.write` | Edit [Highly Sensitive Data](/reference/api/crm/sensitive-data#highly-sensitive-scopes-beta) properties and values for deals. | Deals endpoints | Any _Enterprise_ account | | `crm.objects.deals.read` | View properties and other details about deals. | Deals endpoints | Any account | | `crm.objects.deals.sensitive.read` | View [Sensitive Data](/reference/api/crm/sensitive-data#sensitive-scopes) properties for deals. | Deals endpoints | Any _Enterprise_ account | | `crm.objects.deals.sensitive.write` | Edit [Sensitive Data](/reference/api/crm/sensitive-data#sensitive-scopes) properties and values for deals. | Deals endpoints | Any _Enterprise_ account | | `crm.objects.deals.write` | View properties and create, delete, or make changes to deals. | Deals endpoints | Any account | | `crm.objects.feedback_submission.read` | View details about submissions to any of your feedback surveys. | Feedback surveys endpoints | **_Service Hub_** _Professional_ or _Enterprise_ | | `crm.objects.goals.read` | View all goal types. | Goals endpoints | _**Sales Hub**_ _Starter_, _Professional_, or _Enterprise_ | | `crm.objects.invoices.read` | View details about invoices. | Invoices endpoints | Any account | | `crm.objects.leads.read` | View properties and other details about leads. | Leads endpoints | _**Sales Hub** Professional_ or _Enterprise_ | | `crm.objects.leads.write` | Create, delete, or make changes to leads. | Leads endpoints | **_Sales Hub_** _Professional_ or _Enterprise_ | | `crm.objects.line_items.read` | View properties and other details about line items. | Line items endpoints | Any account | | `crm.objects.line_items.write` | Create, delete, or make changes to line items. | Line items endpoints | Any account | | `crm.objects.listings.read` | View properties and other details about listings. | Listings endpoints | Any account | | `crm.objects.listings.write` | Create, delete, or make changes to listings. | Listings endpoints | Any account | | `crm.objects.marketing_events.read` | View details about marketing events. | Marketing events endpoints | Any account | | `crm.objects.marketing_events.write` | Create, delete, or make changes to marketing events. | Marketing events endpoints | Any account | | `crm.objects.orders.read` | View properties and other details about [orders](/guides/api/crm/commerce/orders). | Orders endpoints | Any account | | `crm.objects.orders.write` | Create, delete, or make changes to [orders](/guides/api/crm/commerce/orders). | Orders endpoints | Any account | | `crm.objects.owners.read` | View details about users assigned to a CRM record. | Owners endpoints | Any account | | `crm.objects.quotes.read` | View properties and other details about quotes and quote templates. | Quotes endpoints | Any account | | `crm.objects.quotes.write` | Create, delete, or make changes to quotes. | Quotes endpoints | Any account | | `crm.objects.services.read` | View properties and other details about services. | Services endpoints | Any account | | `crm.objects.services.write` | Create, delete, or make changes to services. | Services endpoints | Any account | | `crm.objects.subscriptions.read` | View properties and other details about [commerce subscriptions](/guides/api/crm/commerce/subscriptions). | [Commerce subscriptions](/guides/api/crm/commerce/subscriptions) endpoints | Any account | | `crm.objects.users.read` | View properties and other details about [users](/guides/api/settings/users/user-details). | [Users](/guides/api/settings/users/user-details) endpoints | Any account | | `crm.objects.users.write` | Create, delete, or make changes to users. | [Users](/guides/api/settings/users/user-details) endpoints | Any account | | `crm.schemas.carts.read` | View details about property settings for carts. | Carts endpoints | Any account | | `crm.schemas.carts.write` | Create, delete, or make changes to property settings for carts. | Carts endpoints | Any account | | `crm.schemas.commercepayments.read` | View details about property settings for commerce payments. | Commerce payments endpoints. | Any _Starter_ account | | `crm.schemas.companies.read` | View details about property settings for companies | Properties endpoints | Any account | | `crm.schemas.companies.write` | Create, delete, or make changes to property settings for companies. | Properties endpoints | Any account | | `crm.schemas.contacts.read` | View details about property settings for contacts. | Properties endpoints. | Any account | | `crm.schemas.contacts.write` | Create, delete, or make changes to property settings for contacts. | Properties endpoints | Any account | | `crm.schemas.custom.read` | View details about custom object definitions in the HubSpot CRM. | Custom objects endpoints | Any _Enterprise_ | | `crm.schemas.deals.read` | View details about property settings for deals. | Properties endpoints | Any account | | `crm.schemas.deals.write` | Create, delete, or make changes to property settings for deals. | Properties endpoints | Any account | | `crm.schema.invoices.read` | View details about property settings for invoices. | Invoices endpoints | Any account | | `crm.schemas.line_items.read` | View details about line items. | Line items endpoints | Any account | | `crm.schemas.quotes.read` | View details about quotes and quotes templates. | Quotes endpoints | Any account | | `crm.schemas.subscriptions.read` | View details about property settings for [commerce subscriptions](/guides/api/crm/commerce/subscriptions). | Commerce subscriptions endpoints | Any account | | `settings.billing.write` | Make changes to your account's billing settings. This includes managing and assigning paid seats for users. | Settings endpoints | Any account | | `settings.currencies.read` | Reads existing exchange rates along with the current company currency associated with your portal. | Account information endpoints | Any account | | `settings.currencies.write` | Create, update and delete exchange rates along with updating the company currency associated with your portal. | Account information endpoints | Any account | | `settings.users.read` | View details about account users and their permissions. | User Provisioning endpoints | Any account | | `settings.users.write` | Manage users and user permissions on your HubSpot account. This includes creating new users, assigning permissions and roles, and deleting existing users. | User Provisioning endpoints | Any account | | `settings.users.teams.read` | See details about the account's teams. | User Provisioning endpoints | Any account | | `settings.users.team.write` | Assign users to teams on your HubSpot account. | User Provisioning endpoints | Any account | | `account-info.security.read` | Includes access to account activity logs and other account security information. | Account activity API | Any account | | `accounting` | Allows HubSpot and the accounting integration to share invoice, product, and contact details. | Accounting Extension API | Any account | | `actions` | Add forms to the contact's pages that do custom actions. | CRM Extensions API | Any account | | `analytics.behavioral_events.send` | Includes access to send custom behavioral events. | Analytics API | _**Marketing Hub**_ _Enterprise_ | | `automation` | This includes workflows. | Automation API (Workflows endpoints) | _**Marketing Hub**_ _Professional_ or _Enterprise_ | | `behavioral_events.event_definitions.read_write` | Create, read, update, or delete behavioral events. This includes behavioral event properties. | Analytics API | _**Marketing Hub**_ _Enterprise_ | | `business_units.view.read` | View brand data, including logo information. | Brands API | Brands Add-on | | `business-intelligence` | This includes endpoints that sit on top of sources and email. | Analytics API | Any account | | `collector.graphql_query.execute` | Query data from your HubSpot account using the GraphQL API endpoint | GraphQL API endpoint | **_CMS Hub_** _Professional_ or _Enterprise_ | | `collector.graphql_schema.read` | Perform introspection queries via GraphQL application clients such as GraphiQL | GraphiQL and other 3rd party GraphQL clients | _**CMS Hub** Professional_ or _Enterprise_ | | `communication_preferences.read` | View details of your contacts' subscription preferences. | Subscription Preferences API | Any account | | `communication_preferences.read_write` | Subscribe/unsubscribe contacts to your subscription types. It won't subscribe contacts who have unsubscribed. | Subscription Preferences API | Any account | | `communication_preferences.write` | Subscribe/unsubscribe contacts to your subscription types. It won't subscribe contacts who have unsubscribed. | Subscription Preferences API | Any account | | `content` | This includes sites, landing pages, email, blog, and campaigns. | CMS API and Calendar, Email and Email Events endpoints | _**CMS Hub**_ _Professional_ or _Enterprise,_ or _**Marketing Hub**_ _Professional_ or _Enterprise_ | | `conversations.read` | View details about threads in the conversations inbox. | Conversations inbox and messages API | Any account | | `conversations.visitor_identification.tokens.create` | Fetch identification tokens for authenticated website visitors interacting with the HubSpot chat widget. | Visitor Identification API | Any _Professional_ or _Enterprise_ | | `conversations.write` | Send messages in conversations. Create and update message threads. | Conversations inbox and messages API | Any account | | `conversations.custom_channels.read` | View details about custom channels for connected inboxes and help desk. | Custom channels API (BETA) | _**Sales Hub**_ or _**Service Hub**_ _Enterprise_ accounts | | `conversations.custom_channels.write` | Manage custom channels for connected inboxes and help desk. | Custom channels API (BETA) | _**Sales Hub**_ or _**Service Hub**_ _Enterprise_ accounts | | `crm.export` | Export records from your CRM for all CRM data types. | CRM Exports API | Any account | | `crm.import` | Allows you to import records 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). It doesn't include archiving or deleting any data. | CRM Imports API | Any account | | `ctas.read` | Allows read access for CTAs. | No publicAPI available | _**Marketing Hub** or **CMS Hub**_ _Starter, Professional or Enterprise_ | | `e-commerce` | This includes access to e-commerce features. | Products and line items endpoints | Any account. **Note:** Only _Professional_ and _Enterprise_ accounts can use this scope for the Products API. | | `external_integrations.forms.access` | Includes the ability to rename, delete, and clone existing forms. | Forms endpoints | Any account | | `files` | This includes access to File Manager. | Files (File Manager) and file mapper (CMS templates, modules, and layout) endpoints | Any account | | `files.ui_hidden.read` | View details or download user files, attachments, and system files from all HubSpot tools. | Files (File Manager) and file mapper (CMS templates, modules, and layout) endpoints | Any account | | `forms` | This includes access to the Forms endpoints. | Forms endpoints | Any account | | `forms-uploaded-files` | Download files submitted through a form. | Get a file uploaded via form submission endpoint | Any account | | `hubdb` | This includes access to HubDB. | HubDB endpoints | _**CMS Hub**_ _Professional_ or _Enterprise_, or _**Marketing Hub**_ _Professional_ or _Enterprise_ with [Website Add-on](https://www.hubspot.com/products/cms) | | `integration-sync` | This exposes the sync API, which allows syncing of most CRM objects. | Ecommerce Bridge API | Any account | | `marketing-email` | Grants access to send marketing emails through the single-send API | Marketing emails API | _**Marketing Hub**_ _Enterprise_ or the [transactional email add-on](https://www.hubspot.com/products/marketing/transactional-email) | | `media_bridge.read` | Grants access to events and objects from the media bridge. | Media Bridge API | Any account | | `media_bridge.write` | Grants access to create and update events and objects from the media bridge. | Media Bridge API | Any account | | `oauth` | Basic scope required for OAuth. This scope is added by default to all apps. | | Any account | | `sales-email-read` | Grants access to read all details of one-to-one emails sent to contacts. | Engagements endpoints**Note:** This scope is required to get the content of email engagements. See the [Engagements overview](/guides/api/crm/engagements/engagement-details#email-content) for more details. | Any account | | `social` | This includes Social Inbox. | Social Media API | _**Marketing Hub**_ _Professional_ or _Enterprise_ | | `tax_rates.read` | View details about [tax rates](/guides/api/crm/commerce/line-items#retrieve-tax-rates) configured in your account. | Tax rates API | Any account | | `tickets` | This includes access to tickets. | Tickets endpoints | Any account | | `tickets.highly_sensitive` | Grants access to view and edit [Highly Sensitive Data](/reference/api/crm/sensitive-data#highly-sensitive-scopes-beta) properties and values for tickets. | Tickets endpoints | Any Enterprise account | | `tickets.sensitive` | Grants access to view and edit [Sensitive Data](/reference/api/crm/sensitive-data#sensitive-scopes) properties and values for tickets. | Tickets endpoints | Any Enterprise account | | `timeline` | Grants access to manage custom events on HubSpot CRM records. This includes creating or updating records. | Timeline Events endpoints | Any account | | `transactional-email` | This includes transactional emails and the transactional emails endpoints. | Transactional email endpoints | _**Marketing Hub**_ _Professional_ or _Enterprise_ with [Transactional Email Add-on](https://www.hubspot.com/products/marketing/transactional-email) | # Webhooks | Validating Requests @ An overview on validating requests originating from HubSpot to an integration. $ guides & apps/authentication --- # Validating requests from HubSpot 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](/guides/apps/authentication/validating-requests#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 `v1` or `v2`. 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. ## Validate requests using the v1 request signature If your app is subscribed to [CRM object events via the webhooks API](/guides/api/app-management/webhooks), requests from HubSpot 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 //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: ```py 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 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" ``` ```js 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' ``` The resulting hash would be: `232db2615f3d666fe21a8ec971ac7b5402d33b9a925784df3ca654d05f4817de` ## 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 a [custom CRM card](/guides/api/crm/extensions/crm-cards), 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. **Notes:** - 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 Node.js code snippet below details how you could incorporate `v2` request validation for a `GET` request if you were running an Express server 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 Express service. Confirm that you're running the latest stable and secure libraries when implementing request validation for your specific service. ```js // 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(); } }); ``` ## 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 Node.js code snippet below details how you could incorporate v3 request validation for a `POST` request if you were running an Express server 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 Express service. Confirm that you're running the latest stable and secure libraries when implementing request validation for your specific service. ```js // 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 - timestamp > MAX_ALLOWED_TIMESTAMP) { console.log("Timestamp is invalid, reject request"); // Add any rejection logic here } // Concatenate request method, URI, body, and header timestamp const uri = `https://${hostname}${url}`; const rawString = `${method}${uri}${JSON.stringify(body)}${timestamp}`; // 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. } }); ``` # 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. $ guides & apps/authentication --- # Working with OAuth 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](/guides/apps/public-apps/overview#install-an-app) in their HubSpot accounts. - any app designed for installation by multiple HubSpot accounts or listing on the App 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 [App Marketplace Access](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#settings) permissions. ##### Recommended resources - The [OAuth Quickstart Guide](/guides/apps/authentication/oauth-quickstart-guide) will get you up and running with a working example app. - This [HubSpot Academy tutorial](https://app.hubspot.com/l/academy/tracks/71/593/2967) provides a quick introduction on using OAuth with HubSpot, including a breakdown of the HubSpot-OAuth flow and how to refresh an access token. ## Initiating an integration with OAuth 2.0 To initiate an integration with OAuth 2.0: - First, [create an app](/guides/apps/public-apps/overview) in a [HubSpot developer account](https://app.hubspot.com/signup-hubspot/developers). 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. - Use the client ID and client secret, along with the [query parameters](#query-parameters) and [scopes](#scopes) outlined below, to build your authorization URL. - 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. After granting access, they'll be redirected back to your application via a `redirect_url`, which will have a code query parameter appended to it. You'll use that code and the client secret to get an [access_token and refresh_token](/guides/api/app-management/oauth-tokens) from HubSpot. - **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` - Use the `access_token` to authenticate any API calls made for that HubSpot account. - Once the `access_token` expires, use the `refresh_token` to generate a new `access_token`. - 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 <u>do not</u> 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 parameters are required when building an authorization URL for your app: | **Parameter** | **Description** | **How to use** | | --- | --- | --- | | Client ID | `client_id=x`Used in the URL | Get this from your app's Auth settings page (as described above). | | Redirect URL | `redirect_uri=x`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 | `scope=x%20x` | 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 on an account that doesn't have access to an included scope.See the Scopes table below for more details about which endpoints can be accessed by specific scopes. | The following parameters are optional: | **Parameter** | **How to use** | **Description** | | --- | --- | --- | | Optional scopes | `&optional_scope=x%20x` | 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 does not have access to that tool (such as requesting the social scope on a CRM only portal). If you're using optional scopes, you will need to check the access token or refresh token to see which ones were granted. See the table below for more details about scopes. | | State | `&state=y`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_url`. | 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. 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. If your app can work with multiple types of HubSpot accounts, you can use the `optional_scope` parameter to include any tiered scopes you work. This way, customers using CRM-only accounts can still authorize your app, even if they can't access all of its scopes. Your app must check for and handle any scopes that it doesn't get authorized for. | **Scope** | **Description** | **Provides access to** | **Required account tier** | | --- | --- | --- | --- | | `cms.domains.read` | Integrators can list CMS domains in a customer's account. | CMS API | Any account | | `cms.domains.write` | Integrators can create, update, and delete CMS custom domains. | CMS API | Any account | | `cms.functions.read` | Integrators can view all CMS serverless functions, any related secrets, and function execution results. | CMS API | _**CMS Hub** Enterprise_ | | `cms.functions.write` | Integrators can write CMS serverless functions and secrets. | CMS API | _**CMS Hub**_ _Enterprise_ | | `cms.knowledge_base.articles.read` | View details about knowledge articles. | CMS API | _**Service Hub**_ _Professional_ or _Enterprise_ | | `cms.knowledge_base.articles.write` | Grants access to update knowledge articles. | CMS API | _**Service Hub** Professional_ or _Enterprise_ | | `cms.knowledge_base.articles.publish` | Grants access to update and publish knowledge articles. | CMS API | _**Service Hub** Professional_ or _Enterprise_ | | `cms.knowledge_base.settings.read` | View general and template knowledge base settings, such as the domain or root URL. | CMS API | _**Service Hub** Professional_ or _Enterprise_ | | `cms.knowledge_base.settings.write` | Grants access to update general and template knowledge base settings. This includes write access to knowledge articles. | CMS API | _**Service Hub** Professional_ or _Enterprise_ | | `cms.performance.read` | Integrators can view CMS performance data for all your sites. | CMS API | Any account | | `crm.lists.read` | View details about contact lists. | List endpoints | Any account | | `crm.lists.write` | Create, delete, or make changes to contact lists | List endpoints | Any account | | `crm.objects.companies.read` | View properties and other details about companies. | Companies endpoints | Any account | | `crm.objects.companies.write` | View properties and create, delete, or make changes to companies. | Companies endpoints | Any account | | `crm.objects.contacts.read` | View properties and other details about contacts. | Contacts endpoints | Any account | | `crm.objects.contacts.write` | View properties and create, delete, and make changes to contacts. | Contacts endpoints | Any account | | `crm.objects.custom.read` | View details about custom objects in the HubSpot CRM. | Custom objects endpoints | Any _Enterprise_ | | `crm.objects.custom.write` | Create, delete, or make changes to custom objects in the HubSpot CRM. | Custom objects endpoints | Any _Enterprise_ | | `crm.objects.deals.read` | View properties and other details about deals. | Deal endpoints | Any account | | `crm.objects.deals.write` | View properties and create, delete, or make changes to deals. | Deal endpoints | Any account | | `crm.objects.feedback_submission.read` | View details about submissions to any of your feedback surveys. | Feedback survey endpoints | **_Service Hub_** _Professional_ or _Enterprise_ | | `crm.objects.goals.read` | View all goal types. | Goals endpoints | _**Sales Hub**_ _Starter_, _Professional_, or _Enterprise_ | | `crm.objects.line_items.read` | View properties and other details about line items | Line items endpoints | Any account | | `crm.objects.line_items.write` | Create, delete, or make changes to line items. | Line items endpoints | Any account | | `crm.objects.marketing_events.read` | View details about marketing events. | Marketing events endpoints | Any account | | `crm.objects.marketing_events.write` | Create, delete, or make changes to marketing events. | Marketing events endpoints | Any account | | `crm.objects.owners.read` | View details about users assigned to a CRM record. | Owners endpoints | Any account | | `crm.objects.quotes.read` | View properties and other details about quotes and quote templates. | Quote endpoints | Any account | | `crm.objects.quotes.write` | Create, delete, or make changes to quotes. | Quote endpoints | Any account | | `crm.schemas.companies.read` | View details about property settings for companies | Properties endpoints | Any account | | `crm.schemas.companies.write` | Create, delete, or make changes to property settings for companies. | Properties endpoints | Any account | | `crm.schemas.contacts.read` | View details about property settings for contacts. | Properties endpoints. | Any account | | `crm.schemas.contacts.write` | Create, delete, or make changes to property settings for contacts. | Properties endpoints | Any account | | `crm.schemas.custom.read` | View details about custom object definitions in the HubSpot CRM. | Custom objects endpoints | Any _Enterprise_ | | `crm.schemas.deals.read` | View details about property settings for deals. | Properties endpoints | Any account | | `crm.schemas.deals.write` | Create, delete, or make changes to property settings for deals. | Properties endpoints | Any account | | `crm.schemas.line_items.read` | View details about line items. | Line items endpoints | Any account | | `crm.schemas.quotes.read` | View details about quotes and quotes templates. | Quote endpoints | Any account | | `settings.billing.write` | Make changes to your account's billing settings. This includes managing and assigning paid seats for users. | Settings endpoints | Any account | | `settings.currencies.read` | Reads existing exchange rates along with the current company currency associated with your portal. | Account information endpoints | Any account | | `settings.currencies.write` | Create, update and delete exchange rates along with updating the company currency associated with your portal. | Account information endpoints | Any account | | `settings.users.read` | View details about account users and their permissions. | User Provisioning endpoints | Any account | | `settings.users.write` | Manage users and user permissions on your HubSpot account. This includes creating new users, assigning permissions and roles, and deleting existing users. | User Provisioning endpoints | Any account | | `settings.users.teams.read` | See details about the account's teams. | User Provisioning endpoints | Any account | | `settings.users.team.write` | Assign users to teams on your HubSpot account. | User Provisioning endpoints | Any account | | `account-info.security.read` | Includes access to account activity logs and other account security information. | Account activity API | Any account | | `accounting` | Allows HubSpot and the accounting integration to share invoice, product, and contact details. | Accounting Extension API | Any account | | `actions` | Add forms to the contact's pages that do custom actions. | CRM Extensions API | Any account | | `analytics.behavioral_events.send` | Includes access to send custom behavioral events. | Analytics API | _**Marketing Hub**_ _Enterprise_ | | `automation` | This includes workflows. | Automation API (Workflows endpoints) | _**Marketing Hub**_ _Professional_ or _Enterprise_ | | `behavioral_events.event_definitions.read_write` | Create, read, update, or delete behavioral events. This includes behavioral event properties. | Analytics API | _**Marketing Hub**_ _Enterprise_ | | `business_units.view.read` | View business unit data, including logo information. | Business Units API | Business Units Add-on | | `business-intelligence` | This includes endpoints that sit on top of sources and email. | Analytics API | Any account | | `collector.graphql_query.execute` | Query data from your HubSpot account using the GraphQL API endpoint | GraphQL API endpoint | **_CMS Hub_** _Professional_ or _Enterprise_ | | `collector.graphql_schema.read` | Perform introspection queries via GraphQL application clients such as GraphiQL | GraphiQL and other 3rd party GraphQL clients | _**CMS Hub** Professional_ or _Enterprise_ | | `communication_preferences.read` | View details of your contacts' subscription preferences. | Subscription Preferences API | Any account | | `communication_preferences.read_write` | Subscribe/unsubscribe contacts to your subscription types. It won't subscribe contacts who have unsubscribed. | Subscription Preferences API | Any account | | `communication_preferences.write` | Subscribe/unsubscribe contacts to your subscription types. It won't subscribe contacts who have unsubscribed. | Subscription Preferences API | Any account | | `content` | This includes sites, landing pages, email, blog, and campaigns. | CMS API and Calendar, Email and Email Events endpoints | _**CMS Hub**_ _Professional_ or _Enterprise,_ or _**Marketing Hub**_ _Professional_ or _Enterprise_ | | `conversations.read` | View details about threads in the conversations inbox. | Conversations inbox and messages API | Any account | | `conversations.visitor_identification.tokens.create` | Fetch identification tokens for authenticated website visitors interacting with the HubSpot chat widget. | Visitor Identification API | Any _Professional_ or _Enterprise_ | | `conversations.write` | Send messages in conversations. Create and update message threads. | Conversations inbox and messages API | Any account | | `crm.export` | Export records from your CRM for all CRM data types. | CRM Exports API | Any account | | `crm.import` | Allows you to import records 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). It doesn't include archiving or deleting any data. | CRM Imports API | Any account | | `ctas.read` | Allows read access for CTAs. | No publicAPI available | _**Marketing Hub** or **CMS Hub**_ _Starter, Professional or Enterprise_ | | `e-commerce` | This includes access to e-commerce features. | Products and line items endpoints | Any account | | `external_integrations.forms.access` | Includes the ability to rename, delete, and clone existing forms. | Forms endpoints | Any account | | `files` | This includes access to File Manager. | Files (File Manager) and file mapper (CMS templates, modules, and layout) endpoints | Any account | | `files.ui_hidden.read` | View details or download user files, attachments, and system files from all HubSpot tools. | Files (File Manager) and file mapper (CMS templates, modules, and layout) endpoints | Any account | | `forms` | This includes access to the Forms endpoints. | Forms endpoints | Any account | | `forms-uploaded-files` | Download files submitted through a form. | Get a file uploaded via form submission endpoint | Any account | | `hubdb` | This includes access to HubDB. | HubDB endpoints | _**CMS Hub**_ _Professional_ or _Enterprise_, or _**Marketing Hub**_ _Professional_ or _Enterprise_ with [Website Add-on](https://www.hubspot.com/products/cms) | | `integration-sync` | This exposes the sync API, which allows syncing of most CRM objects. | Ecommerce Bridge API | Any account | | `media_bridge.read` | Grants access to events and objects from the media bridge. | Media Bridge API | Any account | | `media_bridge.write` | Grants access to create and update events and objects from the media bridge. | Media Bridge API | Any account | | `oauth` | Basic scope required for OAuth. This scope is added by default to all apps. | | Any account | | `sales-email-read` | Grants access to read all details of one-to-one emails sent to contacts. | Engagements endpoints**Note:** This scope is required to get the content of email engagements. See the [Engagements overview](/guides/api/crm/engagements/engagement-details#email-content) for more details. | Any account | | `social` | This includes Social Inbox. | Social Media API | _**Marketing Hub**_ _Professional_ or _Enterprise_ | | `tickets` | This includes access to tickets. | Tickets endpoints | Any accounbt | | `timeline` | Grants access to manage custom events on HubSpot CRM records. This includes creating or updating records. | Timeline Events endpoints | Any account | | `transactional-email` | This includes transactional emails and the transactional emails endpoints. | Transactional email endpoints | \*\*Marketing Hub\*\* Professional or Enterprise with [Transactional Email Add-on](https://www.hubspot.com/products/marketing/transactional-email) | ## Related docs [Auth methods on HubSpot](/guides/apps/authentication/intro-to-auth) [OAuth Quickstart Guide](/guides/apps/authentication/oauth-quickstart-guide) [Managing tokens](/guides/api/app-management/oauth-tokens) # @ You can receive calls in HubSpot when using calling apps. $ guides & apps/extensions/calling-extensions --- # Receive calls in HubSpot when using calling apps Developers using the [Calling SDK](/guides/api/crm/extensions/calling-sdk) can now enable inbound calling within HubSpot. When a user receives and answers a call through your app in HubSpot, they can access call records directly in HubSpot, eliminating the need to switch back to the calling app. Calls are automatically logged in the [Call Index Page](https://knowledge.hubspot.com/calling/review-calls-in-the-call-index) where users can take real-time notes and review the call after it ends. ## Requirements Keep the following requirements in mind when building your app: 1. **Calling app appears inline within HubSpot:** you want the users of your calling app to be anchored within HubSpot since it’s their primary workspace. To accomplish this, you’ll need to anchor the calling app to the HubSpot navigation bar via the calling remote. 2. **Calls are not interrupted by user navigation:** users should be able to move between pages in HubSpot without losing an ongoing call. To avoid triggering a full page refresh when navigating in HubSpot, which would cause calls to drop, you can leverage the detached calling window, which will hold the call connection and maintain site navigation functionality. ## Approaches to designing your app The following approaches are available when designing the UI of your integration: ### 1. Use both the calling remote and the calling window For a fully featured calling app experience, you can take this approach to ensure that your app appears in the calling remote, and the call connection is also held in the calling window. You'll need to ensure that your integration synchronizes the state of the calling remote and the calling window. For a guide on how to implement this approach, check out the section on [calling state synchronization](#calling-state-synchronization). ### 2. Use the calling remote only With this approach, your calling app will appear in the calling remote, anchored to the HubSpot navigation bar. The main difference between this approach and the first approach is that your calling app must hold the connection using a desktop application or some other custom solution. If you don’t implement a custom solution to hold the connection, calls will still work but they’ll be disconnected if the user navigates to another HubSpot page. This is similar to the [current drag-and-drop call widget experience](https://developers.hubspot.com/docs/guides/api/crm/extensions/calling-sdk). ### 3. (Not recommended) Use the calling window only If needed, you can take this alternate approach and only use the calling window in your app, and users will not be able to use the calling remote with your app. Additionally, incoming calls won't appear in front of the currently focused browser window, and outbound calls will not bring your calling app into focus for the user. This approach will be sunset on August 31, 2025. Integrators should transition to a recommended approach before this date. ## Configure your inbound calling integration Follow the steps below to set up and configure your integration so users can receive inbound calls within HubSpot. ### 1. Configure your calling extension settings Based on the approach you take for the [calling experience of your app](#approaches-to-designing-your-app), update your app settings by making a `PATCH` request to `/crm/v3/extensions/calling/{APP_ID}/settings?hapikey={DEVELOPER_ACCOUNT_API_KEY}`, and provide the ID of your app and your developer account API key in the request. ```shell curl --request PATCH \ --url 'https://api.hubapi.com/crm/v3/extensions/calling/APP_ID/settings?hapikey=DEVELOPER_ACCOUNT_API_KEY' \ --header 'accept: application/json' \ --header 'content-type: application/json' \ --data '{"supportsInboundCalling":true, "usesCallingWindow":true, "usesRemote":true}' ``` You can provide the following fields in your request to specify the approach your app is taking, as well as any other app details: | Field name | Type | Description | | --- | --- | --- | | `supportsInboundCalling` | Boolean | Enables inbound calling feature. The flag is set to `false` by default. This extension setting gates users to the new inbound calling experience and instead shows the drag and drop call widget in record pages for outbound calling. | | `usesCallingWindow` | Boolean | Determines whether the calling window is used. | | `usesRemote` | Boolean | Determines whether the calling remote is used. Note that this field will be deprecated after Aug 31, 2025. | The values you provide for each of the fields above depend on the [approach](#approaches-to-designing-your-app) you take for designing your app. - If you're using both the calling remote and calling window in your app, or if you're just using the window-only approach, each of the values above should be set to `true`: ```json { ...otherExtensionSettings, "supportsInboundCalling": true, "usesCallingWindow": true, "usesRemote": true } ``` - If you decide to use the remote-only approach, you should only set the `supportsInboundCalling` and `usesCallingWindow` fields to `true`: ```json { ...otherExtensionSettings, "supportsInboundCalling": true, "usesCallingWindow": false, "usesRemote": true } ``` For local development, you can either use [local storage overrides](/guides/api/crm/extensions/calling-sdk) to test changes or directly apply the settings using a [developer test account](/getting-started/account-types#developer-test-accounts) without impacting production. ### 2. Install the latest version of Calling SDK For npm, run: ```hubl npm i -s @hubspot/calling-extensions-sdk@latest ``` For yarn, run: ```hubl yarn add @hubspot/calling-extensions-sdk@latest ``` ### 3. Set user availability You must set the user's availability using on of the following events: - Via the `initialized` event: ```js const payload = { // Optional: Whether a user is logged-in isLoggedIn: boolean, // Optional: Whether a user is available for inbound calling isAvailable: boolean, // Optional: The desired widget size sizeInfo: { height: number, width: number, }, }; extensions.initialized(payload); ``` - Via the `userAvailable` event: ```js extensions.userAvailable(); ``` - Via the `userUnavailable` event: ```js extensions.userUnavailable(); ``` ### 4. Send message to notify HubSpot that an inbound call started You will be able to send calling lifecycle events, such as `callAnswered` and `callCompleted`, in the same way it is done for outgoing calls. ```js const callInfo = { fromNumber: string, // Required: The caller's number toNumber: string, // Required: The recipient's number createEngagement: boolean, // Whether HubSpot should create an engagement for this call }; extensions.incomingCall(callInfo); ``` - If you’ve set `createEngagement` to true, you can subscribe to `onCreateEngagementSucceeded` and `onCreateEngagementFailed`. It is recommended you do this so that you can enable your calling app to support [custom objects](/guides/api/crm/objects/custom-objects). This will allow future integration into other areas of HubSpot. ```js onCreateEngagementSucceeded(data) { const { /* A HubSpot created engagement id. */ engagementId: number, } = data; ... } onCreateEngagementFailed(data) { const { error: { message: string } } = data; ... } ``` ### 5. Receive caller ID matches - You will be able to subscribe to `onCallerIdMatchSucceeded` and `onCalledIdMatchFailed`. This will enable you to receive contact matching data for the incoming call that previously had to be obtained via the [Search API](/guides/api/crm/search), and will solve its rate limitations. ```js onCallerIdMatchSucceeded: data => { /* HubSpot has fetched caller id matches for this call. */ const { callerIdMatches: (ContactIdMatch | CompanyIdMatch)[]; } = data; } onCallerIdMatchFailed: data => { /* HubSpot has failed to fetch caller id matches for this call. */ const { error: { message: string } } = data; } ``` ```js type ObjectCoordinates = { portalId: number; objectTypeId: string; objectId: number; } type ContactIdMatch = { callerIdType: 'CONTACT'; objectCoordinates: ObjectCoordinates; firstName: string; lastName: string; email: string; } type CompanyIdMatch = { callerIdType: 'COMPANY'; objectCoordinates: ObjectCoordinates; name: string; } ``` ### 6. Navigate to a record page Once you receive the caller ID matches, you can send HubSpot a message to navigate to a [contact or company record page](https://knowledge.hubspot.com/records/view-and-filter-records). ```js const data = { objectCoordinates: ObjectCoordinates, // from onCallerIdMatchSucceeded }; extensions.navigateToRecord(data); ``` Once the call engagement is created, HubSpot will redirect to the contact page specified in the `navigateToRecord` payload and will sync with the SDK in the `onReady` event. You'll need to re-initialize the SDK using the engagement ID and show an incoming call within the iframe. ```js // Receive an engagementId for an existing inbound call type Payload = { engagementId: number | undefined } // Message indicating that HubSpot is ready to receive messages onReady(payload) { // Send initialized message to HubSpot to indicate that the call widget is also ready extensions.initialized(payload); if (payload.engagementId) { // Initialize calling state in the app for existing inbound call ... } ... } ``` In the following sections, preview how the incoming call feature will work in your calling app. ### 7. Set the provider Before logging in to your calling app, you'll need to select the provider from your call settings: - In your HubSpot account, click the **settings icon** in the main navigation bar. - In the left sidebar menu, click **General**. Then, click the **Calling** tab at the top. - Click the **Make and receive calls through** dropdown menu, then select your calling app. Once the preferred provider is selected, incoming calls will <u>only</u> be received through the selected provider. HubSpot doesn't support receiving incoming calls from multiple providers in this version. To switch providers, repeat the steps above to access the provider menu and select a different provider. If you're switching from a provider that does not support inbound calling and still uses the drag-and-drop call widget for outbound calls, you can also find the **Change provider** button within the drag-and-drop call widget to access the provider menu. Calling remote change provider button: ### 8. Receive incoming calls If you've not already set up an integration with any of the [calling apps](https://ecosystem.hubspot.com/marketplace/apps/calling), [learn more](https://knowledge.hubspot.com/calling/integrate-a-third-party-calling-provider-with-hubspot). - Log in to your calling app through the call widget in HubSpot. The call widget can be accessed on the main navigation bar. - Open calling window if calling window is used by your calling app. - Set availability to "Available" to start receiving calls. - Answer inbound calls from the call remote. - The behavior may vary slightly based on each calling apps' implementation. - If the call widget is minimized but you're set to _Available_, you will still receive calls. If the call tab is closed during an ongoing call, the call will get disconnected. Once the call is completed, the inbound call gets logged in the [Call Index page](https://knowledge.hubspot.com/calling/review-calls-in-the-call-index). Missed calls will also get logged here. Once the above steps are complete, you can continue to extend the functionality of your calling app in HubSpot by [setting up third-party calling in help desk (BETA)](/guides/api/crm/extensions/third-party-calling). ## Calling state synchronization If you decide to take the [first approach above](#approaches-to-designing-your-app) and implement the calling remote and window functionality when building your app, calling apps will maintain their call connection in the calling window of your app. Your integration must still handle synchronizing state between the calling window and the calling remote. The setup section below outlines how to structure an `onReady` event handler to conditionally respond to different user events. You can review [a full example](https://github.com/HubSpot/calling-extensions-sdk/tree/master/demos/demo-react-ts/src) in the calling-extensions-sdk repository. ### Setup When HubSpot is ready to receive messages, the `onReady` event handler will be called with a set of [properties](/guides/api/crm/extensions/calling-sdk#onready), including `iframeLocation`. You can use this property in your app to define conditions based on the value of `iframeLocation`: - `iframeLocation = window`**:** this means your calling app is hosted in the detached calling window. It's recommended to handle the main calling logic in the calling window to maintain the call connection while navigating throughout HubSpot. - `iframeLocation = remote`**:** this indicates that your calling app is hosted in the anchored calling remote. It's recommended that you use the remote to reflect the calling state happening in the calling window and rely on the calling window to handle the main calling logic. This ensures users can navigate freely within HubSpot without disconnecting their calls. ```js import CallingExtensions from "@hubspot/calling-extensions-sdk"; const options = { ..., eventHandlers: { ..., onReady: ({iframeLocation, ...restOfTheProperties}) => { /* HubSpot is ready to receive messages. */ // Store the property to create different logic for different scenarios this.iframeLocation = iframeLocation }, } }; const extensions = new CallingExtensions(options); ``` ### Example One option to keep instances of your calling applicaiton synchronized is to use the [broadcast channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API). Based on whether the user is starting an outbound call from the remote or the calling window, you can structure your logic accordingly: **When a user starts the outbound call from the remote:** - The remote UI updates to display a ringing state. - The remote UI sends a broadcast message to the calling window. - When the calling window receives the broadcast message: - The calling window UI places the call based on your app's custom routing logic. - The calling window UI updates to display a ringing state. - If necessary, the calling window uses any other methods from the [calling extensions SDK](/guides/api/crm/extensions/calling-sdk) to display additional updates (e.g., recording the call) **When a user starts the outbound call from the calling window:** - The calling window UI places the call based on on your app's custom routing logic. - The calling window UI updates to display a ringing state. - The calling window UI sends a broadcast message to the remote. - If necessary, the calling window uses the [Calling Extensions SDK](/guides/api/crm/extensions/calling-sdk) to communicate updates. - When the remote receives the broadcast message, the remote UI updates to display the ringing state. The following code snippet provides an example of how you might handle the scenarios above. ```js private iframeLocation; private extensions; this.extensions = new CallingExtensions({ eventHandlers: { ..., onReady: (data: {iframeLocation, ...restOfTheProperties}) => { this.iframeLocation = iframeLocation }, ... }, }); // Create a BroadcastChannel to communicate between your app this.broadcastChannel = new BroadcastChannel("calling-app-example"); this.broadcastChannel.onmessage = (data) => { switch(data.type) { case 'OUTGOING_CALL_STARTED': { if (this.iframeLocation === 'window') { placeCall() this.extensions.outgoingCall({externalCallId: '123'}) } handleUIChangeToDialing() } ... } } // When users start an outbound call, this function will be called handleOutgoingCall(callDetails: OnOutgoingCall) { this.broadcastChannel.postMessage({ type: thirdPartyToHostEvents.OUTGOING_CALL_STARTED, payload: callDetails, }); if (this.iframeLocation === 'window') { placeCall() this.extensions.outgoingCall({externalCallId: '123'}) } handleUIChangeToDialingState() } ``` You can review a full implementation of [this logic](https://github.com/HubSpot/calling-extensions-sdk/blob/master/demos/demo-react-ts/src/hooks/useCti.ts) and the broadcast channel handlers in the [demo repository](https://github.com/HubSpot/calling-extensions-sdk/blob/master/demos/demo-react-ts/src/components/App.tsx#L163). # Extending the CRM | Recordings and transcriptions @ Learn how to log call recordings and their associated transcriptions in HubSpot. $ guides & apps/extensions/calling-extensions --- # Recordings and transcripts If you want to make call recordings playable in your HubSpot account, or you want to build on top of HubSpot's [Conversation Intelligence](https://knowledge.hubspot.com/calling/manage-phone-numbers-registered-for-calling#turn-on-conversation-intelligence-sales-hub-or-service-hub-enterprise-only) functionality, you can use the endpoints to automatically transcribe calls and log them within HubSpot. ## Requirements - HubSpot will only transcribe calls associated with [users with a paid _Sales_ or _Services_ hub seat](https://knowledge.hubspot.com/account-management/manage-seats). - Only .WAV, .FLAC, and .MP4 audio files will be transcribed. - The audio file must be downloadable as an octet-stream. - In the transcription system, HubSpot splits the audio file into its different channels and treats each channel as a separate speaker. If all of the speakers are on the same audio channel, or if the caller or recipient are on an unexpected channel, HubSpot will <u>not</u> be able to transcribe the audio recording. Therefore, each speaker in an audio file should be on a separate channel. For calls with two channels, the caller should be on channel 1, and the call recipient should be on channel 2, regardless of whether the call is inbound or outbound. - If your users want to fast forward or rewind a call recording in the HubSpot app, the recording URL needs to respect the `range` header and return a `206 partial content`​(not a `200`server code). ## Create an endpoint to provide an authenticated recording URL for a call To list and transcribe calls on a [record's timeline](https://knowledge.hubspot.com/records/view-the-history-of-an-activity-on-a-record) in HubSpot, create an endpoint that will be invoked to retrieve the authenticated call URLs associated with each engagement. Your endpoint should accept the following parameters: - **externalId:** the unique ID associated with a call URL, provided as a path parameter. This will correspond to the same parameter you include in the metadata of your `POST` request to the engagements API, which you can then use in your app's backend to associate with the recording URL. - **externalAccountId:** a unique ID associated with the HubSpot account that made the call engagement, provided as a query parameter. You can use this parameter along with the externalId to identify the call recording. - **appId:** the [ID of your app](https://developers.hubspot.com/docs), provided as a query parameter. Your endpoint should return a JSON response with a `authenticatedUrl` field that provides the recording URL. ```json // Response to GET request to your app's endpoint { "authenticatedUrl": "https://app-test.com/retrieve/authenticated/recordings/test-call-01" } ``` ## Register your app's endpoint with HubSpot using the calling settings API Once your endpoint is ready, make a `POST` request using your app's ID to `/crm/v3/extensions/calling/{appId}/settings/recording` and provide the URL of your endpoint with the `urlToRetrieveAuthedRecording` parameter in the body of your request. - Your endpoint's URL must contain the _%s_ character sequence, which HubSpot will substitute with the `externalId` of the engagement when calling your endpoint. The _%s_ character sequence can be located anywhere in your URL. - Provide the full path of your endpoint URL in your `POST` request, including the _https://_ prefix. For example: ```json // Example POST request to configure your app's endpoint { "urlToRetrieveAuthedRecording": "https://app-test.com/retrieve/authenticated/recordings/%s" } ``` If you change the location of your endpoint, you can make a `PATCH` request to the same HubSpot endpoint above and provide an updated value for `urlToRetrieveAuthedRecording`. ## Log a call with your app's endpoint using the engagements API After you've registered your calling app's endpoint with HubSpot, you can log a call by making a `POST` request to the `/crm/v3/objects/calls` [endpoint](/guides/api/crm/engagements/calls), and including the engagement data within the properties field in the body of your request. The `hs_call_external_id`, `hs_call_external_account_id`, `hs_call_app_id`, and `hs_call_source` properties are required to ensure that HubSpot can fetch the authenticated recording URL. The body of an example request is shown below: ```json // POST request to https://api.hubapi.com/crm/v3/objects/calls { "properties": { "hs_timestamp": "2021-03-17T01:32:44.872Z", "hs_call_title": "Test v3 API", "hubspot_owner_id": "11526487", "hs_call_body": "Decision maker out, will call back tomorrow", "hs_call_duration": "3800", "hs_call_from_number": "(555) 555 5555", "hs_call_to_number": "(555) 555 5555", "hs_call_source": "INTEGRATIONS_PLATFORM", // this has to be INTEGRATIONS_PLATFORM "hs_call_status": "COMPLETED", "hs_call_app_id": "test-app-01", "hs_call_external_id": "test-call-01", "hs_call_external_account_id": "test-account-01" } } ``` Next, you'll need to [associate the call with a record type](/guides/api/crm/engagements/calls#associate-calls-with-records) to ensure the transcript appears on the record timeline. - To make this association, make a `PUT` request to `/crm/v3/objects/calls/{callId}/associations/{toObjectType}/{toObjectId}/{associationType}`. - For example, if the ID of the logged call you created above is _17591596434_, the ID of the contact you wanted to associate it with is _104901_, and the ID of the associationType is _194_, your request URL would be: `https://api.hubspot.com/crm/v3/objects/calls/17591596434/associations/contacts/104901/194` When one of your app's users navigates to the associated record timeline to view the engagement, HubSpot will call the endpoint you configured to serve the authenticated recording URL. For example, to retrieve the recording URL associated with the example engagement above, HubSpot would make a `GET` request to: `https://app-test.com/retrieve/authenticated/recordings/test-call-01?appId=app-101&externalAccountId=test-account-01` ## Mark a call recording as ready Create the call object as shown [above](#log-a-call-with-your-app-s-endpoint), and then do the following: Make a `POST` request to `/crm/v3/extensions/calling/recordings/ready` with the `engagementId` for the call that was created. This will notify HubSpot that the recording is ready and transcription can begin. The body of an example request is shown below: ```json // Example POST request to log a recording as being ready for a call { "engagementId": 17591596434 } ``` - If you're using the legacy approach of logging call recordings without authentication, please update to the authenticated approach before September 2024. After this time, the unauthenticated approach will no longer be supported. # Extensions overview @ The HubSpot API allows developers to build custom apps and integrations. Get started by reading our API overview and creating a developer account. $ guides & apps/extensions --- # Extensions overview Using extensions, you can customize the functionality of the HubSpot CRM. HubSpot offers a variety of extensions, such as creating custom events for CRM record timelines or enabling custom calling options with the calling SDK. You can also create UI extensions if you’re enrolled in the CRM development tools beta, which enables you to create custom cards with a wide variety of customizable components. Below is a list of the extensions that HubSpot currently offers: - [**Calling extensions SDK**](/guides/api/crm/extensions/calling-sdk)**:** enable users to [make calls](https://knowledge.hubspot.com/calling/use-the-calling-tool) using custom calling options. - [**Custom timeline events**](/guides/api/crm/extensions/timeline)**:** create custom events that display information from other systems on CRM record timelines. - [**Classic CRM cards**](/guides/api/crm/extensions/crm-cards)**:** create cards to pull external data into CRM records. These types of cards are separate from UI extensions, and don't offer as much customization options. - [**UI extensions (BETA)**](/guides/crm/ui-extensions/create)**:** customize CRM record pages with custom cards that can send and receive HubSpot and external data using a wide variety of customizable components. - [**Video conference extension**](/guides/api/crm/extensions/video-conferencing)**:** integrate video conferencing into the meetings tool. Extensions are powered by apps, which means you’ll first need to create a [public app](/guides/apps/public-apps/overview) or a [private app in projects (BETA)](/guides/crm/private-apps/creating-private-apps) before you can add an extension to an account. ## Extension support in apps Because private apps and public apps support different extensions, you’ll first need to decide what type of app to create. Review the table below to understand which extensions are supported by which app types. Learn more about the differences between these types of apps in the [building apps overview](/guides/apps/overview). | App type | Supported extensions | | --- | --- | | Private app | [UI extensions](/guides/crm/ui-extensions/overview) are supported in [private apps created with projects (BETA)](/guides/crm/private-apps/creating-private-apps). | | Public app | <ul><li>[Calling SDK](/guides/api/crm/extensions/calling-sdk)</li><li>[Classic CRM cards](/guides/api/crm/extensions/crm-cards)</li><li>[Timeline events](/guides/api/crm/extensions/timeline)</li><li>[Video conference extension](/guides/api/crm/extensions/video-conferencing)</li></ul>[UI extensions](/guides/crm/ui-extensions/overview) are supported in [public apps created with projects (BETA)](/guides/crm/public-apps/creating-public-apps). | Classic CRM cards are different from the app cards you can create as [UI extensions with projects (BETA)](/guides/crm/ui-extensions/create). UI extensions offer more advanced functionality and customizable components. # App Marketplace | App listing requirements @ App Partners can now see HubSpot's guidelines and requirements for getting an app listed on the App Marketplace in one place. $ guides & apps/marketplace --- # App Marketplace listing requirements 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](/guides/apps/marketplace/listing-your-app) from within your app developer account by navigating to **App Marketplace > Listings > Create listing**. ## Minimum requirements - **Single HubSpot app ID:** your app must authorize API requests with the public HubSpot app ID (and [OAuth client ID](/guides/apps/authentication/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. - Your listed public app must be unique. If you have already listed an app and you 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. - **OAuth:** your app must use OAuth as its sole authorization method. Learn more about [working with OAuth](/guides/apps/authentication/working-with-oauth). - **Installs:** your app must have at least three [active, unique installs](/guides/apps/marketplace/certification-requirements#:~:text=Active%20installs%20are%20the%20number%20of%20unique%20HubSpot%20production%20accounts%2C%20unaffiliated%20with%20your%20organization%2C%20showing%20successful%20app%20activity%20within%20the%20past%2030%20days.). You won’t be able to submit your app listing without this. - **Scopes:** you must only request scopes your app needs. [Review your scopes](/guides/apps/authentication/working-with-oauth#scopes) and make sure you’re not asking for unnecessary access. Apps that do this tend to have better conversion rates. - Your app must have [advanced scope settings](/guides/apps/public-apps/overview#configure-scopes) turned on. All required, conditionally required, and optional scopes should be selected to prevent errors. These settings can be found in the [developer account](/getting-started/account-types#app-developer-accounts) that manages your app. - **Terms:** you must review and agree to the terms in [HubSpot's App Partner Program Agreement](https://legal.hubspot.com/app-program-agreement). This protects you, HubSpot, and our shared customers. You won’t be able to submit your app listing without completing this step. - **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). ### Brand requirements - Your app and its associated collateral (documentation, landing pages, etc.) must meet [HubSpot’s Branding Guidelines](https://www.hubspot.com/partners/app/branding-guidelines). For example, capitalize the “S” in “HubSpot” any time you’re referring to HubSpot. - Your app and its associated collateral (documentation, landing pages, etc.) must not infringe [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), [Reveal](https://ecosystem.hubspot.com/marketplace/apps/reveal-191193). - All URLs in your App 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](/guides/apps/marketplace/create-an-app-listing-setup-guide). This guide also includes an example template that meets all requirements. - For a live example, check out the [OrgChartHub setup guide](https://orgcharthub.com/guides/setup). - 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 <u>250</u> 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](/guides/apps/marketplace/app-marketplace-listing-requirements#:~:text=Webhooks%20API.-,Scopes,-%3A%20You%20must) 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 App 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. - Follow the guidelines listed [here](/guides/apps/marketplace/testing-credentials) for providing testing credentials for your app listing. ## 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 6o days from the time feedback is shared. As stated in the [App Marketplace Terms](https://legal.hubspot.com/app-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 App Partners - Dedicated HubSpot App 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 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](/guides/apps/marketplace/listing-your-app) - [App certification requirements](/guides/apps/marketplace/certification-requirements) - [API reference documentation](/reference/api/overview) - [How to use the HubSpot APIs](/guides/api/overview) - [Developer community forum](https://community.hubspot.com/t5/APIs-Integrations/bd-p/integrations) - [Contact the App Partner team](https://www.hubspot.com/partners/app/join) # 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. $ guides & apps/marketplace --- # Apply for app certification ### What is app certification? App certification involves the HubSpot Ecosystem Quality team reviewing and confirming that your listed app meets [these requirements](/guides/apps/marketplace/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](/guides/apps/marketplace/certification-requirements) app 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](/guides/apps/marketplace/certification-requirements). You will not be able to apply unless your app 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: - [Log in](https://knowledge.hubspot.com/account-management/why-can-t-i-log-into-hubspot) to your developer account and navigate **Marketplace** \> **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:** enter testing credentials for your app for the HubSpot Ecosystem Quality team to evaluate its functionality. - At the bottom, click **Submit certification application**. ### Requirements for the app demo video Demo videos help the HubSpot Ecosystem Quality team test your app. The team <u>will not</u> 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 the HubSpot App 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](/guides/apps/marketplace/certification-requirements) or reach out to your [App Partner Manager](/guides/apps/marketplace/certification-requirements#related-resources) if you have any questions about eligibility. ### How will users know my app is certified? Once certified, your App 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 **App Partner Manager** and their email information by logging into your developer account and navigating to **App Marketplace** > **Listings**. Hover over your app, click **More** > **View Listing Details**. --- #### Related docs [App certification requirements](/guides/apps/marketplace/certification-requirements) [App listing requirements](/guides/apps/marketplace/app-marketplace-listing-requirements) # App Marketplace | App certification requirements @ Here's what App Partners need to get their app certified in HubSpot's App Marketplace. $ guides & apps/marketplace --- # Getting certified in the App Marketplace App certification involves the HubSpot Ecosystem Quality team reviewing and confirming that your [listed app](/guides/apps/marketplace/app-marketplace-listing-requirements) meets the requirements below for security, privacy, reliability, performance, usability, accessibility, and value. Certified apps stand out in the [App 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. - These requirements are subject to change, as HubSpot is continuously making improvements to the HubSpot App Marketplace and Ecosystem. HubSpot can reject an app certification request at their discretion if it doesn't meet the set standards. - HubSpot will <u>not</u> review your app unless you submit a demo video as instructed when [applying for app certification](/guides/apps/marketplace/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](/guides/apps/authentication/working-with-oauth) and all scopes it requires. - Be associated with a [verified domain](/guides/apps/public-apps/overview). - Public assets associated with your app must abide by security best practices. See the [detailed list of security and privacy requirements](#security-privacy). App activity is defined by OAuth-authenticated requests to HubSpot's [APIs](/reference/api/overview) and [signed requests](/guides/apps/authentication/validating-requests) from HubSpot [webhook subscriptions](/guides/api/app-management/webhooks) and extensions (e.g. [CRM card](/guides/api/crm/extensions/crm-cards) 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/app), certified apps receive: - A "HubSpot Certified App" badge displayed on its App Marketplace listing. - More prominent visibility in the HubSpot App Marketplace: - Inclusion in the "HubSpot Certified App" search filter. - Eligibility for inclusion in curated App 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 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](/guides/apps/authentication/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](/guides/apps/authentication/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](/guides/apps/authentication/working-with-oauth) it requests for installation (i.e. both in the required `scope` parameter and the `optional_scope` parameter). - Have [advanced scope settings](/guides/apps/public-apps/overview#configure-scopes) turned on and select all required, conditionally required, and optional scopes the app requests for installation. - 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. - Be associated with a [verified domain](/guides/apps/public-apps/overview). - Your public assets will be assessed for security best practices related to outdated software and various web server vulnerabilities and findings. #### 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 Your app must: - Be in good standing, meaning: - 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, beta, and developer preview APIs are considered unstable and must not be used in your production app. - If your app uses APIs that are not stable or public, please surface this during the certification review or to the App Partner Manager team to discuss options. - Maintain a reasonable volume of [activity](#activity) from HubSpot customer accounts unaffiliated with your organization. - Your app must adhere to the [API usage guidelines](/guides/apps/api-usage/usage-details) 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. - Your app must 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](/guides/apps/marketplace/certification-requirements#activity). - Your app may have 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. [CRM cards](/guides/api/crm/extensions/crm-cards) and [custom workflow actions](/guides/api/automation/custom-workflow-actions)) provide more consistent user experiences for customers - Your app will be subjected to an additional security assessment if it includes a browser extension. #### 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](/guides/api/crm/objects/contacts) instead. ### Usability & Accessibility | App - Your app must be [listed](/guides/apps/marketplace/app-marketplace-listing-requirements) in the HubSpot [App 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. #### 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 | App Marketplace Listing Your App 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. - Contain clear and accurate pricing information, including a link to your current pricing page. - If multiple packages support your app, the App Marketplace listing must, at minimum, include the least expensive option. - 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/apps/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](/guides/apps/marketplace/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 App 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 app 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 our [design system](https://canvas.hubspot.com/). - Videos are also 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 App 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 App 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](/guides/apps/marketplace/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](/guides/apps/marketplace/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 App 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. After being certified for one year, the HubSpot Ecosystem Quality team may review your app to ensure it still meets the rigorous certification requirements. If you app no longer meets these standards, the team will collaborate with you for up to sixty days to resolve concerns. 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 one year. As stated in the [App Partner Program Agreement](https://legal.hubspot.com/app-program-agreement), HubSpot also reserve the right to unpublish your app at any time. ## Frequently asked questions No, we do not charge you a fee to list or certify your apps in the App Marketplace, nor a fee for installs generated through the App 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 [App Partner Manager](#app-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/app/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 app partners! We recommend you reach out to your [App Partner Manager](#app-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 App Marketplace are continuing to evolve. We’d like to learn more about how you would find being featured the most helpful (e.g. App Marketplace, HubSpot community, HubSpot curated newsletters or other forms). Your [App Partner Manager](#app-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. The HubSpot Ecosystem Quality team will reach out if your app is out of compliance or due for annual recertification with next steps. We encourage you to monitor your app’s performance, certification requirements, [Developer Changelog](https://developers.hubspot.com/changelog), and any additional HubSpot resources related to any changes in technology used and how your app could stay up to date. Find your **App Partner Manager** and their email information by logging into your developer account and navigating to **App Marketplace** > **Listings**. Hover over your app, click **More** > **View Listing Details**. --- ## Related resources [How to apply for app certification](/guides/apps/marketplace/applying-for-app-certification) [How to list your app](/guides/apps/marketplace/listing-your-app) [App listing requirements](/guides/apps/marketplace/app-marketplace-listing-requirements) [Developer community forum](https://community.hubspot.com/t5/APIs-Integrations/bd-p/integrations) [Contact the App Partner team](https://www.hubspot.com/partners/app/join) # @ Learn more about creating a publicly available setup guide to include with your HubSpot Marketplace app listing. $ guides & apps/marketplace --- # Create an app listing setup guide In order for an app to be [listed](/guides/apps/marketplace/listing-your-app) or [certified](/guides/apps/marketplace/certification-requirements) on the HubSpot App Marketplace, the listing must include a publicly available setup guide URL. 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 App Marketplace listing. ## Setup guide requirements In order to be [listed](/guides/apps/marketplace/listing-your-app) on the HubSpot App 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](/guides/apps/marketplace/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](/guides/apps/marketplace/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](/guides/apps/marketplace/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). # App Marketplace | Listing your app @ Follow these steps to submit an app for listing on HubSpot's App Marketplace. $ guides & apps/marketplace --- # Listing your app After you’ve created an app in your developer account that meets the [App Marketplace listing requirements](/guides/apps/marketplace/app-marketplace-listing-requirements), you can submit a listing to add it to the [HubSpot App Marketplace](https://ecosystem.hubspot.com/marketplace/apps). The HubSpot Ecosystem Quality team will review your submission and follow up via email if the app has been approved or rejected. Below, learn how to create and submit your app listing. 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 Before submitting an app listing, review the [App listing requirements page](/guides/apps/marketplace/app-marketplace-listing-requirements) to understand how to fill your listing. - In your [app developer account](/guides/apps/overview), navigate to **Marketplace** \> **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 the App Marketplace will not appear here. - On the next screen, click the **Select the languages your app is available in** dropdown menu and select the **languages** your app software is offered in. The App Marketplace is available in 14 languages: German, English, Spanish, French, Italian, Japanese, Dutch, Portuguese, Chinese, Finnish, Danish, Norwegian, Polish, and Swedish. - Click the **Select the primary listing language for \[app name\]** dropdown menu and select the **default language** users will see when browsing the App 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**. - In the _App icon_ section, upload an 800px by 800px icon for your app. This will appear in the App 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](/guides/apps/marketplace/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 the App 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/apps/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 app 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](/guides/apps/marketplace/testing-credentials). - In the _App Partner Program points of contact_ section, add details for members of your team to receive listing, technical and program information about your app listing. - You must include at least an _App Partner Program main point of contact_ and a _Developer_ to contact. You must include the person's **role**, **first name**, **last name**, **email**, and **country** where they are based in. - To add another person as a point of contact, at the bottom, click **Add another point of contact**. ### 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**. ## 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 create listings in other languages. - 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 the App Marketplace. - Click **Save.** Once you have set a primary language, you will be able to add a new localized listing: - Hover over the **app listing** and click **More** > **Create listing in another language**. - If you'd like to auto translate your listing to other languages, click to toggle the **Auto translate** switch on. - If you've selected this option, any translated listings will be automatically and immediately published to the marketplace. The auto translated listings will bypass admin review. - Once the listings are translated, any edits made in the primary listing will <u>not</u> automatically update in the translated listings. To update an auto translated listing: - Delete the existing translated listing and then create translated listing again in the same languages. When creating a new listing, changes from the primary listing will be pulled in. - You can also manually update the translated listing. However, manual changes will go through the review process and has to be approved by HubSpot admins before being published. - 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. - If the _Auto translate_ option has been turned on, the languages selected will be auto translated and published as new listings. - Click **Create**. If you're manually creating a listing, follow the steps to create and submit a listing in the selected language. Updating your primary listing does not automatically update your localized listings. Each localized listing must be updated independently. ## Edit a live app listing Once an app listing is live on the app marketplace, to edit the 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**. - After making your changes, in the top right, click **Validate & submit**. ## 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. # Measuring app performance @ Learn more about measuring your app's performance through the app listing page details view. $ guides & apps/marketplace --- # Measure app performance After [listing your app](/guides/apps/marketplace/listing-your-app) in the [HubSpot App Marketplace](https://ecosystem.hubspot.com/marketplace/apps), you can review performance metrics for your app in your developer account. This includes performance metrics such as usage data, install data, and listing analytics. ## Review your app's marketplace analytics - In your [app developer account](/guides/apps/overview), navigate **Marketplace** \> **App Listings**. This will bring you to a dashboard with all your App Marketplace listings. - Click the **name** of your app. ### Installs In the _Installs_ tab, review detailed metrics on your app's performance. At the top, you can set the **Frequency** and **Date range** for your data. You can analyze the following data: - App installs - App uninstalls - App installs vs uninstalls - Free vs Paid installs - Free vs Paid uninstalls - Install by Country - Installs by Hub ### Listing analytics In the _Listing analytics_ tab, review metrics for the app's listing page. At the top, you can set the **Frequency** and **Date range** for your data. You can analyze the following data: - Pageviews - Pageview Source - CTA Usage - Free vs Paid Pageviews - Pageviews by Country - Pageviews by Hub - Pageviews by Domain ### User actions In the _User actions_ tab, review the type and count of user actions that occurred from the app: - At the top, you can filter your user actions by **date range**. - In the table, review the following: - **User action:** the type of activity that the user engaged in. For example, the number of contacts created from the app, the count of files uploaded, and more. - **Activity:** the count of user actions. - To sort the table by _user action_ or _activity_ count, click the table's **headers**. ## 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: - **App partner program benefits:** the _App Partner Program Benefits_ guide will lists all benefits that you get as a partner with a listed app on the Hubspot App Marketplace. - **Feedback survey:** a feedback survey to provide feedback about the app partner experience. - **App partner manager:** the contact information for your app partner manager. ## App Activity In the _App Activity_ section, you can review and export API usage data for your app: - At the top, you can filter your API usage data by **date range**. By default, this will be set to _All time._ - In the table, review the following: - **Date:** the date the API calls were made. - **API:** the API endpoint requested by the app. - **Successful calls:** the count of API calls successfully made, without errors. ## 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 enables you to view how much traffic is coming to your website from your listing page, using HubSpot, Google Analytics, or other analytics platforms of your choosing. It's recommended to add UTM parameters to the following URLs included on your listing page: - Supporting content on your App 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` 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 the [basics of UTM parameters](https://blog.hubspot.com/customers/understanding-basics-utm-parameters) and how to create UTM tracking URLs with [HubSpot](https://knowledge.hubspot.com/settings/how-do-i-create-a-tracking-url) and [Google Analytics](https://blog.hubspot.com/marketing/what-are-utm-tracking-codes-ht). 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**. 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 **App Marketplace** \> **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. Because your app listing can be found through Google and other search engines, it's also important to ensure your listing is SEO-friendly. One recommended strategy to improve your SEO is through backlinks. Whether you're writing website content, sending out email newsletters, or drafting social media messages, consider adding links to your listing page, along with relevant information about your integration. You can further expand your reach through strategies like [guest blogging](https://blog.hubspot.com/marketing/guest-blogging) to improve SEO authority. Learn more about [creative ways to earn backlinks](https://blog.hubspot.com/marketing/backlink-strategies), and check out [HubSpot Academy's free lesson on link building](https://academy.hubspot.com/lessons/link-building-tutorial). #### Related docs [App certification requirements](/guides/apps/marketplace/certification-requirements) [App listing requirements](/guides/apps/marketplace/app-marketplace-listing-requirements) # @ Follow these steps to provide testing credentials for your app when listing or certifying it on the App Marketplace. $ guides & apps/marketplace --- # Provide testing credentials for your app Valid testing credentials must be provided to [list](/guides/apps/marketplace/listing-your-app) or [certify](/guides/apps/marketplace/applying-for-app-certification) your app on the HubSpot App 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 <u>all</u> 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 <u>not able</u> 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 <u>does not</u> 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 <u>are able</u> 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. <u>**Do not**</u> 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 <u>not able</u> 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. <u>**Do not**</u> ask the HubSpot Product team to create new accounts or sign up for free trials. 3. For each platform/application/service that you are <u>not</u> 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 <u>must</u> meet the requirements listed [here](/guides/apps/marketplace/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. # App Marketplace | Understand app categories @ When submitting an app for listing on HubSpot's App Marketplace, understand app categories before selecting the category for your app. $ guides & apps/marketplace --- # Understand app categories When [listing your app](/guides/apps/marketplace/listing-your-app) in the HubSpot App Marketplace, you can select up to <u>two</u> categories for your app. Use the categories to define the functionality and purpose of your app. 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 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. # Building apps overview @ Learn about HubSpot's developer tools and developer accounts. Use them to build an integration, list an app on the Marketplace, or create a test account. $ guides & apps --- # Building apps overview Apps enable you to authenticate API calls to your HubSpot account, subscribe to events with webhooks, and extend the HubSpot UI, such as with custom cards. HubSpot offers several types of apps and extensions depending on your integration's needs. Below, learn more about the types of apps and extensions you can build with HubSpot, how to get started building them, and how to manage them in HubSpot. ## Types of apps Depending on the type of integration you want to build, you’ll need to choose the right type of app. Below, learn more about the types of apps that you can build and the functionalities that they support. For building extensions, [view the reference table below](#supported-extensions-by-app-type) for a quick overview of which extensions can be built with which types of apps. ### Private apps Private apps can be created for a single HubSpot account, and are best suited for one-off scripts or single-purpose extensions. In general, private apps are simpler to implement than public apps. Private apps authenticate with access tokens and cannot be listed on the HubSpot App Marketplace. For example, you might build a private app for your HubSpot account to: - Create a new custom object through the API. - Import CRM records from a CSV file. - Authenticate API requests in custom automation actions and chat bots. - [Create and edit webhook subscriptions](/guides/apps/private-apps/create-and-edit-webhook-subscriptions-in-private-apps). Learn more about [when to build private apps](https://developers.hubspot.com/blog/hubspot-integration-choosing-private-public-hubspot-apps). If this type of app fits your needs, [get started creating a private app in your HubSpot account](/guides/apps/private-apps/overview). #### Private apps in projects (BETA) Private apps built using the developer projects framework enable you to create UI extensions for CRM records and help desk preview panels. You'll need a _**Sales Hub**_ or **_Service Hub_** _Enterprise_ subscription to build a UI extension in for a private app in a standard HubSpot account. However, you can try these tools out for free in a [developer test account](/getting-started/account-types#developer-test-accounts). To get started, [enroll your account into the beta](/guides/crm/overview#get-started-with-private-apps), then follow the [quickstart guide](/guides/crm/private-apps/quickstart). ### Public apps Public apps can be installed in multiple accounts. In addition to the types of extensions you can build with a private app, public apps support advanced functionality, such as: - Subscribing to account-wide events using the webhooks API. - Creating custom timeline events on CRM records using the timeline events API. - Creating custom app settings pages in HubSpot. Public apps authenticate with OAuth and can be listed on the HubSpot App Marketplace. Learn more about [when to build public apps](https://developers.hubspot.com/blog/hubspot-integration-choosing-private-public-hubspot-apps). If this type of app fits your needs, [get started creating a public app in your app developer account](/guides/apps/public-apps/overview). #### Public apps in projects (BETA) Public apps built using the developer projects framework enable you to create UI extensions for CRM records and help desk preview panels. You can also include other extensions in a public app created this way, but you'll need to manage those extensions in HubSpot and through the API as before. If you're enrolled in the beta, get started by following the [quickstart guide](/guides/crm/public-apps/quickstart), or learn how to [migrate an existing public app to the projects framework](/guides/crm/public-apps/migrate-a-public-app-to-projects). ### Supported extensions by app type | App type | Supported extensions | | --- | --- | | Private app | [UI extensions](/guides/crm/ui-extensions/overview) are supported in [private apps created with projects (BETA)](/guides/crm/private-apps/creating-private-apps). | | Public app | <ul><li>[Calling SDK](/guides/api/crm/extensions/calling-sdk)</li><li>[Classic CRM cards](/guides/api/crm/extensions/crm-cards)</li><li>[Timeline events](/guides/api/crm/extensions/timeline)</li><li>[Video conference extension](/guides/api/crm/extensions/video-conferencing)</li></ul> [UI extensions](/guides/crm/ui-extensions/overview) are supported in [public apps created with projects (BETA)](/guides/crm/public-apps/creating-public-apps). | \* Classic CRM cards are different from the app cards you can create as [UI extensions with projects (BETA)](/guides/crm/ui-extensions/create). UI extensions offer more advanced functionality and customizable components. ## Types of accounts for app development While app developer and test accounts work together, they each serve a distinct purpose. - App developer accounts are intended for building and listing apps on the App Marketplace. - Developer test accounts, which can be created in developer accounts, are intended for testing APIs and apps you’re building without impacting data in a real HubSpot account. - If you have a **_Sales Hub_** or _**Service Hub** Enterprise_ subscription, you can use [development sandboxes](/getting-started/account-types#development-sandbox-accounts-beta-) in a standard HubSpot account to quickly iterate without impacting the account's data or assets. Learn more about [HubSpot's account types](/getting-started/account-types). ## Authentication If you want to build a custom integration with a single HubSpot account, you can create a [private app](/guides/apps/private-apps/overview) and use its [access token](/guides/apps/private-apps/overview#make-api-calls-with-your-app-s-access-token) to authenticate API calls, or you can use [OAuth](/guides/apps/authentication/working-with-oauth) with a public app. Any app designed for installation by multiple HubSpot accounts or listing on the App Marketplace must use OAuth. As of November 30, 2022, HubSpot API Keys have been deprecated and are no longer supported. Continued use of HubSpot API Keys is a security risk to your account and data. You should instead authenticate using a private app access token or OAuth. Learn more about [this change](https://developers.hubspot.com/changelog/upcoming-api-key-sunset) and how to [migrate an API key integration](/guides/apps/private-apps/migrate-an-api-key-integration-to-a-private-app) to use a private app instead. If you want to subscribe to webhooks or set up [OAuth for your app](/guides/apps/authentication/working-with-oauth), you should [create a developer account](https://app.hubspot.com/signup-hubspot/developers). With developer accounts, you can also [list your apps](/guides/apps/marketplace/listing-your-app) on the App Marketplace or [create a test account](/getting-started/account-types). ### Developer API keys To manage your app's settings through the API, you can use a developer API key. This API key is separate from standard API keys, which have been deprecated. Developer API keys can be used for managing subscriptions for the [Webhooks API](/guides/api/app-management/webhooks/overview) and [creating or updating event types for the timeline events feature](/guides/api/crm/extensions/timeline). All other API calls need to be made using a [private app access token](/guides/apps/private-apps/overview#make-api-calls-with-your-app-s-access-token) or OAuth. To access your app developer account API key: - In your app developer account, navigate to **Keys** > **Developer API Key** in the left navigation bar. - Click **Create key**. - Once the key is created, you can click **Show key**. The key will be revealed, and you can then click **Copy** below the key. - To review a list of recent security actions associated with the key, click **View Audit Logs**. - You can also rotate the current API key by clicking the **Actions** dropdown menu, hovering over **Rotate key**, then clicking **Deactivate and rotate this key now**. - To deactivate the key without regenerating a new one, click the **Actions** dropdown menu, hover over **Deactivate key**, then click **Deactivate this key now**. ## Delete a developer account You can delete app developer accounts if they don’t contain apps with installations or active marketplace listings. If your account has apps with installations or active marketplace listings and you’d like to delete your account, please reach out to support for assistance. Once you delete your account, you will no longer be able to access that account. If you can switch between multiple HubSpot accounts, the deleted account will no longer appear. In your HubSpot API developer account, click your account name in the top right corner, then click on **Account.** Click **Delete account.** If your account has any apps with installations or active marketplace listings this button will be disabled. In the dialog box, enter your account ID then click **Delete developer account** # Create and edit webhook subscriptions in private apps @ Learn how to create and edit webhook subscriptions in private apps. $ guides & apps/private-apps --- # Create and edit webhook subscriptions in private apps Webhooks allow you to subscribe to events in a [private app](/guides/apps/private-apps/overview) that you've created in your HubSpot account. Rather than making an API call to query for event changes in your HubSpot account, you can subscribe to events which will trigger HubSpot to send an API request to an endpoint that you configure in your private app. Using webhooks allows your private app to be more scalable than having to regularly poll for changes. In your private app settings, you can subscribe to CRM object events, which includes contacts, companies, deals, tickets, products and line items, as well as conversations events. ## Limitations The following limitations apply to webhook subscriptions in private apps: - If you're currently enrolled in the [CRM development tools beta](/guides/crm/overview), [private apps created in a project](/guides/crm/private-apps/creating-private-apps) do <u>not</u> currently support webhook subscriptions. You can still follow the instructions in this article to create standalone private apps, separate from the ones you create in your projects, to create webhook subscriptions. - Managing your private app's webhook subscriptions via API is not currently supported. Subscriptions can only be managed in your private app settings. - A limit of 1000 webhook subscriptions applies per private app. ## Create a webhook in your private app - In your HubSpot account, click the **settings icon** in the main navigation bar. - In the left sidebar menu, navigate to **Integrations** > **Private Apps**. - Click the **name** of your private app. If you haven't created a private app yet, follow the instructions in [this article](/guides/apps/private-apps/overview#create-a-private-app) to create one. - Click the **Webhooks** tab. - Click **Edit webhooks**. - Under _Target URL_, enter the **URL** that HubSpot will make a `POST` request to when events trigger. - Click **Create subscription**. - In the right panel, configure your subscription: - Select the **object types** that you want to subscribe to, then select the **events** associated with those objects (e.g., created, merged, deleted) that will trigger HubSpot to send a request to the endpoint you configured. - If you select an object type that requires scopes your app hasn't been authorized for, you'll be prompted to add those scopes to your app. - If you select _Property changed_ for the event type, you can then select any of the associated object properties that you want to subscribe to changes for. - Click **Subscribe**. - If you don't want your webhook subscriptions to be active immediately, or if you want to delete a subscription you mistakenly created, you can hover over the webhook and manage its status, unsubscribe to delete it, or review the subscription details. If you've configured multiple webhook subscriptions, you can edit their statuses in bulk by selecting the **checkboxes** next to each one then clicking **Activate** or **Pause**. - When you're ready to save your changes, click **Commit changes** in the top right. After you've configured your webhook subscriptions, HubSpot will begin sending `POST` requests to your _Target URL_ once any of the events associated with your active subscriptions are triggered. For each private app, HubSpot sets a concurrency limit of 10 requests when sending subscription event data. This concurrency limit is the maximum number of in-flight requests that HubSpot will attempt at a time. Each request can contain up to 100 events. ## Review and test webhook subscriptions On the _Webhooks_ tab of your private app, you can review, edit, and test all your configured subscriptions. - To edit a subscription, hover over it then click **Edit subscriptions**. - Hover over an existing subscription to pause, activate, or delete it. You can also click **Create subscription** to configure a new webhook. - When you're done making changes, click **Commit changes** in the top right. If the changes you made required changes to your private app's scopes, you'll be prompted to confirm the scope changes. - To test a webhook subscription, hover over it then click **View details**. - In the right panel, review the details of your webhook subscription, including the event payload that HubSpot will send in the request. - To test that your target URL is working properly, click **Test**. This will send the listed sample event payload to your configured target URL, and the resulting response from your endpoint will appear at the bottom of the panel. To ensure that the requests you're receiving in your webhook endpoint are actually coming from HubSpot, HubSpot populates a `X-HubSpot-Signature` header with a SHA-256 hash that's built using your private app's client secret along with data from the request itself. You can find your private app's client secret by navigating to the **Auth** tab in your private app's settings, then clicking **Show secret**. . ## Check webhook subscription logs To help you debug and analyze the volume of your configured subscriptions, webhook triggers and the corresponding responses from your endpoint will be provided in your private app logs. - On the details page of your private app, click the **Logs** tab. - Click **Webhooks**. - Review the history of your event subscription triggers, which includes both successful and unsuccessful API requests that HubSpot performed. At the top of the table, you can click the dropdown menus to filter by subscription type, status, time period, or you can search for specific log entries by batch ID or log ID. - To get more details about a log entry, click the **entry** to see additional details about when the event triggered, what fields were included in the payload, along with other metadata for the event. # Migrate an API key integration to a private app @ Learn how to migrate an API key integration to a private app. $ guides & apps/private-apps --- # Migrate an API key integration to a private app If you're seeing a banner in your account about deactivating your API key: - Ensure that you've migrated all impacted integrations, then [deactivate the API key](https://developers.hubspot.com/docs/guides/apps/authentication/intro-to-auth#deactivate-your-api-key). - To check if the account's API key has been used in the past seven days, you can [view your API key call log history](https://developers.hubspot.com/docs/guides/apps/authentication/intro-to-auth#view-your-api-key-call-log). The call log will not show any requests made with the key more than seven days ago. - Apps listed on the [_Connected apps_ page of your account settings](https://knowledge.hubspot.com/integrations/connect-apps-to-hubspot#:~:text=You%20can%20view%20all%20your,Connected%20Apps%20page.) do not need to be migrated, as they authenticate with OAuth. - Developer API keys are separate from standard HubSpot API keys, and are <u>not</u> being deprecated. Developer API keys are used for managing settings related to your HubSpot apps, including [webhooks API](/guides/api/app-management/webhooks) subscriptions and [timeline events API](/guides/api/crm/extensions/timeline) event types. If you've built an internal integration that uses a [HubSpot API key](https://developers.hubspot.com/docs/guides/apps/authentication/intro-to-auth), your API key provides both read and write access to all of your HubSpot CRM data, which can be a security risk if your API key is compromised. By migrating to a private app, you can authorize the specific scopes that your integration requires, which generates an access token that limits the data that your integration can request or change in your account. Follow the steps below to migrate an existing API key integration to a private app. It's recommended you first use a test environment, such as a [developer test account](/getting-started/account-types) or [sandbox account](https://knowledge.hubspot.com/account-management/set-up-a-hubspot-standard-sandbox-account), before making changes in production. If you have questions while migrating your app, visit the [Developer Community](https://community.hubspot.com/t5/HubSpot-Developers/ct-p/developers). For a video walkthrough of migrating an API key integration to a private app, check out the HubSpot Developers video below: ## In this guide - [Create a new private app](#create-a-new-private-app) - [Update the authorization method of your integration's API request](#update-the-authorization-method-of-your-integration-s-api-requests) - [Verify requests and monitor logs](#verify-requests-and-monitor-logs) - [Implementation examples](#implementation-examples) Private apps do not support webhooks and [certain types of extensions](/guides/apps/extensions/overview). If your existing integration uses any of these features, you should create a public app using [OAuth](/guides/apps/authentication/working-with-oauth) instead. ## Create a new private app - In your HubSpot account, click the **settings icon** in the main navigation bar. - In the left sidebar menu, navigate to **Integrations** > **Private Apps**. - Click **Create private app**. - On the _Basic Info_ tab, configure the details of your app: - Enter your app's **name**. - Hover over the placeholder logo and click the **upload icon** to upload a square image that will serve as the logo for your app. - Enter a **description** for your app. - Click the **Scopes** tab. - Next, select the scopes to authorize based on the APIs that your integration uses. To find out which scopes your app will need: - Compile a list of HubSpot APIs that your existing integration uses. - For each API request, navigate to the associated developer documentation (e.g., the [contacts API](/guides/api/crm/objects/contacts)). - At the top of the guide, click the link to the _reference documentation_, then scroll to the endpoint your integration is using. - Under the _Requirements_ section, locate the scopes required to use the endpoint. Whenever possible, you should opt for scopes listed under _Granular scopes_ instead of the ones under _Standard scopes_. If no granular scopes are listed, use the standard scopes. - Back in the settings for your private app, select the **Read** or **Write** checkboxes next to the matching scopes. You can also search for a scope using the _Find a scope_ search bar. - After you're done selecting your scopes, click **Create app** in the top right. You can always make changes to your app after you create it. - In the dialog box, review the info about your app's access token, then click **Continue creating**. With your private app created, you can start making API requests using its access token. On the _Details_ tab of the settings page of your private app, click **Show token** under your access token to reveal it. ## Update the authorization method of your integration's API requests Instead of using a `hapiKey` query parameter to make API requests, private app access tokens are included in the `Authorization` header of your request. When making a request, set the value of the `Authorization` header to `Bearer YOUR_ACCESS_TOKEN`. Unless otherwise noted, this method of authorization is compatible with all public API endpoints, including the legacy APIs listed on HubSpot's [legacy developer documentation](/reference/api). Your request may resemble the following: ```js axios.get( 'https://api.hubapi.com/crm/v3/objects/contacts', { headers: { Authorization: `Bearer ${YOUR_ACCESS_TOKEN}`, 'Content-Type': 'application/json', }, }, (err, data) => { // Handle the API response } ); ``` ```php $headers = [ 'Content-Type: application/json', 'Authorization: Bearer ' . YOUR_ACCESS_TOKEN, ]; $curl = curl_init(); curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); curl_setopt($curl, CURLOPT_URL, 'https://api.hubapi.com/crm/v3/objects/contacts'); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); $contacts = curl_exec($curl); curl_close($curl); var_dump($contacts); ``` ```ruby require 'uri' require 'net/http' require 'openssl' url = URI("https://api.hubapi.com/crm/v3/objects/contacts") http = Net::HTTP.new(url.host, url.port) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE request = Net::HTTP::Get.new(url) request['content-type'] = 'application/json' token = 'YOUR_ACCESS_TOKEN' request['authorization'] = "Bearer #{token}" response = http.request(request) puts response.read_body ``` ```py import requests url = "https://api.hubapi.com/crm/v3/objects/contacts" headers = { 'content-type': 'application/json', 'authorization': 'Bearer %s' % YOUR_ACCESS_TOKEN } response = requests.request("GET", url, headers=headers) print(response.text) ``` Private app access tokens are implemented on top of OAuth, so you can also make authenticated calls with your access token using one of HubSpot's client libraries. For example, if you're using the [Node.js client library](https://github.com/HubSpot/hubspot-api-nodejs), you can instantiate an OAuth client by passing in your app's access token. ```js const hubspotClient = new hubspot.Client({ accessToken: YOUR_ACCESS_TOKEN }); ``` ```php $hubSpot = \HubSpot\Factory::createWithAccessToken('access-token'); $response = $hubSpot->crm()->contacts()->basicApi()->getPage(); ``` ```ruby # Load the gem require 'hubspot-api-client' # Setup client client = Hubspot::Client.new(access_token: 'YOUR_ACCESS_TOKEN') # Get contacts contacts = client.crm.contacts.basic_api.get_page ``` ```py from hubspot import HubSpot api_client = HubSpot(access_token='YOUR_ACCESS_TOKEN') api_client.crm.contacts.get_page() ``` To complete the migration over to your private app, remove all references to the HubSpot API key from your code, and instead use the approach above to use your private app's access token. Depending on the request you're making, you may want to create a secret to store your token, rather than hard coding it in your requests. Using a secret will prevent your token from being exposed, such as when using a token in a [serverless function](#serverless-functions). To store the access token as a secret: - In the terminal, run `hs secrets add secretName`. It's recommended to name the secret something simple so that you can easily reference it in the future. - Paste the access token into the terminal, then press **Enter**. You can then access your secret as an environment variable. Learn more in the [serverless functions example below](#serverless-functions). To confirm that all references to your API key have been removed, you can check the call log in your HubSpot account: - In your HubSpot account, click the **settings icon** in the main navigation bar. - In the left sidebar, navigate to **Integrations > API key**. - Review the most recent requests in the _Call log_ tab to confirm that no recent requests have been made since removing all previous references over to use your private app's access token. If you have a paid _**Marketing Hub**_ account with [marketing contacts](https://knowledge.hubspot.com/records/marketing-contacts), and you previously [set contacts created by integrations using your API key as marketing contacts](https://knowledge.hubspot.com/integrations/manage-marketing-contacts-settings-for-your-integrations#set-contacts-created-by-api-key-apps-as-marketing-contacts), you must also do the same for your private app: - In your HubSpot account, click the **settings icon** in the main navigation bar. - In the left sidebar, navigate to **Integrations > Marketing contacts**. - Under _Your connected apps_, use the search bar to locate your private app, then click the **Turn on to sync contacts as marketing contacts** switch on. Once you've finished setting up your private app and you've confirmed all references to your API key have been removed in your code, you can [deactivate the key](https://developers.hubspot.com/docs/guides/apps/authentication/intro-to-auth#deactivate-your-api-key). ## Verify requests and monitor logs Once you've removed all references to the HubSpot API key in your code and replaced them with references to your private app's access token instead, no further code changes are required. If you followed the steps above in a developer test or sandbox account, repeat the same process in your production account. Then, monitor your private app's API call logs and confirm that none of your app's requests return `400` errors: - In your HubSpot account, click the **settings icon** in the main navigation bar. - In the left sidebar menu, navigate to **Integrations** > **Private Apps**. - Click the **name** of your private app. - Click the **Logs** tab. - Any unsuccessful API request that failed due to a missing scope will appear as a `403` error. If you access the runtime logs of your integration, the response from the corresponding API request should include an error message with details about any missing scopes. - If you need to include a new scope for your private app: - Click the **Details** tab. - Click **Edit details**. - At the top of the page, click **Scopes**. - Select the **checkbox** next to any missing scopes, then click **Commit changes** in the top right when you're done. Learn more about creating and managing private apps, along with their limits, in the [private apps guide](/guides/apps/private-apps/overview). ## Implementation examples Below, learn more about common API key usages and how to migrate to private app access tokens instead. ### Serverless functions If you’re using an API key within a [serverless function](/guides/cms/content/data-driven-content/serverless-functions/overview), you can similarly use the private app’s access token for authentication. You'll need to ensure that the private app has the [scopes](/guides/apps/authentication/working-with-oauth#scopes) that the function needs to execute. To authenticate a serverless function with a private app access token: - On the _Access_ _token_ card, click **Show token** to reveal your access token. Then click **Copy** to copy the token to your clipboard. - With your access token copied, create a new secret to store the token: - In the terminal, run `hs secrets add secretName`. It's recommended to name the secret something simple so that you can easily reference it in the future. - Paste the access token into the terminal, then press **Enter**. - In your serverless function's `serverless.json` file, add the secret name to the `secrets` array: ```json // example serverless.json file { "runtime": "nodejs18.x", "version": "1.0", "secrets": ["secretName"], "endpoints": { "getPrompts": { "method": "GET", "file": "serverlessFunction.js" } } } ``` - In your serverless function's JavaScript file, set the value of the `Authorization` header to `Bearer secretName`. For example, if you're making a call to the [Contacts API](/guides/api/crm/objects/contacts) using Node.js and [axios](https://www.npmjs.com/package/axios), the request would look like the following: ```js // example serverless function const axios = require('axios'); exports.main = (context, sendResponse) => { axios.get(`https://api.hubapi.com/crm/v3/objects/contacts`, { headers: { Authorization: `Bearer ${process.env.secretName}`, 'Content-Type': 'application/json', }, }); sendResponse({ statusCode: 200 }); }; ``` Learn more about [making API calls with your app's token](/guides/apps/private-apps/overview#make-api-calls-with-your-app-s-access-token). ### One-time jobs If you’re using an API key for running one-time jobs, such as [creating a property](/guides/api/crm/properties), you can instead create a private app and use its access token to authenticate the call. Once a private app is created, you can reuse its access token for any one-time jobs, as long as the private app has the proper [scopes](/guides/apps/authentication/working-with-oauth#scopes). You can update a private app’s scopes at any time from the private app’s settings in HubSpot. Or, you can delete the private app and create a new one specific to the job you need to run. ### Create custom objects Instead of using an API key to [create a custom object](/guides/api/crm/objects/custom-objects), you can instead create a private app and use its access token to authenticate the call, as long as the app has the necessary [scopes](/guides/apps/authentication/working-with-oauth#scopes). For example, when using Postman to create a custom object, set the authorization type to **Bearer token**, then enter the token into the _Token_ field. Learn more about creating a custom object using a private app on [HubSpot's developer blog](https://developers.hubspot.com/blog/how-to-build-a-custom-object-using-private-apps). ### Custom code workflow actions If you’re using an API key in a [_Custom code_ workflow action](/reference/api/automation/custom-code-actions#create-a-custom-code-action), you can instead use the private app’s access token, as long as the app has the necessary [scopes](/guides/apps/authentication/working-with-oauth#scopes). To make the update, open the custom action in the workflow editor, then make the following updates: - First, [add a new secret](/guides/api/conversations/working-with-chatbots/code-snippets) that contains the private app access token. - Then [update the action code](/reference/api/automation/custom-code-actions#secret) with the new secret. ```js const hubspotClient = new hubspot.Client({ accessToken: process.env.secretName, }); ``` # Private apps @ Learn how to create private apps in HubSpot. $ guides & apps/private-apps --- # Private apps Private apps allow you to use HubSpot's APIs to access specific data from your HubSpot account. You can authorize what each private app can request or change in your account, which will generate an access token that's unique to your app. You must be a [super admin](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#super-admin) to access private apps in your HubSpot account. Private apps do not support custom timeline events. Webhooks are [supported in private apps](/guides/apps/private-apps/create-and-edit-webhook-subscriptions-in-private-apps), but subscriptions cannot be edited programmatically via an API, and must instead be edited in your private app settings. If you plan on building an app using custom timeline events, you should create a public app instead. Learn more about the [differences between private and public apps](/guides/apps/overview). ## Create a private app - In your HubSpot account, click the **settings icon** in the main navigation bar. - In the left sidebar menu, navigate to **Integrations** > **Private Apps**. - Click **Create private app**. - On the _Basic Info_ tab, configure the details of your app: - Enter your app's **name**. - Hover over the placeholder logo and click the **upload icon** to upload a square image that will serve as the logo for your app. - Enter a **description** for your app. - Click the **Scopes** tab. - At the top of the page, click **Add new scope**. - In the right panel, select the **checkbox** for each scope you want your private app 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](/guides/apps/authentication/scopes). - Click **Update** when you're done adding scopes. If you later decide that your app requires additional scopes, you can also configure them after your app is created. - Review the scopes you've selected for your app. If you decide your app 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 app's scopes and the associated access granted for each one. - To subscribe to events triggered by changes to CRM objects in your account, you can set up webhook subscriptions for your private app: - At the top of the page, Click the **Webhooks** tab. - Under _Target URL_, enter the **URL** that HubSpot will make a `POST` request to when events trigger. - Click **Create subscription**. - In the right panel, select the **object types** that you want to subscribe to, then select the **events** associated with those objects (e.g., created, merged, deleted, etc.) that will trigger HubSpot to send a request to the endpoint you configured. - If you select an object type that requires additional scopes you haven't authorized, you'll be prompted to add those scopes to your app. - If you select _Property changed_ for the event type, you can then select any of the associated object properties that you want to subscribe to changes for. - Click **Subscribe**. - If you don't want your webhook subscriptions to be active immediately, or if you want to delete a subscription you mistakenly created, you can hover over the webhook and manage its status, unsubscribe to delete it, or review the subscription details. Learn more about using webhook subscriptions in a private app in [this article](/guides/apps/private-apps/create-and-edit-webhook-subscriptions-in-private-apps). - After you're done configuring your app, click **Create app** in the top right. - In the dialog box, review the info about your app's access token, then click **Continue creating**. Once you've created your app, you can start making API calls using the app's access token. If you need to edit your app's info or change its scopes, click **Edit app** in the top right of the app details page. ## Make API calls with your app's access token Private apps will lose access to scopes when your HubSpot account is downgraded and loses access to functionality. For example, if your account does not have access to HubDB, your private app will not have access to the HubDB scope. Private app access tokens will be updated to reflect available scopes in your HubSpot account and what you configured for the private app, but the token string itself will not change. To start making API calls, navigate to the details page of your app. - In your HubSpot account, click the **settings icon** in the main navigation bar. - In the left sidebar menu, navigate to **Integrations** > **Private apps**. - Click the **name** of your app. - Click the **Auth** tab, click **Show token** to reveal your access token. Click **Copy** to copy the token to your clipboard. - You can then paste the access token to provide it to your developers, or use it yourself as you develop your app. When making a call to one of the HubSpot API endpoints, set the value of the _Authorization_ field to **Bearer \[YOUR_TOKEN\]**. For example, if you're making a call to the [Contacts API](/guides/api/crm/objects/contacts) using Node.js and [axios](https://www.npmjs.com/package/axios), the request would look like the following: ```js axios.get( 'https://api.hubapi.com/crm/v3/objects/contacts', { headers: { Authorization: `Bearer ${YOUR_TOKEN}`, 'Content-Type': 'application/json', }, }, (err, data) => { // Handle the API response } ); ``` - Private app access tokens are implemented on top of OAuth, so you can also make authenticated calls with your access token using one of HubSpot's client libraries. For example, if you're using the [Node.js client library](https://github.com/HubSpot/hubspot-api-nodejs), you can instantiate an OAuth client by passing in your app's access token: ```js const hubspotClient = new hubspot.Client({ accessToken: YOUR_ACCESS_TOKEN }); ``` If you [remove the user](https://knowledge.hubspot.com/user-management/deactivate-hubspot-users) who originally created a private app, any API calls that previously used the app's access token will fail with a `result` of `"USER_DOES_NOT_HAVE_PERMISSIONS"`. If this issue occurs, it's recommended that you follow [the instructions in the section below](#rotate-your-access-token) to rotate your access token. If you mistakenly removed the user from your account, [adding them back](https://knowledge.hubspot.com/account-management/add-and-remove-users) will also fix the issue. ## View private app access token information To view information about a private app's access token, such as the Hub ID and scopes associated with the token, make a `POST` request to `/oauth/v2/private-apps/get/access-token-info`. In the request body, include your access token: ```json // POST request response body { "tokenKey": {{accessToken}} } ``` The response will include information about the user who created the token, the Hub ID of the account, the private app ID, and the scopes associated with the token. ```json // Example response for GET request to /oauth/v2/private-apps/get/access-token-info { "userId": 123456, "hubId": 1020304, "appId": 2011410, "scopes": ["oauth", "crm.schemas.companies.write"] } ``` ## Rotate your access token If you access token is lost or otherwise compromised, you can rotate the token. A new access token will be created and the original access token will expire. - In your HubSpot account, click the **Settings** page in the main navigation bar. - Navigate to **Integrations** > **Private Apps**. - Click the **name** of your private app. - Next to your access token, click **Rotate**: - If your token is compromised and you want to immediately revoke access, click **Rotate and expire now**. - If there's no imminent threat to your token, it's still recommended that you rotate your token every six months. If you're ready to initiate a regular rotation of your token, click **Rotate and expire later**, which will trigger an expiration of the token in 7 days. - If your app 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 token and revoke the new access token. HubSpot will also send email notifications to [super admins](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#super-admin) with reminders about access token rotation status, as well as other related alerts. Super admins in your HubSpot account will receive notifications for the following events and reminders: - A super admin initiated a rotation (either immediately or scheduled for 7 days from now). - A super admin canceled a pending rotation. - A super admin opted to expire an access token immediately by clicking **Expire now** instead of waiting 7 days for the token to expire. - The app's access token is about to expire in 24 hours. - The app's access token has been rotated and expired after 7 days. - If you haven't rotated your access token in over 180 days, super admins will also receive a reminder email to rotate your app's access token. ## View API call logs To review the API calls your app has made in the past 30 days: - On the details page of your app, click the **Logs** tab. - Review and filter your private app API calls: - Click the **Method** and **Response** dropdown menus to filter your historical API calls by request method or response code. - Click the **start date** or **end date** dropdown menus to narrow your call logs to a specific time range. - You can also search for specific calls by URL in the _Search by request URL_ search box. HubSpot does not store the request body, request URL parameters, or response payload of successful API calls in the logs for private apps. If you want to track this data, or you want to store log data from more than 30 days ago, it's recommended that you maintain an external log of your app's historical calls. - To export the API call logs, click **Export logs (CSV)**. Then, specify a **date range** (up to the past 30 days) and click **Export**. - In the pop-up box, select the **date range** of API calls you want to export and click **Export**. You will receive an email with a download link when the export is ready. ## Private app limits You can create up to 20 private apps in your HubSpot account. Each private app is subject to [HubSpot's API usage guidelines](/guides/apps/api-usage/usage-details). The number of calls your private app can make is based on your account subscription and whether you've purchased the API add-on: | | Product Tier | Per 10 Seconds | Per Day | | --- | --- | --- | --- | | Private Apps | (Any Hub)Free and Starter | 100 / private app | 250,000 / account | | | (Any Hub)Professional | 190 / private app | 625,000 / account | | | (Any Hub)Enterprise | 190 / private app | 1,000,000 / account | | Private Apps with [API Limit Increase](https://legal.hubspot.com/hubspot-product-and-services-catalog#Addons) | (Any Hub)Free, Starter, Professional, and Enterprise | 200 / private app | 1,000,000 / account on top of your base subscription, for each limit increase. You can purchase a maximum of two API limit increase. | If you have both a Starter and Professional plan, limits for the higher tier (Professional) apply to your account. You can make a `GET` request to `/account-info/v3/api-usage/daily/private-apps` to review the daily API usage for all private apps in your HubSpot account. Learn more about using the [account information API](/guides/api/settings/account-information-api). Purchasing an API Limit Increase will increase the maximum number of requests you can make to the [associations API](/guides/api/crm/associations/associations-v4) to 1,000,000 daily requests and 200 requests per 10 seconds, but these limits <u>cannot</u> be increased further with an additional API Limit Increase purchase. ## Delete a private app When you delete one of your private apps, its access token will be permanently revoked and you'll no longer be able to use it to make API calls. To delete an app: - In your HubSpot account, click the **settings icon** in the main navigation bar. - Click the **name** of your private app. - At the top of the page, click the **Auth** tab. - At the bottom of the page, click **Delete \[name of your app\]**. - In the dialog box, type the name of your app to confirm its deletion, then click **Delete**. # Developer tools | Create a settings page for your app @ Learn how to create a settings page for your app. $ guides & apps/public-apps --- # Create a settings page for your app Create a settings page for your public app to give users more control over how they use your app in their HubSpot account. Developers can create a settings page for their app using the settings builder so that users can access the settings from the [_Connected Apps_](https://knowledge.hubspot.com/integrations/connect-apps-to-hubspot) page in HubSpot. To create the app settings page: - In your [developer account](/getting-started/account-types), click **Apps**. - Click the **app** you want to create a settings page for. - In the left sidebar menu, click **App settings**. - To create a settings page from a template, click **Build from a template**. - To create a settings page from scratch, click **Start from scratch**. You'll then be brought to the app settings page builder. If you select the _Build from a template_ option, ensure that all placeholder control component URLs are updated to your own API endpoints. There are two tabs on the settings page that you can customize: - On the _General settings_ tab, users can interact with selected components such as buttons and toggles to configure their app settings. - On the _Feature discovery_ tab, add cards highlighting your app’s features, explain how the features work, and link to further documentation or resources. The app settings builder is organized around three sections: accounts, controls, and feature cards. Below, learn how to set up the following: - An account component to enable users to configure user-specific settings. - Control components to enable users to configure settings with buttons, dropdown menus, and more. - Feature cards to provide context to users as they configure their settings. ## Set up the account component App settings can be uniquely configured for each user. The account component is a dropdown menu of all users in the connected HubSpot account. When viewing the settings page, a user can select any user in the account to view and set user-specific settings for the app. The account component is required and only one component can be added per app. For an account component to work: - That account component must have a URL for fetching account data. - The account data fetched from the URL must include a list of connected users for the integration. - The `accountID` returned to HubSpot is what’s passed on to the control and feature card components so you’ll know which account HubSpot is interacting with. The **account** **URL** is required to populate a list of user accounts on the customer’s app setting page. Users can then pick any of the accounts listed. Whichever account is selected will be sent on future action calls for toggles, buttons, and other action types. This allows the use of multiple account configurations in a single account that installs your app. You should use all the parameters HubSpot sends to determine which account you are making future changes for. HubSpot will send a `X-HubSpot-Signature` header to verify that the request came from HubSpot. Read more about [request signatures](/guides/apps/authentication/validating-requests) for details. To add an account component: - Create an endpoint that’s configured to handle either a `GET` or `POST` request from HubSpot. This will be called to populate a list of user accounts on the customer’s app settings page. - If you select `GET` as the HTTP method for your endpoint, the fields will be included as query params. For example: ```json https://api.hubapi.com/application/settings/test/v1/actions/accounts-fetch?actionType=ACCOUNTS_FETCH&appId=999&portalId=1234&userId=1001&userEmail=test%40example.com ``` - If you select `POST` as the HTTP method, the fields will be included in the request body as a JSON object. For example: ```json // Request body for POST request to /settings-account-fetch { “actionType”: “ACCOUNTS_FETCH”, “appId”: 123, “portalId”: 456 “userId”: 1 “userEmail”: “test@example.com”, } ``` - In the request handler for your app’s endpoint, you should return a JSON object to HubSpot that includes a response field, which contains the `accountId`, `accountName` of the user, along with an optional `accountLogoUrl`. ```json { "response": { "accounts": [ # Object[] REQUIRED { "accountId": "abc123", # String REQUIRED "accountName": "Joe Cool", # String REQUIRED "accountLogoUrl": # Url OPTIONAL } ] } } ``` ## Set up the control component You can set up controls for users such as buttons, toggles, or a dropdown menu to manage general app settings and functions in the control component. Controls are optional. If you choose to add a control, each control must contain a short header and description describing its purpose or function. To add a component, populate the following fields: - **Header**: what the control does. - **Description**: a description of the control. - **Control type**: select from three control types: button, toggle, or dropdown menu. HubSpot will send a `X-HubSpot-Signature` header to verify that the request came from HubSpot. Read more about [request signatures](/guides/apps/authentication/validating-requests) for details. HubSpot will attempt to parse responses to actions as JSON and look for a message property. This message will be displayed to the user on either success or failure. Response status code of 2xx will show a success message and status codes of 4xx or 5xx will show an error message. An example response may look similar to the following: ```json // Example GET to https://api.hubapi.com/application/settings/test/v1/actions/toggle-update { "message": "You have successfully enabled your data sync" } ``` ### Add a button control type Use a button to initiate an action via a call to your API endpoint. You will supply the API endpoint and choose the request method (either `PUT`, `POST` or `DELETE`). While no data is returned, HubSpot will communicate the resulting success, failure, or timeout status of the action to the customer. You should use all parameters HubSpot sends to determine which account you are making changes for. To add a button control type:: - Select a **button** control type. - Click **Configure button behavior**. - Select **Hit an external endpoint**. - In the **Button text** field, enter text for the button. This should tell the customer what it does. - Create an endpoint that’s configured to handle either a `PUT`, `POST` or `DELETE` request from HubSpot. HubSpot will provide the associated identifying fields for the customer based on the HTTP method you select when configuring your button. - The fields will be included in the request body as a JSON object. For example: ```json curl -i -X POST \ -H 'Content-Type: application/json' \ -d ' { "actionType": "BUTTON_UPDATE", "appId": 999, "portalId": 1234, "userId": 1001, "userEmail": "jondoe@example.com", "accountId": "jondoe@example.com" } ' \ 'https://api.hubapi.com/application/settings/test/v1/actions/button-update' ``` - In the request handler for your app’s endpoint, you should return a JSON object to HubSpot. This should include a response field along with an optional message to the user. For example: ```json { "response": null, # Ignored for beta "message": "Custom message to user" # Optional } ``` ### Open an iframe with a button control type You can also use a button to open a modal displaying an iframe. To do so: - Select a **button** control type. - Click **Configure button behavior**. - Select **Open an external URL within an iframe**. - In the **Button text** field, enter text for the button. This should tell the customer what it does. - Select the **HTTP Method** (`GET` or `POST`). - Enter the **Endpoint URL**. <u>Do not</u> input the iframe URL. - If you select `GET` as the HTTP method for your endpoint, the fields will be included as query parameters. For example: ```json https://api.hubapi.com/application/settings/test/v1/actions/iframe-fetch?actionType=IFRAME_FETCH&appId=999&portalId=1234&userId=1001&userEmail=test%40example.com&accountId=joedoe@example.com ``` - If you select `POST` as the HTTP method, the fields will be included in the request body as a JSON object. For example: ```json curl -i -X POST \ -H 'Content-Type: application/json' \ -d ' { "actionType": "IFRAME_FETCH", "appId": 999, "portalId": 1234, "userId": 1001, "userEmail": "jondoe@example.com", "accountId": "jondoe@example.com" } ' \ 'https://api.hubapi.com/application/settings/test/v1/actions/iframe-fetch' ``` - In the request handler for your app’s endpoint, you should return a JSON object to HubSpot. This should include a response field containing the `iframeURL`: ```json { "response": { "iframeUrl": "https://en.wikipedia.org/wiki/Flywheel" } } ``` All iframe modals will open from within your app’s settings page. ### Add a toggle control type The toggle control type provides users with a toggle option that they can turn on or off. HubSpot retrieves a toggle’s current state from the `TOGGLE_FETCH` URL. When a user clicks the toggle, HubSpot calls the `TOGGLE_UPDATE` URL to send the toggle’s new state. Other identifiers such as `accountId` and `portalId` are also passed – this allows you to identify the user triggering the toggle. To add a toggle: - Select a **Toggle** control type. - Click **Configure toggle behavior**. - In the _Toggle settings_ pop-up, set up the toggle status and update toggle sections. - If you’re fetching the toggle status: - Create an endpoint that’s configured to handle either a `GET` or `POST` request from HubSpot, which will be called to fetch the existing toggle state for a customer when they visit the settings page for your app. - If you select `GET` as the HTTP method for your endpoint, the fields will be included as query parameters. For example: ```json https://api.hubapi.com/application/settings/test/v1/actions/toggle-fetch?actionType=TOGGLE_FETCH&appId=999&portalId=1234&userId=1001&userEmail=test%40example.com&accountId=jondoe%40example.com ``` - If you select `POST` as the HTTP method, the fields will be included in the request body as a JSON object. For example: ```json curl -i -X POST \ -H 'Content-Type: application/json' \ -d ' { "actionType": "TOGGLE_FETCH", "appId": 999, "portalId": 1234, "userId": 1001, "userEmail": "jondoe@example.com", "accountId": "jondoe@example.com", "enabled": false } ' \ 'https://api.hubapi.com/application/settings/test/v1/actions/toggle-fetch' ``` - In the request handler for your app’s endpoint, you should return a JSON object to HubSpot that includes a response field, which contains the current enabled state of the toggle as a boolean, along with an optional message to the user. For example: ```json { "response": { "enabled": true }, "message": "Optional custom message to user" } ``` - To configure the toggle when it’s turned on or off: - Create an endpoint that’s configured to handle either a `POST` or `PUT` request from HubSpot, which will be called when the toggle is turned on or off. - The fields will be included in the request body as a JSON object. For example: ```json curl -i -X POST \ -H 'Content-Type: application/json' \ -d ' { "actionType": "TOGGLE_UPDATE", "appId": 999, "portalId": 1234, "userId": 1001, "userEmail": "jondoe@example.com", "accountId": "jondoe@example.com", "enabled": false } ' \ 'https://api.hubapi.com/application/settings/test/v1/actions/toggle-update' ``` - In the request handler for your app’s endpoint, you should return a JSON object to HubSpot that includes a response field, which contains the current enabled state of the toggle as a boolean, along with an optional message to the user. For example: ```json { "response": {"enabled": false}, # Boolean REQUIRED "message": "Custom message to user" # Optional } ``` ### Add a dropdown control type The dropdown control type provides the customer with a list of options they can select from. HubSpot retrieves the list from the `DROPDOWN_FETCH` URL. Your API may also return a `selectedOption` field to display which option the user has selected, as well as a placeholder field signifying what text to display when no option is selected. By passing the `accountId`, `portalId`, and other identifiers, you can identify which user triggered the control. When a user clicks on an option in the dropdown menu, HubSpot calls the `DROPDOWN_UPDATE` URL to send the same identifiers along with the selected option. To add a dropdown menu: - Select the **dropdown** control type. - Click **Configure dropdown behavior**. - In the _Dropdown settings_ pop-up box, configure the _Dropdown fetch_ and _Dropdown update_ sections. - To fetch the list of dropdown options: - Create an endpoint that’s configured to handle either a `GET` or `POST` request from HubSpot, which will be called to fetch the list of dropdown options from the app settings page. - If you select `GET` as the HTTP method for your endpoint, the fields will be included as query parameters. For example: ```json // GET request to /dropdown-fetch https://api.hubapi.com/application/settings/test/v1/actions/dropdown-fetch?actionType=DROPDOWN_FETCH&appId=123&portalId=456&userId=1&userEmail=jkurien@hubspot.com&accountId=jkurien@hubspot.com ``` - If you select `POST` as the HTTP method, the fields will be included in the request body as a JSON object. For example: ```json curl -i -X POST \ -H 'Content-Type: application/json' \ -d ' { "actionType": "DROPDOWN_FETCH", "appId": 999, "portalId": 1234, "userId": 1001, "userEmail": "jondoe@example.com", "accountId": "jondoe@example.com", "enabled": false } ' \ 'https://api.hubapi.com/application/settings/test/v1/actions/dropdown-fetch' ``` - In the request handler for your app’s endpoint, you should return a JSON object to HubSpot that includes a response field, which contains the current enabled state of the toggle as a boolean, along with an optional message to the user. For example: ```json { "response": { "options": [ { "text": "Option 1", "value": "option1" }, { "text": "Option 2", "value": "option2" } ], "selectedOption": "option2" # optional "placeholder": "Choose an Option" # optional }, } ``` - To configure what happens when an option is selected from the dropdown menu: - Create an endpoint that’s configured to handle either a `POST` or `PUT` request from HubSpot. The fields will be included in the body as a JSON object: ```json curl -i -X POST \ -H 'Content-Type: application/json' \ -d ' { "actionType": "DROPDOWN_UPDATE", "appId": 999, "portalId": 1234, "userId": 1001, "userEmail": "jondoe@example.com", "accountId": "jondoe@example.com", "selectedOption": "option1" } ' \ 'https://api.hubapi.com/application/settings/test/v1/actions/dropdown-update' ``` - In the request handler for your app’s endpoint, you should return a JSON object to HubSpot that includes a response field, which contains the current enabled state of the toggle as a boolean, along with an optional message to the user. For example: ```json { "response": { "actionType": "DROPDOWN_UPDATE", "selectedOption": "option2" "message": "null" }, } ``` ## Set up feature cards Use feature cards to introduce, describe, and help users set up app features. You can add up to <u>six feature cards</u> per app. To add a feature card: - Set up the following fields for the card - **Card header:** enter a **header** – this is the title of a feature. - **Description**: enter a **description** for the feature. - **CTA button text**: enter **text** for the CTA button. - **CTA button URL**: enter a **URL.** It is recommended to have the CTA direct users to where the feature appears in HubSpot. - **Illustration:** select an illustration for the feature card. - **Add image or video:** provide a preview carousel of the feature to your users. - Upload an image or video file. You can upload up to six images or videos. - Add a **description** of the image or video. - Once you’ve set up all the fields, click **Add to settings.** Once you’ve added all the components to the settings page builder, click **Deploy changes** in the upper-right to deploy the settings page to all users. ## Restore a previous version of an app settings page The app settings builder supports versioning and allows you to restore previously deployed settings pages. To do so: - Click the **Version history** tab. - Next to the version you want to deploy, click **Restore**. The selected version of the app settings page will load in the builder. - From there, you can make new edits to the settings page and deploy it. ## Test your app settings page To test your app’s settings page, set up a [test account](/getting-started/account-types) from your developer account, then install the app on that test account. The app settings page will load on the test account’s connected app. HubSpot also logs all requests made to URLs in the app settings page. To view this request log: - In your developer account, click **Apps**. - Click the **app** you want to create a settings page for. - In the left sidebar menu, click **Monitoring**. - Click the **App settings** tab. Clicking buttons or toggles will make calls to the URLs you have configured, so ensure that you are using a true developer test account. # Create generic webhook subscriptions (BETA) @ Learn how to create and manage generic webhook subscriptions (BETA) $ guides & apps/public-apps --- # Create and manage generic webhook subscriptions (BETA) Generic webhooks allow you to subscribe to events in a [public app](/guides/apps/public-apps/overview) or [private app](/guides/apps/private-apps/overview) to listen and react to changes to any standard CRM object (e.g., contacts, companies, etc), engagement (call, engagement, etc.), or other objects in your account. This feature is currently in beta and is subject to change based on testing and feedback. By using this functionality, you agree to adhere to [HubSpot's Developer Terms](https://legal.hubspot.com/hs-developer-terms) & [Developer Beta Terms](https://legal.hubspot.com/hubspot-beta-terms). You also acknowledge the risk associated with testing an unstable API. ## Supported object types The following object types are currently supported when creating generic webhook subscriptions, subject to the restrictions in the alert below: - Creating `EMAIL` subscriptions for the `hs_email_html` and `hs_email_subject` properties is not currently supported. - Creating `COMMUNICATION` subscriptions for the `hs_communication_body` property is not currently supported. - You can also create generic webhook subscriptions for custom objects, but only when developing a private app. - Generic webhook subscriptions are not currently supported for `conversation.*` and `contact.privacyDeletion` events. If you want to subscribe to these events, you should use the old format for webhooks detailed in [this article](/guides/api/app-management/webhooks#webhook-subscriptions). ## Create a generic webhook in a private app To create a generic webhook subscription in a private app: - In your HubSpot account, click the **settings icon** in the main navigation bar. - In the left sidebar menu, navigate to **Integrations** > **Private Apps**. - Click the **name** of your private app. If you haven't created a private app yet, follow the instructions in [this article](/guides/apps/public-apps/overview) to create one. - In the top right, click **Edit app**. - Click the **Webhooks** tab. - Under _Target URL_, enter the **URL** that HubSpot will make a `POST` request to when an event triggers. - Click **Create subscription**. - In the right panel, configure your subscription: - Click to toggle the **Use expanded object support** switch on. - Select the **object types** that you want to subscribe to, then select the **events** associated with those objects (e.g., created, merged, deleted, etc.) that will trigger HubSpot to send a request to the endpoint you configured. - If you select an object type that requires scopes your app hasn't been authorized for, you'll be prompted to add those scopes to your app. - If you select _Property changed_ for the event type, you can select any of the associated object properties that you want to subscribe to changes for. - Click **Subscribe**. - If you don't want your webhook subscriptions to be active immediately, or if you want to delete a subscription you mistakenly created, you can hover over the webhook and manage its status, unsubscribe to delete it, or review the subscription details. If you've configured multiple webhook subscriptions, you can edit their statuses in bulk by selecting the **checkboxes** next to each one then clicking **Activate** or **Pause**. - When you're ready to save your changes, click **Commit changes** in the top right. After you've configured your webhook subscriptions, HubSpot will begin sending `POST` requests to your _Target URL_ once any of the events associated with your active subscriptions are triggered. For each private app, HubSpot sets a concurrency limit of 10 requests when sending subscription event data. This concurrency limit is the maximum number of in-flight requests that HubSpot will attempt at a time. Each request can contain up to 100 events. ## Create a generic webhook in a public app To create a generic webhook subscription in your HubSpot developer account: - In your developer account, navigate to the **Apps** dashboard. - Click the **name** of an app. - In the left sidebar menu, navigate to **Webhooks**. - Under _Target URL_, enter the **URL** that HubSpot will make a `POST` request to when events trigger. - Click **Create subscription**. - In the right panel, configure your subscription: - Select the **object types** that you want to subscribe to, then select the **events** associated with those objects (e.g., created, merged, deleted) that will trigger HubSpot to send a request to the endpoint you configured. - - If you select _Property changed_ for the event type, you can then select any of the associated object properties that you want to subscribe to changes for. - Click **Subscribe**. - After you've created your subscription, hover over the associated object type and click **View subscriptions**. When you're ready to activate the subscription, hover over the event subscription and click **Activate**. After you've configured your webhook subscriptions, HubSpot will begin sending `POST` requests to your _Target URL_ once any of the events associated with your active subscriptions are triggered in an account that's installed your app. ## Parse generic webhook payloads The endpoint at the target URL that you specify in your app's webhooks settings will receive `POST` requests containing JSON formatted data from HubSpot. To ensure that the requests you're getting at your webhook endpoint are actually coming from HubSpot, HubSpot populates a `X-HubSpot-Signature` header with a SHA-256 hash built using the client secret of your app combined with details of the request. Learn more about [validating request signatures](/guides/apps/authentication/validating-requests). For generic webhook payloads, the subscriptionType will follow the format of `object.*` (e.g., `object.propertyChange` instead of the previous format that was specific to each object type like `contact.propertyChange`). For creation, deletion, merge, restore, and property change events, the associated object type will be provided in the `objectTypeId` field of the payload, as defined in the table [here](/guides/api/crm/understanding-the-crm#object-type-id). The tables below define the fields provided in different event payloads. ### General The following fields will appear in all webhook event payloads: | Field | Description | | --- | --- | | `objectId` | The ID of the object that was created, changed, or deleted. For example, for contact event changes this is the contact ID; for companies, the company ID; for deals, the deal ID; and for conversations the [thread ID](/guides/api/conversations/inbox-and-messages). | | `eventId` | The ID of the event that triggered this notification. This value is not guaranteed to be unique. | | `subscriptionId` | The ID of the subscription that triggered a notification about the event. | | `portalId` | The specific [HubSpot account ID](https://knowledge.hubspot.com/account-management/manage-multiple-hubspot-accounts#check-your-current-account) where the event occurred. | | `appId` | The ID of your public app or private app. This is used in case you have multiple applications pointing to the same webhook URL. | | `label` | When this event occurred as a unix timestamp (in milliseconds). | | `attemptNumber` | Starting at 0, which number attempt this is to notify your service of this event. If your service times-out or throws an error as describe in the _Retries_ section below, HubSpot will attempt to send the notification again. | | `changeSource` | The source of the change. This can be any of the change sources that appear in contact property histories. | | `subscriptionType` | The webhook subscription type. This can be one of the following types: `object.creation`, `object.deletion`, `object.merge`, `object.restore`, `object.propertyChange`, or `object.associationChange`. | | `objectTypeId` | The type of the object that triggered the event. See the [table in this article](/guides/api/crm/understanding-the-crm#object-type-ids) for a full reference of each `objectTypeId`. | | `sourceId` | The user or source of the triggered webhook event (e.g., a user in the HubSpot account updating a value). | ### Property change events The following fields are specifically included in the payload of `object.propertyChange` events: | Field | Description | | --- | --- | | `propertyName` | The name of the object property that changed (e.g., the `city` property for a contact). | | `propertyValue` | The new value of the property. | ### Merge events The following fields are specifically included in the payload of `object.merge` events: | Field | Description | | --- | --- | | `primaryObjectId` | The ID of the merge winner, which is the record that remains after the merge. In the HubSpot merge UI, this is the record on the right. | | `mergedObjectIds` | An array of IDs that represent the records that are merged into the merge winner. In the HubSpot merge UI, this is the record on the left. | | `newObjectId` | The ID of the record that is created as a result of the merge. This is separate from `primaryObjectId` because in some cases a new record is created as a result of the merge. | | `numberOfPropertiesMoved` | An integer representing how many properties were transferred during the merge. | ### Association events The fields below are specifically included in the payload of `object.associationChange` events. | Field | Description | | --- | --- | | `fromObjectTypeId` | The ID of the object you're associating. To find the ID values, refer to this [list of object type IDs,](/guides/api/crm/understanding-the-crm#object-type-id) or for contacts, companies, deals, tickets, and notes, you can use the object name (e.g., `contact`, `company`). | | `toObjectTypeId` | The ID of the object you're associating the record to. To find the ID values, refer to this [list of object type IDs,](/guides/api/crm/understanding-the-crm#object-type-id) or for contacts, companies, deals, tickets, and notes, you can use the object name (e.g., `contact`, `company`). | | `associationTypeId` | The type of association you want to create. A full list of association types is provided [here](/guides/api/crm/associations/associations-v4#association-type-id-values). | | `associationCategory` | Whether the association type was created by HubSpot (`HUBSPOT_DEFINED`) or by a user (`USER_DEFINED`). | Association change events will trigger on both incoming and outgoing associations. This means that an `object.associationChange` for an `objectName` of `contact` will trigger on a `DEAL_TO_CONTACT` association change with `toObjectTypeId: 0-1` and the corresponding `associationTypeId` in the payload. - The `associationChange` event is not currently supported for custom objects in private apps. - Associations with user-defined labels are not currently supported with generic webhook subscriptions. # @ Learn how to import users into any HubSpot account that has installed your app. $ guides & apps/public-apps --- # Import users into any HubSpot account from an external app Learn how to set up the _User import source_ feature to register your public app as a data source for bulk user imports. This means customers who have installed your app in their HubSpot account can use it to bulk import users. This feature is only available for [public apps](/guides/apps/public-apps/overview). ## Set up the User import source feature Before you configure your app to use the _User import source_ feature, you'll need to set up and host two API endpoints that will accept `POST` requests from HubSpot when a customer who's installed your app triggers a bulk user import. One endpoint you set up will be to return account data, while the other will return user data. The specifications for each endpoint are detailed in the sections below. Requests are sent with the HubSpot Signature. To ensure that the requests that are received are from HubSpot, they can be validated using the signature: [https://developers.hubspot.com/docs/api/webhooks/validating-requests](/guides/apps/authentication/validating-requests) ## Endpoint to return account data The URL of the endpoint that returns account data should be structured like `https://BASE_PATH_OF_YOUR_SERVICE/feature/USER_IMPORT/event/FETCH_ACCOUNTS`. HubSpot wil provide the following fields in the body of the request: ```json // Example request body HubSpot will send to your hosted service { "portalId": 10, "appId": 20 } ``` The response you return when HubSpot makes a `POST` request to the endpoint should be a JSON-formatted object with the following fields: ```json { "portalId": 10, "appId": 20, "eventType": "FETCH_ACCOUNTS", "payload": { "accounts": [ { "id": "testId1", "name": "testAccount1" }, { "id": "testId2", "name": "testAccount2" } ] } } ``` ## Endpoint to return user data The URL of the endpoint that returns account data should be structured like `https://BASE_PATH_OF_YOUR_SERVICE/feature/USER_IMPORT/event/FETCH_USERS`. HubSpot will provide the following fields in the body of the request: ```json // Example request body that HubSpot will send to your hosted service { "portalId": 10, "appId": 20 } ``` The response you return when HubSpot makes a `POST` request to the endpoint should be a JSON-formatted object with the following fields: ```json { "portalId": 10, "appId": 20, "eventType": "FETCH_USERS", "payload": { "users": [ { "id": "testUser1", "email": "testuser1-invalid@hubspot.com", "firstName": "Test", "lastName": "User-One", "existingUserId": null }, { "id": "testUser2", "email": "testuser2-invalid@hubspot.com", "firstName": "Test", "lastName": "User-Two", "existingUserId": null }, { "id": "testUser3", "email": "testuser3-invalid@hubspot.com", "firstName": "Test", "lastName": "User-Three", "existingUserId": null } ] } } ``` Once you've set up your two endpoints, you can configure your app settings. - In your app developer account, navigate to **apps** in the main navigation bar. - Click the **name** of the app. - In the left sidebar, click **More features**. - In the _Target URL_ section, enter the **URL** that HubSpot will make a `POST` request to when events trigger. Or, use this testing URL: `https://api.hubspot.com/integration-components-example/v1/sample-app-webhooks/beta-app` - Click to toggle the **User import source** switch on. - Click **Save changes**. ## Test the functionality After setting up your two endpoints and configuring your app settings, you can test your endpoints out: - [Install your app](/guides/apps/public-apps/overview). - In your HubSpot account, click the **settings icon** in the top navigation bar. - In the left sidebar menu, navigate to **Users & Teams**. - Click **Create user**. - In the _Or create multiple users at once_ section, click your **app** to begin the import process. - Follow the steps in [this article](https://knowledge.hubspot.com/account-management/add-and-remove-users#create-users) to import and set up your users. # Public apps @ Learn how to create and install an app in HubSpot. Apps are integrations that can be installed on customer accounts through an OAuth connection process. $ guides & apps/public-apps --- # Public apps In HubSpot, a public app is a type of integration that can be installed on customer accounts or listed on the App Marketplace. It requires authentication via [OAuth](/guides/apps/authentication/working-with-oauth). Once a user [installs](#installing-apps) your app on their HubSpot account, you’ll be able to make API calls to that account using an [OAuth access token](/guides/api/app-management/oauth-tokens). Your app will also appear in the account’s _Connected Apps_ settings. Connected apps are also able to take advantage of [subscribing to changes using webhooks](/guides/api/app-management/webhooks) and creating custom [timeline events](/guides/api/crm/extensions/timeline). Below, learn how to: - [Create a public app](#create-a-public-app) - [Install a public app in an account](#install-an-app) - [Manage the app, including monitoring usage](#manage-public-apps-in-hubspot) - [Add a verified domain to the app](#add-a-verified-domain) ## Create a public app When you create a public app in HubSpot, you're essentially associating an app you've built with an [app developer account](https://app.hubspot.com/signup-hubspot/developers). To get started with creating your public app: - Navigate to your app developer account: - To create a new app developer account, click [here](https://developers.hubspot.com/get-started), then click **Create App Developer account**. - If you already created an app developer account, navigate to the [account selection page](https://app.hubspot.com/myaccounts-beta), then locate and click your **account.** - In your app developer account, navigate to **Apps** in the main navigation bar. - In the upper right, click **Create app**. - Next, enter your **App name**, and provide other basic information about your app, including a **description** and **logo**. When users authenticate your app with their HubSpot account, they’ll see the name, description, logo, and any support contact info you provide on this page. The app name will be used wherever your app displays in HubSpot. This includes when installing the app as well as the _Powered by_ footer for [CRM cards](/guides/api/crm/extensions/crm-cards) and [timeline events](/guides/api/crm/extensions/timeline). - Click the **Auth** tab. Your app credentials (i.e., your _App ID_, _Client ID_, and _Client secret_) as well as your _Sample install URL_ are <u>not</u> accessible while your app is in a draft state, and will only be available after you create your app. - Enter a **Redirect URL** for your app. Users will be redirected to this URL after granting access and the associated account permissions to your app. If you're just getting started and you don't have an official redirect URL ready yet, you can also use `http://localhost` for local development as you build out and test your app. Next, you'll configure the scopes your app will require. ### Configure scopes Scopes determine your app's permissions to access data sources or tools in an account that's installed your app. The scopes you configure will appear as the `scope` and `optional_scope` query parameters in an install URL that you can then provide to users. #### Scope types On the _Auth_ tab, 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 <u>and</u> must be present in the `scope` query parameter in your app's install URL for successful installation. - **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 <u>not</u> required to successfully install your app. These scopes are specified 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](/guides/api/crm/objects/custom-objects) data (which is only available to _Enterprise_ HubSpot accounts), you could add the `crm.objects.custom.read` scope as an optional scope. Then, if an account has access to the custom objects, the scope will be authorized. Otherwise, they’ll still be able to install the app without the custom objects scope. #### Add scopes To customize your scope settings and add new scopes: - In the _Scopes_ section, click **Add new scope**. - In the right panel, use the **search bar** to search for a scope, then select the **checkbox** next to any scope you want the user to authorize. Click the **dropdown menu** next to the scope and select a **scope type**. - Click **Update**. - Review your configured scopes. If you turned on advanced scope settings, you can switch the scope type of any scope by clicking the **dropdown menu** next to the scope. You can also click **Delete** to remove one of your app's scopes. - Once you've finished setting up your app settings, click **Create app** in the bottom left. With your app created, you can now walk through the installation process and access your app credentials. It's recommended to [add a verified domain](#add-a-verified-domain) to the app to add another level of trust for users installing your app. Otherwise, the app will display a banner stating that the app is not verified. ## Install an app Before installing your app, keep in mind the following: - An app won’t appear on an account’s _Connected Apps_ page until the initial access and refresh tokens are created. - Only users with with access to an app’s required or conditionally required scopes can install an app. - Apps can’t be installed on developer accounts. To test your app, you’ll need to create a [test account](/getting-started/account-types) in your app developer account and install it there. App installation can be broken down into two steps: authorization and token generation. ### Authorize your app with a customer account - To authorize your app with a HubSpot account, you’ll need to create an authorization URL. Do this by getting the client ID for your app and [initiating the OAuth process](/guides/apps/authentication/working-with-oauth). - Once your URL is ready, open it in your browser to see a list of all your HubSpot accounts. This is also what users will see once you begin directing them to this URL. - Select the **account** where you want to install your app. - After choosing an account, you'll be presented with a list of scopes based on the `&scope=` and `&optional_scope=` parameters you set for the authorization URL. If you include an `optional_scope` and the selected account doesn't have access to it (such as the content scope for a CRM-only account), it will not be listed. - Click **Grant access** to authorize the connection. - After granting access, you'll be redirected based on the `&redirect_uri=` parameter in the original authorization URL, and a `?code=` parameter will be appended to the URL. Use that code in the next step to generate an access token. ### Generate the initial OAuth tokens To generate your refresh and initial access tokens, you’ll need the code from the `?code=` parameter of the authorization URL, `redirect_url`, client ID, and client secret. Detailed instructions are [here](/guides/apps/authentication/working-with-oauth). Once you’ve authorized your app and generated the initial tokens, installation is complete. It’ll be listed on your [Connected Apps](https://app.hubspot.com/login?loginRedirectUrl=https%3A%2F%2Fapp.hubspot.com%2Fshortlink%2Fintegrations-beta) page, and you’ll start getting [webhook](/guides/api/app-management/webhooks) and [CRM Cards](/guides/api/crm/extensions/crm-cards) fetch requests. ## Manage public apps in HubSpot ### Find an app's ID You can find a public app's ID in your app developer account using either of the methods below: - In your developer account, navigate to **Apps** in the main navigation bar, then view the _App ID_ listed below the name of your app. - In your developer account, navigate to **Apps** in the main navigation bar, then click the **name** of the app. On the _Basic info_ page, click the **Auth** tab, then view the _App ID_. ### Monitor app behavior HubSpot logs all requests made to or from a connected app, including incoming requests using an [OAuth access token](/guides/api/app-management/oauth-tokens) or outgoing requests for webhooks or CRM cards. To view this request log: - In your developer account, navigate to **Apps** in the main navigation bar. - Click the **name** of the app. - In the left sidebar menu, navigate to **Monitoring**. - Use the **tabs** to view different types of requests being made to or from the app. While viewing these logs, you can click an individual request to view more information about it, including: - for <u>successful</u> requests, the request method, path, and time of request. - for <u>unsuccessful</u> requests, additional error information such as response headers and body. Below, learn more about each tab of the _Monitoring_ page. - **API calls:** the _API calls_ tab shows all requests made to your app using an OAuth access token. It can be filtered by HTTP method, response code, timeframe, or request URL. - **Webhooks:** the _Webhooks_ tab shows HubSpot requests for any of your app’s [webhook subscriptions](/guides/api/app-management/webhooks). Filter by response (including timeouts and connection failures), status (success, will retry, or failure), subscription type, time frame, attempt, batch, event, or account ID. The attempt ID is a combination of the `subscriptionId`, `eventId`, and `attemptNumber` from a specific request. - **CRM extensions:** the _CRM extensions_ tab shows requests for your app’s [CRM cards](/guides/api/crm/extensions/crm-cards). Filter by extension object type, CRM object type (contact, company, ticket, or deal), error or warning type, time frame, request ID, or CRM record ID (i.e. a specific contact ID). - **App settings:** the _App settings_ tab enables you to configure the [settings page](/guides/apps/public-apps/create-an-app-settings-page) that comes with your app. On each tab, if any associated events occurred in the last 30 days (e.g., a webhook trigger occurred or an API call was made), you can click **Export logs** to export the associated event data to a CSV: - In the dialog box, configure how many days' worth of data to export (up to 30 days). - Click **Export**. An email notification will be sent to the email address associated with your user in your HubSpot settings. ## Add a verified domain When HubSpot users install an app, they consent to give the app’s developer access to their account data. The developer’s identity and reputation each play an important role in a user’s decision to continue with the install. To ensure full user consent when installing an app, HubSpot will display a message on the app install screen to indicate the app's level of verification and App Marketplace listing: - When an app doesn't have a verified domain, HubSpot will display a banner on the install screen that says the app has not been verified. - When app has a verified domain but is not [listed on the App Marketplace](/guides/apps/marketplace/listing-your-app), HubSpot will display the verified domain along with a banner on the install screen that says the app has not been reviewed or approved by HubSpot. - When an app has been listed on the marketplace, passing HubSpot's app review process, HubSpot will not display either of the above banners. You're not required to verify the domain if your app has been listed on the App Marketplace. ### Add a verified domain To add a verified domain to the app, you'll need to first add the domain to the app's settings, then add a TXT record to the domain's DNS settings: - In your app developer account, navigate to **Apps**. - Click the **name** of the app. - In the left sidebar, navigate to **Contact & support**. - In the _Company domain_ field, enter your domain, then click **Save**. A message will appear below the _Company domain_ stating that the domain has not yet been verified. - Click **Verify it now** to begin the verification process. - In the right panel, confirm that the domain has been entered correctly, then click **Next**. - Copy the required TXT record value by clicking **Copy** in the _Value_ column. - In your DNS provider, create a TXT record with the copied value. Below are instructions for some common DNS providers: - [GoDaddy](https://www.godaddy.com/help/add-a-txt-record-19232) - [BlueHost](https://www.bluehost.com/help/article/dns-management-add-edit-or-delete-dns-entries#add) - [Namecheap](https://www.namecheap.com/support/knowledgebase/article.aspx/317/2237) - [Cloudflare](https://developers.cloudflare.com/dns/manage-dns-records/how-to/create-dns-records/#h_60566325041543261564371) - [Hover](https://support.hover.com/support/solutions/articles/201000064728) - [Name](https://www.name.com/support/articles/115004972547-Adding-a-TXT-Record) - [United Domains](https://help.uniteddomains.com/hc/en-us/articles/115000887125-How-to-set-up-a-TXT-record-on-a-domain-name) - After updating your DNS settings, navigate back to HubSpot, then click **Next** in the right panel. DNS records can take up to 48 hours to update, so HubSpot might not recognize the change immediately. You can get back to this screen any time by selecting **Verify it now** again from the _Company info_ settings page. - Once verified, you'll see a success status indicator under the _Company domain_ field. #### Additional Notes - 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. - Currently, 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, all apps in your developer account will get the install warning again. You can verify another domain, but the process will take a couple hours. # @ Learn how to set up a feature so that any customer who has installed your app in their account can share HubSpot reports and dashboards with it. $ guides & apps/public-apps --- # Use your installed app to share HubSpot reports to channels and users Learn how to set up the _Share With_ feature to allow your public app to share HubSpot reports and recurring updates to specific channels and users. This means you'll be able to [share your HubSpot reports and dashboards](https://knowledge.hubspot.com/dashboard/email-or-export-reports-and-dashboards) with the installed app. This feature is only available for [public apps](/guides/apps/public-apps/overview). ## Set up the Share With feature Before you set up your app to use the _Share With_ feature, you'll need to create and host four API endpoints that will accept `POST` requests from HubSpot when a customer who has installed your app shares their HubSpot reports and dashboards. You'll need to set up the following endpoints to handle the four webhook event triggers: - `CHECK_PERMISSIONS` - `FETCH_RECIPIENTS` - `FETCH_RECIPIENT` - `SHARE_OBJECT` Requests are sent with the HubSpot Signature. To ensure that the requests that are received are from HubSpot, they can be validated using the signature: [https://developers.hubspot.com/docs/api/webhooks/validating-requests](/guides/apps/authentication/validating-requests) ## Check permissions endpoint The URL of the endpoint that returns account data should be structured like `https://BASE_PATH_OF_YOUR_SERVICE/feature/SHARE_VIA/event/CHECK_PERMISSIONS`. HubSpot will provide the following fields in the body of the request: ```json // Example request body that HubSpot will send your hosted service { "portalId": 10, "appId": 20, "payload": { "requiredPermissions": ["upload-file"] } } ``` The response you return when HubSpot makes a POST request to the endpoint should be a JSON-formatted object with the following fields: ```json // Example response you should return after a POST request from HubSpot { "portalId": 10, "appId": 20, "eventType": "CHECK_PERMISSIONS", "payload": { "hasAllRequiredPermissions": true } } ``` ## Fetch recipients endpoint The URL of the endpoint that returns account data should be structured like `https://BASE_PATH_OF_YOUR_SERVICE/feature/SHARE_VIA/event/FETCH_RECIPIENTS`. HubSpot will provide the following fields in the body of the request: ```json { "portalId": 10, "appId": 20, "payload": { "recipientsFilter": { "type": 1, // integer: 0 for Channel or 1 for User "name": "Recipient" // substring which denotes filtering by name }, "limit": 5, "nextPageAfter": "hashkey" //string hashkey to denote the next page } } ``` The response you return when HubSpot makes a POST request to the endpoint should be a JSON-formatted object with the following fields: ```json { "portalId": 10, "appId": 20, "eventType": "FETCH_RECIPIENTS", "payload": { "recipients": { "total": 2, "results": [ { "id": "R2", "name": "Recipient Two", "type": 1 }, { "id": "R4", "name": "Recipient Four", "type": 1 } ] } } } ``` ## Fetch recipient endpoint The URL of the endpoint that returns account data should be structured like `https://BASE_PATH_OF_YOUR_SERVICE/feature/SHARE_VIA/event/FETCH_RECIPIENT`. HubSpot will provide the following fields in the body of the request: ```json { "portalId": 10, "appId": 20, "payload": { "id": "R3", "type": 0 } } ``` The response you return when HubSpot makes a `POST` request to the endpoint should be a JSON-formatted object with the following fields: ```json { "portalId": 0, "appId": 0, "eventType": "FETCH_RECIPIENT", "payload": { "recipient": { "id": "R3", "name": "Recipient Three", "type": 0 } } } ``` ## Share object endpoint The URL of the endpoint that returns account data should be structured like `https://BASE_PATH_OF_YOUR_SERVICE/feature/SHARE_VIA/event/SHARE_OBJECT`. HubSpot will provide the following fields in the body of the request: ```json { "portalId": 10, "appId": 20, "payload": { "sender": { "id": 2387654 // HubSpot Id of sender }, "recipient": { "id": "R3", "name": "Recipient Three", "type": 0 }, "message": "New report for this week", "objectMetadata": { "id": 2334567, "type": 2, // integer denotes Type of Object: 2 for Report "name": "Chat Conversations", "screenshotUrl": "https://api-na1.hubspotqa.com/filemanager/api/signed-url-redirect?portalId=10" } } } ``` The response you return when HubSpot makes a `POST` request to the endpoint should be a JSON-formatted object with the following fields: ```json { "portalId": 10, "appId": 20, "eventType": "SHARE_OBJECT", "payload": { "recipientUrl": "https://example.com" //optional redirect URL } } ``` Once you've set up your four endpoints, you can configure your app settings. - In your app developer account, navigate to **apps** in the main navigation bar. - Select the **app**. - In the left sidebar, click **More features**. - In the _Target URL_ section, enter the **URL** that HubSpot will make a `POST` request to when events trigger. Or, use this testing URL: `https://api.hubspot.com/integration-components-example/v1/sample-app-webhooks/beta-app` - Click to toggle the **Share via** switch on. - Click **Save**. ## Test the functionality After setting up your four endpoints and configuring your app settings, you can test this feature: - [Install your app](/guides/apps/public-apps/overview). - In your HubSpot account, navigate to **Reporting & Data** > **Reports**. - Hover over a report and click the **Actions** dropdown menu. Then, click **Share Via**. - In the right panel, select the **connected app** and click **Next**. - Select the **frequency** of the message and which **channel or user** you're sending the report to. - Enter an **optional message**. - Click **Send now**. # Accessibility @ HubSpot is committed to making sure that we have provided the necessary tools to help you make your content accessible. While not a definitive guide, here are some steps that you can take to make your content accessible. $ guides & cms/content --- # Accessibility As websites continue to become more and more critical to businesses, ensuring that content on websites is accessible to all visitors has become more vital than ever. In the United States, this is often called [section 508](http://www.section508.gov/) compliance, which refers to the section of the Reauthorized Rehabilitation Act of 1998 that requires federal agencies to make electronic and information technology accessible to people with disabilities. While section 508 compliance is a good baseline, increasingly the [Web Content Accessibility Guidelines (WCAG)](https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_WCAG) are being used as the standard for creating accessible content on the web. ## Accessibility is not a feature The legal aspect shouldn't be your motivator for providing a good experience for your users. [One in four Americans have a disability according to the CDC](https://www.cdc.gov/ncbddd/disabilityandhealth/infographic-disability-impacts-all.html). Choosing not to provide a good experience for 25% of visitors is like a physical store denying every fourth visitor from entering. Not only will those customers be unhappy, they likely will not come back or recommend your services. A common phrase among developers is that "accessibility is not a feature". The idea is don't treat accessibility like a bonus thing to add, or something to deal with at the end of a project. Accessibility requires thought, empathy, and understanding. When approached early on in a project you can design and develop solutions instead of needing to re-architect something later. **Often times solving for accessibility solves for your other priorities as a developer**, user experience, performance, and SEO are a few top concerns that are directly intertwined with accessibility. Improving one often improves the other. Empathy is a huge part of accessibility that is easy to overlook. As developers we want to automate everything, if something is hard or time-consuming to do, we want tools to either do it for us or tell us the right way to do it. Unfortunately, these tools can only scratch the surface, because they are not human, they can't truly experience the page the way a human can. **It is technically possible to create a web page that passes most automated accessibility tests with flying colors but is completely unusable to all humans, sighted, blind, deaf, color blind, limited or unlimited motor function.** You can technically meet requirements, but provide an unusable frustrating experience that completely alienates people. The takeaway is this: don't rely on the tools, test your work, empathize with your users, collect feedback. While not a definitive guide, here are some steps that you can take to make your content accessible, as well as resources for further diving in. ## Provide text alternatives for any non-text content All images, icons, videos, and buttons that convey meaning or are interactive should have a text alternative. Not only is this good for visitors that are consuming your content via a screen reader, but it is also great for search engines. The alternative text should describe the content of the image. Most images on your site can likely be edited in the page editor, [setting alt text in the page editor is easy](https://knowledge.hubspot.com/cos-pages-editor/how-do-i-add-alt-text-to-my-image). Inside of custom modules and templates, however, you need to make sure that alt text specified in page editors is honored. This means making sure if you have an `<img>` element that the alt text the user provides is actually used. ```html Good: <img src="{{ module.image.src }}" alt="{{ module.image.alt }}" /> Bad: <img src="{{ module.image.src }}" /> ``` If for some reason you are not making an image editable in the page editor, you should hard-code the alt text. There is one exception to this alt text rule. If your image provides no additional context and is purely just presentational it is better to have a ["null" alt text](https://www.w3.org/WAI/tutorials/images/tips/) value than to go without an alt attribute altogether. ```html Good if only presentational: <img src="{{ module.image.src }}" alt="" /> ``` For videos, use a service that supports captions and consider including a transcript. Some services that support uploading transcripts include [Vidyard](https://knowledge.vidyard.com/hc/en-us/articles/360009876234-Make-your-Vidyard-players-accessible), [YouTube](https://support.google.com/youtube/answer/2734799), [Vimeo](https://help.vimeo.com/hc/en-us/articles/224968828-Captions-and-subtitles), and [Wistia](https://wistia.com/support/player/captions). ### Use a11y friendly lazy loading solutions Lazy image loading is a common performance enhancement technique for building websites. The method used to actually lazy load matters for accessibility. JavaScript solutions for this typically rely on the `src` attribute of images to be something untrue (like a small `.gif` file) instead of the true source of the image, which is held in a `data-src` attribute until the user scrolls the image close to view. This means that initially the image is inaccessible to a screen reader user who is navigating the page, especially one using a rotor to look through the entire page's contents without even scrolling. Additionally, if JavaScript isn't loaded on a page, these lazy loading solutions will fail, and therefore, leave assistive technology users without the proper images on the page. Using [native image lazy loading](https://web.dev/native-lazy-loading/) avoids this problem, keeping the image markup the same, regardless of whether JavaScript loads. ```html Good: <img src="{{ module.image.src }}" alt="{{ module.image.alt }}" loading="lazy" height="200px" width="400px" /> Bad: <img src="1px.gif" data-src="{{ module.image.src }}" alt="{{ module.image.alt }}" /> ``` HubSpot supports [browser-based lazy loading](/guides/cms/content/performance/lazy-loading) in custom modules. It is recommended you use it. ## Make sure that all links and form inputs have descriptive text Links, buttons, and form elements all need to have actual text that indicates what it does or where it goes. Otherwise screen readers will read out that the user has a link or button selected, but will have no idea what it does. If using an icon, provide screen reader text. For example, some standard HubSpot templates use an icon shortcode: ```html <a href="https://www.facebook.com" ><i class="fab fa-facebook-f"></i ><span class="screen-reader-text">Facebook</span></a > ``` Don’t use `'display: none'` to hide form labels and other descriptive text. This prevents text from being read aloud for those with difficulty seeing, as well as hurts users who can see but have trouble seeing the text due to lack of contrast on the placeholder text. Not to mention, who hasn't started filling out a form or had a form get auto-filled for them, but had no idea if it was entered correctly because the form field labels were invisible. If you absolutely have to have hidden text or hidden text could provide additional context that a sighted user wouldn't need. Use CSS that makes the text invisible without hiding it from screen readers or add the `'.screen-reader-text'` class. ```css .screen-reader-text { clip: rect(1px, 1px, 1px, 1px); height: 1px; overflow: hidden; position: absolute !important; width: 1px; } ``` ## Adding a skip to content link When a visitor is navigating using a screen reader, or just simply using their keyboard, it is super helpful if there is a way to skip over the portions of the page that are repeated such as a header. We can do this by adding a link that points to the main content of the page. In your header, add HTML with the following content: ```html <a href="#content" class="screen-reader-text" >Five ways to make your site more accessible</a > ``` Then, in every template ensure that there is an element with an ID attribute of content. In our example, we used the article title as the text to jump to. This enables search engines to understand what is being linked to. It's a more semantic way of jumping to the content. ```html <main id="content"> <!-- your page or post's actual content --> </main> ``` ## Use semantic markup Semantic markup is HTML that conveys meaning. Some examples of elements that are semantic are `<header>`, `<main>`, `<footer>`, `<nav>`, `<time>`, `<code>`, `<aside>`, and `<article>`. Some examples of elements that are not semantic are `<div>` and `<span>`. Non semantic elements still have their place, but should be used primarily for presentation and not for conveying meaning. Semantic elements can be understood by search engines and assistive technologies, both positively affecting SEO and improving your user experience. Do not use `<div>` elements as interactive elements like buttons, tabs, or links unless you're sure you've provided a good experience via aria attributes. Use links (`<a />`) and `<button />` appropriately. Use links for linking to sections of a page and navigating to other pages. Use Buttons for interactions that do not result in leaving the page or navigating to an ID. When working with tables - if a table has a descriptive title include it in a `<caption>` element just after the `<table>` element. Use `<th>` elements with the proper scope attribute in tables for column and row headings and `<thead>`, `<tbody>`, and `<tfoot>` to denote sections. Use the scope attribute on `<th>` attributes to denote whether the heading is for the row or column. Need an accordion? Don't forget about the `<details>` and `<summary>` elements they give you most of this functionality for free, and it's accessible. [Be sure to add a polyfill or fallback if you need to support old browsers](https://caniuse.com/#feat=details). ## Keyboard navigation Some users use their keyboard to navigate webpages and forms out of preference. Some visitors must use the keyboard or some sort of assistive device that emulates a keyboard to navigate websites. - Make sure that [':focus'](https://www.w3schools.com/cssref/sel_focus.asp) is styled so that when a user is navigating via the keyboard they can see what is focused. Only disable the CSS outline property if you are providing an acceptable alternate visual indicator. Use [:focus-within](https://css-tricks.com/almanac/selectors/f/focus-within/) if it can help direct users attention usefully. - When hiding/showing portions of the page with [':hover'](https://www.w3schools.com/cssref/sel_hover.asp) or via Javascript, make sure that users are also able to navigate those controls via Javascript or that there is alternative path to the information. - Make sure your site's main navigation supports keyboard navigation, a commonly missed issue is that drop-downs and fly-outs are not made accessible. This prevents users from getting to parts of websites that may be critical. - Provide and update `hidden`, `aria-expanded`, `aria-current`, `aria-selected`, and other state attributes as necessary to ensure screen readers properly communicate the state of elements. ## Fallback to roles if needed When creating templates or coded files, you should use correct semantic elements (`<header>`, `<main>`, `<footer>`, `<nav>`, `<dialog>`, etc.) to communicate to web browsers and screen readers what type of content is inside your elements. In the off-chance semantic elements are not appropriate for your scenario you should add [role attributes](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles). Adding landmarks make it easier for users that are using screen readers to jump between the major sections of a web page. HubSpot includes 'role' attributes to the menu, simple menu, and Google Search modules. ## Additional design tips 1. Make sure that when a user zooms their browser to 200% content remains visible and readable. 2. It is not advised to make fonts much smaller than 16px as it may become hard for visitors to read. 3. Avoid using color as the only way of communicating information. A surprising percentage of the world population is color blind. 4. Make sure that there is sufficient contrast between the color of text and the background so that users with limited vision can read the content. 5. Avoid animations that flash rapidly (more than three times per second) as it could trigger seizures in some visitors. 6. Consider supporting [prefers-reduced-motion](https://css-tricks.com/introduction-reduced-motion-media-query/) if you have animations on your site. 7. Today building a responsive website is considered a must. Make sure all interactive elements function as expected on mobile. Interactive elements must be easy to tap, and there should plenty of room between tap regions. ## Content writing An easy thing to forget is that accessibility should also be considered in your text content. 1. Avoid directional language "see sidebar on the left". If the user is on mobile, odds are this makes no sense to them as content usually stacks on mobile. 2. Use headings like `<h1>` `<h2>` etc. and nest them consecutively. Do not skip a heading for visual effect. 3. When adding a link, never have the link text be "Click here", it conveys no meaning to screen readers, doesn't make any sense on touch screens or other types of devices. Instead, your text should state what is at that link. This is also better for SEO, because search engines understand what is being linked to. 4. Make sure if you use any special styles on the text to convey meaning, you are using the appropriate `<mark>` `<strong>` `<em>` `<sup>` etc. semantic tag, otherwise your meaning may be lost to some visitors. 5. If you are targeting visitors worldwide, avoid jokes that are region specific. If you are having your site translated, avoid puns. As much as we love a good dad joke, they often don't translate well. 6. Use tools to help improve your grammar and spelling. This helps comprehension and results in better translations. 7. Write your content in a way that fits your site's audience. You wouldn't explain astrophysics to 5th grader, the same as you would an accomplished physicist. That said, avoid fancy words just for the sake of using them, [plain is better](https://www.nngroup.com/articles/plain-language-experts/). ## HubSpot specific recommendations Most of the work toward improving accessibility is not HubSpot specific. That said we want to set you up for success on HubSpot and there are some things you can do either as a developer or content creator to provide a better experience for users. ### Use custom modules and templates that follow a11y best practices Whether you are installing a module or template from the marketplace, developing them or having them built for you, one of the best things you can do is to make sure they follow a11y best practices. Custom Modules can have the greatest impact on the accessibility of your site because they are often used many times over, sometimes even on the same page. Similarly, a single template may be used hundreds or thousands of times on a single site. If your site is not accessible you are likely cutting your profits. Like with advertising it naturally makes sense to pay a little more to make sure your website reaches a wider audience. One of the benefits of modules is that you can often improve the code later as guidelines and best practices evolve, improving accessibility for every page that module lives on at once. On the HubSpot marketplace look for modules and templates that talk about accessibility, and meeting WCAG requirements in their descriptions. If you're working with a developer tell them from the beginning that accessibility is important for you. Not all developers build with a11y in mind from the start. Telling them late in a project will likely cost you more than telling them from the get-go because instead of prioritizing it from the beginning, they now need to retroactively fix accessibility issues. If you are a developer, build modules and templates that make meeting a11y guidelines easy for content creators. Use appropriate headings, semantic HTML, make interactive elements provide proper roles, and context. Content creators that are aware of accessibility issues are generally happy to pay a little more for modules and templates that are inclusive, that said you need to show that your modules take accessibility into account. Include skip links in templates, make global groups and global partials used in your templates a11y friendly, these get used throughout an entire website and can have the greatest impact on the usability of a site. If you build for the HubSpot marketplace know that your content could get used across thousands, even millions of pages on the web. You make a choice that affects people when publishing your content and sharing your work, whether your work is inclusive or not. ## Beware of one-stop solutions There are dozens of tools that make claims like _"Just add our script to your website and your site will be accessible"_. Some of these tools will try to make sense of parts of the page and change html and attributes to try to fix some issues. That said they are guessing, and can be wrong or could break functionality on your site or make matters worse for users. These tools may not always operate as expected, sometimes supplying their own screen reader system. If you have a complicated UI, your website may actually become harder to operate through the injected tool. Because of these issues tools like this are not a substitute for building your site with accessibility in mind. Invest time and money into providing the best experience for all of your users. Reliance on one-stop solutions could cost you the same or more, than simply building things the correct way. By testing and building with accessibility in mind you'll also get the benefit of empathy and understanding of your customers. To be clear this is not referring to testing tools, specifically this is regarding tools that claim to solve accessibility woes for you. Testing tools themselves are great and you should be using them, in-addition to manual testing. [Learn more about accessibility scripts/overlays.](https://overlayfactsheet.com/) ## HubSpot Developer Podcast In January 2021 HubSpotters along with Jon Sasala of Morey Creative chatted about accessibility. ## More accessibility information There are some really great resources out there for building accessible-minded websites. We highly, highly encourage you to check them out. - [The A11Y Project](https://a11yproject.com/) - [MDN Accessibility Docs](https://developer.mozilla.org/en-US/docs/Web/Accessibility) - [Web Accessibility Initiative](https://www.w3.org/WAI/) - [WebAIM](https://webaim.org/) - [Simplified WCAG 2 checklist](https://webaim.org/standards/wcag/checklist) - [Section 508 checklist](https://webaim.org/standards/508/checklist) - [Detailed WCAG 2.2 recommendations](https://www.w3.org/TR/WCAG22/) - [AXE by Deque](https://www.deque.com/axe/) - [WAVE](https://wave.webaim.org/) - a tool for testing the accessibility of a webpage - [The Ultimate guide to web accessibility](https://blog.hubspot.com/website/web-accessibility) # Content Search @ Utilizing native the site search functionality within the HubSpot CMS, Content Search. The HubSpot CMS has built-in site search functionality to allow your visitors to easily find the content they are looking for. $ guides & cms/content --- # Content Search Site search functionality allows your visitors to search your website for specific content. All of content hosted on HubSpot is automatically indexed in HubSpot’s search engine. Content beyond an HTML size of 2 MB will be trimmed before being stored in content search. ## Searching Content The search engine is accessible through the [site search API](/guides/api/cms/site-search). This API supports numerous filtering options to allow you to create a highly customized and powerful search experience on your website. For example, if you wanted to build a search into your blog, you can query for `type=BLOG_POST` to only return blog posts. Or, if you wanted to build search into the Spanish version of your website, you could query `language=es` to only return Spanish pages. The API returns JSON that can be parsed with JavaScript to display the results on your website. All content types will return the page domain, title, url and language. The description returned is a sample of text from the content which best matches the search term. A `<span class="hs-search-highlight hs-highlight-html">` element will wrap perfectly matching text, allowing you to highlight matching text with CSS. Depending on the type of content searched, the results return slightly different information, so you can display results for unique content types differently. For example, blog posts will return information on which tags the post has, who the author is, and when it was published. ```json // json { "total": 850, "offset": 0, "limit": 10, "results": [ { "id": 3761238962, "score": 0.30858153, "type": "SITE_PAGE", "domain": "designers.hubspot.com", "url": "https://developers.hubspot.com/docs/cms/search", "language": "en", "title": "Content Search", "description": "Using HubSpot’s native search engine, content search, to implement <span class="hs-search-highlight hs-highlight-html">search on your website</span>" }, { "id": 3254581958, "score": 0.30858153, "type": "BLOG_POST", "domain": "designers.hubspot.com", "url": "https://designers.hubspot.com/blog/getting-started-with-local-development", "featuredImageUrl":"https://designers.hubspot.com/hubfs/local-development.jpg" "title": "Getting Started with Local Development", "description":"The beta Local Development Tooling connects your local workflow to HubSpot, allowing you to utilize <span class="hs-search-highlight hs-highlight-html">version control</span>, your favorite text editor and various web development technologies when developing on the HubSpot CMS." "authorFullName": "William Spiro", "tags": [ "Website Development", "Local Development" ], "publishedDate": 1447938000000 } } ``` ## Implementing search on your website There are two default modules you can use to easily implement search on your website, which use the [site search API](/guides/api/cms/site-search): `search_input` and `search_results`. ### Site Search Input An input field for visitors to enter search terms into. This module can exist anywhere on your website. This module can be included in a template with `{% module "search_input" path="@hubspot/search_input" %}`. ```hubl {% module "search_input" path="@hubspot/search_input" %} ``` ### Site Search Results Returns a listing of content based on the search term. This module can exist anywhere on your website. This module can be included in a template with ```hubl {% module "search_results" path="@hubspot/search_results" %} ``` If the functionality of these modules is not how you want search to work on your website, you can build your own search modules or functionality. The default search modules are designed to be extended based on your search needs. You can view and clone these modules in the Design Manager by searching for their names in the Finder, or, you can fetch them to your local environment using the [CMS CLI](/guides/cms/tools/local-development-cli) by running `hs fetch @hubspot/search_input.module` or `hs fetch @hubspot/search_results.module`. ## Search Results Template Every domain has its own search results page by default. The template and path for this page are set at **Settings > Website > Pages** under the [System Pages tab](https://app.hubspot.com/login?loginRedirectUrl=https%3A%2F%2Fapp.hubspot.com%2Fshortlink%2Fsettings%2Fwebsite%2Fpages%2Fall-domains%2Fsystem-pages) for specific domains. See the [CMS theme boilerplate search results template](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/system/search-results.html) as an example. The domain set for the search results page is automatically connected to the default search modules. However, you can use the [site search API](/guides/api/cms/site-search) to build our your search results as you'd like on any pages of your website. ## How is the search precedence determined? The order of search results is determined by a series of weighted comparisons of page content to the visitor's search term. Page content is separated into comparison fields with varying weight based on where the content lives within the HTML of your pages. Comparison fields are grouped in order of weight: 1. HTML title 2. Meta description 3. H1 HTML elements 4. H2 HTML elements 5. H3 HTML elements 6. Other HTML elements Please note that this list is subject to change. If you wish to tell our indexer to specifically boost certain elements on a page, you can wrap the content in a `hs-search-keyword` class. ## Control indexing during development While developing a new site, it's useful to be able to test site search without worrying about the site being indexed by search engines such as Google. In your [`robots.txt`](https://knowledge.hubspot.com/seo/prevent-content-from-appearing-in-search-results) you can tell HubSpot to crawl everything, while blocking other bots. ```shell # Block all bots User-agent: * Disallow: / # Allow HubSpot Site Search Indexing User-agent: HubSpotContentSearchBot Allow: / ``` If any of your pages have a meta tag specifying no index, the page will still not be indexed, even if allowed in the `robots.txt`. Also remember to review your `robots.txt` prior to launch to ensure everything indexes how you want it to. ## Default indexing behavior Because the CMS knows the intent of certain types of pages, content search is able to safely limit indexing to allow indexing of content type pages. Examples of content type pages: - Site Pages - Landing Pages - Knowledge articles - blog posts **System pages and password protected pages are not indexed.** CMS Membership restricted pages will only display in hubspot content search for users that are signed in and have access to the pages. ## How can I exclude pages from being returned in search results? If you block a page from being indexed to search engines via your websites [`robots.txt`](https://knowledge.hubspot.com/seo/prevent-content-from-appearing-in-search-results) file or via `meta` tags, they will not be indexed for site search. In your `robots.txt` add a `disallow`. ```shell # Block all bots User-agent: * Disallow: /path-to-page # Block just HubSpot Site Search Indexing User-agent: HubSpotContentSearchBot Disallow: /path-to-page ``` You can also add a `NOINDEX, NOFOLLOW` meta tag in the `<head>` at the page or template level. ```html <meta name="ROBOTS" content="NOINDEX, NOFOLLOW" /> ``` You don't need to block robots using both `robots.txt` and the meta tag. Doing so can make it confusing later if you decide to allow indexing of a page. ## How to exclude sections of a page from being indexed in search Sometimes there are regions of a page that are not useful to return in search results. This might be content from a header, a footer, sidebar, etc. When this is the case, you can add the class `hs-search-hidden` to your HTML for those regions to have search ignore the content inside. # Content Staging @ Content Staging is an in-app development environment that allows you to redesign or create staged pages, before publishing them to your production site. $ guides & cms/content --- # Content Staging Content Staging is an in-app separate content environment that allows you to update or create staged pages before publishing them to your production site. Content staging can be used during a website redesign project to stage existing pages using a new template. Analytics tied to the page will carry over to the new page on publish from staging. Since redesigns usually involve more than one page being updated, you can stage all of the pages you need to and publish all of them to the live site at once. See [Using content staging for a website redesign](https://knowledge.hubspot.com/cms-general/redesign-and-relaunch-your-site-with-content-staging) for detailed instructions. For a full tutorial on how to use Content Staging, [check out this article from the Website User Guide](https://knowledge.hubspot.com/cms-general/redesign-and-relaunch-your-site-with-content-staging). For building and testing changes to assets like themes, templates, modules, we recommend using a [developer sandbox account](https://offers.hubspot.com/free-cms-developer-sandbox). [Learn more about building an efficient development workflow.](/guides/cms/setup/creating-an-efficient-development-workflow) # Sync content to a CMS Developer Sandbox @ Learn how to sync content from a production account to a CMS Developer Sandbox account. $ guides & cms/content --- # Sync content to a CMS Developer Sandbox account (BETA) Sync content from a production account to a CMS Developer Sandbox account to keep assets in your sandbox environment up to date. This one-way sync supports a variety of content-related data, from templates and modules, to brand settings and navigation menus, so that you can ensure you're accurately testing and iterating. ## Content types You can sync the following types of content from your production account to a CMS Developer Sandbox account: - **Developer assets:** - **Themes:** theme files and settings, theme field configurations, and theme CSS and JavaScript. - **Templates:** page, blog, email, and system templates. - **Modules:** custom modules, module configurations, and module assets (HTML, CSS, and JavaScript). - **Global Partials:** header and footer template partials, reusable template sections, and global partial configurations. - **Content supported assets:** - **Developer assets:** all developer assets above. - **Website/Landing page content:** images, documents, and files used on your pages (including videos and downloadable content). - **Navigation menus:** primary and secondary navigation menus, and footer menus. - **Brand settings:** colors, fonts, and logs, along with global style settings. - **GraphQL-enabled HubDB tables & data:** custom database tables and dynamic content collections. - **Global content:** headers and footers, along with global settings and partials. ## Syncing content To sync content from a production account to a CMS Sandbox account: - In your HubSpot account, navigate to **Content** > **Design Manager**. - In the bottom bar, click **Content Sync**. When syncing for the first time, you'll then be taken directly to the content sync setup page. - Click **Next**. - Click the **dropdown menu** to select the CMS Developer Sandbox account that you want to sync to. - Click **Next**. - Select the **content** that you want to sync. Learn more about [syncable types of content](#content-types). - Click **Next**. - Click **Start Sync**. After initiating the content sync, you'll be brought to a summary page where you can view your sync history and trigger additional content syncs. A tab will be created for each CMS Developer Sandbox you've synced with. You can return to this screen any time by clicking **Content Sync** in the bottom bar of the design manager, then selecting **View Sync Activity**. To resync your content or sync to a new account: - In the bottom bar, click **Content Sync**. - To resync to the most recently synced account, click **Resync** in the pop-up box. - To sync to a new CMS Developer Sandbox account, click **Sync to new account**. - To sync with other previously-synced accounts, or view all sync activity history, click **View Sync Activity**. ## View sync history On the _Content Sync_ page, you can review your sync activity history for each CMS Developer Sandbox you've synced with. - Click the **tabs** to select the CMS Developer Sandbox account to view history for. - To view the logs for a specific sync, click **View details**. In the right sidebar, review the log details. - To resync to that account, click **Resync**. # CRM objects in CMS pages @ CRM objects can be queried and rendered on HubSpot hosted content, allowing data to be shared between your business operations, website, and emails. $ guides & cms/content/data-driven-content --- # Use CRM object data in CMS pages You can query CRM objects to use data from the object's records on HubSpot hosted content, allowing data to be shared between your business operations, website, and emails. Using the [`crm_object`](/reference/cms/hubl/functions#crm-object), [`crm_objects`](/reference/cms/hubl/functions#crm-objects) and [`crm_associations`](/reference/cms/hubl/functions#crm-associations) HubL functions, you can display and control logic based on your CRM object data. Using CRM data on your website means that your sales, marketing, and website data all live in the same place and will always reflect the same information. In addition, because you can associate CRM records with one another, you can also pull in associated data onto your website pages. Similarly, you can [create sets of dynamic pages that automatically generate using CRM object or HubDB data](/guides/cms/content/data-driven-content/dynamic-pages/overview). You can learn more about building data-based CMS pages in HubSpot Academy's [CMS Data-Driven Content course](https://app.hubspot.com/academy/tracks/1148948/intro). ### Example use case One example of using CRM object data in pages is a real estate listing page. With a custom object called _property_, individual object records can be created for each house that needs to be listed. Real estate agents can then add information to object properties to store details, such as location, number of bedrooms, and asking prices. Website pages can then pull in that record data for each property to create a listing page and details pages for each property. [Check out the GitHub repo](https://github.com/HubSpot/cms-custom-objects-example) to view the full example. For an overview of this example, check out the [recording of HubSpot Developer Day 2020](https://developers.hubspot.com/developer-day-2020). ## Supported CRM object types Below are the types of CRM objects that you can pull data from for your CMS pages. Whether you can use the data across all pages or only on private pages depends on the object type. In the tables below, learn about which object types are available for CMS content along with their object type names and fully qualified names. ### CRM object data available for all pages Data from the following CRM objects can be used on any CMS page. | Object type | object_type_name | Fully qualified name | | --- | --- | --- | | [Products](/guides/api/crm/commerce/products) | `product` | `PRODUCT` | | [Marketing events](/guides/api/marketing/marketing-events) | `marketing_event` | `MARKETING_EVENT` | | [Custom objects](/guides/api/crm/objects/custom-objects) | _**Content Hub** Enterprise_ only. You can either use the object's [fully qualified name](/guides/cms/content/data-driven-content/crm-objects#getting-a-custom-object-type-s-details), or the name that was entered at the time of creation. For example, if you create an object named `Cars`, you cannot reference it with `cars` or `Car`.You must use the [fully qualified name](/guides/cms/content/data-driven-content/crm-objects#getting-a-custom-object-type-s-details) if the custom object shares a name with a standard object. It's also strongly recommended to use the FQN if the object name is fully uppercased to avoid potential conflicts with HubSpot's standard objects. | | ### CRM object data available for private pages Data from the following CRM objects can be used only on pages that require either a [password](https://knowledge.hubspot.com/website-pages/password-protect-a-page) or a [membership login.](https://knowledge.hubspot.com/website-pages/require-member-registration-to-access-private-content) | Object type | object_type_name | Fully qualified name | | --- | --- | --- | | [Contacts](/guides/api/crm/objects/contacts) | `contact` | `CONTACT` | | [Companies](/guides/api/crm/objects/companies) | `company` | `COMPANY` | | [Deals](/guides/api/crm/objects/deals) | `deal` | `DEAL` | | [Tickets](/guides/api/crm/objects/tickets) | `ticket` | `TICKET` | | [Quotes](/guides/api/crm/commerce/quotes) | `quote` | `QUOTE` | | [Line items](/guides/api/crm/commerce/line-items) | `line_item` | `LINE_ITEM` | ## Display data from a single CRM record with the crm_object function Use the [`crm_object`](/reference/cms/hubl/functions#crm-object) function to get a single record from the HubSpot CRM by query or by CRM record ID. Object records are returned as a dict of properties and values. ```hubl {# Render custom object by query #} {% set event = crm_object("event", "name=Defensive Health") %} {{ event.name }} {# Render custom objects specifying the id of the object #} {% set event = crm_object("event", 289236) %} {{ event.name }} ``` ```html <p>Defensive Heatlh</p> <p>Defensive Heatlh</p> ``` If a query returns a collection of records, the function will return the first record in the collection. ## Display data from multiple CRM records with the crm_objects function Use the [`crm_objects()`](/reference/cms/hubl/functions#crm-objects) function to get CRM records by object type from the HubSpot CRM by query or by record ID. Records are returned as a dict of properties and values. The record returned contains a `results` property that can be looped through to display the information in the record's items. ```hubl {# Render custom objects by query #} {% set events = crm_objects("event", "limit=3&type=virtual") %} <h3>{{events.total}} New Events:<h3> <ul> {% for event in events.results %} <li>Name: {{ event.name }}</li> {% endfor %} <ul> {# Render custom objects by ids #} {% set events = crm_objects("event", [289236,289237,289238]) %} <h3>{{events.total}} New Events:<h3> <ul> {% for event in events.results %} <li>Name: {{ event.name }}</li> {% endfor %} <ul> ``` ```html <h3> 3 New Events: <h3> <ul> <li>Name: Defensive Health</li> <li>Name: Body Balance</li> <li>Name: Happy Heart</li> <ul> <h3> 3 New Events: <h3> <ul> <li>Name: Defensive Health</li> <li>Name: Body Balance</li> <li>Name: Happy Heart</li> <ul></ul> </ul> </h3> </h3> </ul> </ul> </h3> </h3> ``` ## Display associated records Use the [`crm_associations`](/reference/cms/hubl/functions#crm-associations) HubL function to get a list of associated records from the HubSpot CRM based on the given record ID, association category, and association definition ID.[](#getting-a-custom-object-s-details) Records are returned as a dict of properties and values. ```hubl {% set associated_objects = crm_associations(289236, "USER_DEFINED", 3) %} <h3>Contacts Associated With Event</h3> <ul> {% for contact in associated_objects.results %} <li>Name: {{ contact.firstname }} {{ contact.lastname }}</li> {% endfor %} </ul> ``` ```html <h3> Contacts Associated With Event <h3> <ul> <li>Name: Brian Halligan</li> <li>Name: Dharmesh Shah</li> <li>Name: Yamini Rangan</li> </ul> </h3> </h3> ``` ## Getting a custom object type's details To get a custom object type's `name`, `id`, `fullyQualifiedName`, association IDs, and other details, you can make a `GET` request to the [CRM Objects schema API](/reference/api/crm/objects/custom-objects). `fullyQualifiedName` for account-specific object types includes the HubSpot account ID, so it's recommended to avoid using it when developing your code for multiple HubSpot accounts. ## CRM Object Module field To provide a way for content creators to select CRM records to display or execute logic, you can build modules that include the [CRM object field](https://developers.hubspot.com/docs/reference/cms/fields/module-theme-fields#crm-object). For example, you may want to display information from a specific product, contact, company, deal, quote, ticket, or custom object. ## CRM Object tutorials and resources - [Essentials of getting started with Custom Objects](https://developers.hubspot.com/blog/essentials-for-getting-started-with-custom-objects) - [Think like an architect: Build scalable Custom Objects](https://developers.hubspot.com/blog/how-to-think-like-an-architect-by-building-scalable-custom-objects) - [Build Dynamic Pages with CRM Objects](/guides/cms/content/data-driven-content/dynamic-pages/crm-objects) # Build dynamic pages using CRM objects @ The HubSpot CMS is an extension of the HubSpot CRM. Use CRM objects like custom objects to build dynamically generated pages. $ guides & cms/content/data-driven-content/dynamic-pages --- # Build dynamic pages using CRM objects You can build CRM object dynamic pages using [standard HubSpot objects](/guides/api/crm/understanding-the-crm), such as _Products_, or [custom objects](/guides/api/crm/objects/custom-objects) (_Enterprise_ only). Dynamic pages consist of a listing page view and a unique details page for each record. The listing page displays a list of all records in the object, and the details pages display information about each record based on the record's property values. You can also [build dynamic pages using HubDB](/guides/cms/content/data-driven-content/dynamic-pages/hubdb). Learn more about the different types of [dynamic pages](/guides/cms/content/data-driven-content/dynamic-pages/overview). - You can create up to 10 pages per each CRM object data source. For example, you can create up to 10 dynamic pages using the contacts object and 10 dynamic pages using the companies object. This limit does not take into account the number of child pages generated by the CRM objects, meaning that a dynamic page using the contacts object might generate 100 pages, but it would still count as one page toward this limit. - CRM object dynamic pages are limited to 50,000 [content search](/guides/cms/content/content-search) index updates per day per HubSpot account. Any updates to these pages beyond this limit will not be reflected in content search that day. Building a CRM object dynamic page requires four steps: 1. Prepare your CRM object with data you want to display. 2. Create a custom module to display the data. 3. Create a listing module to display all records on a listing page. 4. Add the modules to a drag and drop page and select the object as the data source. This tutorial will walk through how to build a CRM object dynamic page using the example of a _Car_ custom object, which stores data on individual cars at a dealership. You can learn more about building data-based CMS pages in HubSpot Academy's [CMS Data-Driven Content course](https://app.hubspot.com/academy/tracks/1148948/intro). ## Prerequisites The following are required for building dynamic pages using CRM objects: - For custom object dynamic pages, you'll need a **_Content_** **_Hub_** _Enterprise_ subscription, or a **_Marketing Hub_** _Enterprise_ account with **_Content Hub_** _Professional_. - For standard CRM object dynamic pages, you'll need a _**Content Hub** Professional_ or _Enterprise_ account. - An understanding of how to [create custom modules](/guides/cms/content/modules/quickstart). - A standard or [custom object](/guides/api/crm/objects/custom-objects) to use as a data source. You'll also need [user permission for the object](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#custom-object-access), such as _Companies_ or _Custom Objects_ access. - Records created under the object you're using, such as individual products or custom object records. Records can be [created in HubSpot’s UI](https://knowledge.hubspot.com/records/use-custom-objects#desktop) or via [API endpoints](/guides/api/crm/understanding-the-crm). ## Prepare your CRM object Each dynamic page will pull its metadata from the properties of the individual records of your selected CRM object. The values contained in these properties should uniquely identify the content of the page to stand out in search results. Metadata includes: - Page slug - Page title - Meta description - Featured image For the page title, meta description, and featured image, you can use any of the object's existing single-line text properties. However, the dynamic page slug must be a property configured with `"hasUniqueValue": true`. Existing properties cannot be updated with this configuration, so you will need to create a new property for this field. For custom objects, create a new property by [updating the object schema](/guides/api/crm/objects/custom-objects). For standard objects, create a new property by [using the properties API](/guides/api/crm/properties). | Dynamic page property | Property type | Description | | --- | --- | --- | | `Dynamic page slug` | | `Page Title` | text | The page's title. | | `Meta description` | text | The page's meta description. | | `Featured image` | text | The page's featured image, which will appear when the page is shared. | In our example, we’ll be using a custom property _VIN_ (Vehicle Identification Number) as a page slug, since we know each car’s _VIN_ is unique. [content search](/guides/cms/content/content-search) will index up to 10,000 CRM records per set of dynamic pages. For example, if you build a set of dynamic pages based on a CRM object that has 15,000 records, only 10,000 of those pages will be indexed in the search feature. If you create another set of dynamic pages based on the same object, that set of pages will also be limited to 10,000 search-indexed pages. ## Create a module to display details for a single record Individual detail pages will use this module to display the object record data. In our example, the module will display details for an individual car's page. Within this module, you'll use the [`dynamic_page_crm_object`](/reference/cms/hubl/variables#crm-object-dynamic-pages) variable to access the data stored to the current dynamic page's object instance. To set up the details module: - In the design manager, [create a new custom module](https://knowledge.hubspot.com/design-manager/create-and-edit-modules). Tip: naming the module something like _\[object type\] - detail_ can make it clear what this module does. For example, _Car - detail._ - In the `module.html` field, add the following code: ```hubl {% if dynamic_page_crm_object and !module.car %}{# detail page and content creator has not selected an object#} {% set car = dynamic_page_crm_object %} {# easy variable to access the CRM objects properties from #} {# To see all of the properties your object during development, print it to the page with the |pprint filter #} <h1>{{car.year}} {{car.make}} {{model}} </h1> </div> </div> {% else %}{# The page is not a dynamic page or the user selects a specific object, we can get the data from a CRM object field if there is data. #} {% set car = module.car.properties %} {# easy variable to access the CRM objects properties from #} {# To see all of the properties your object during development, print it to the page with the |pprint filter #} <section aria-label="featured car"> </div> </section> {% endif %} ``` This code checks if the page is a CRM object dynamic page. If it is, use `dynamic_page_crm_object` to retrieve the object record data. Object record data will be retrieved based on the object type set in the page editor. This makes the module useful outside of the context of a dynamic page as well, as you can use it to feature object records on dynamic pages by having multiple instances of the module. In the above code, we’ve created a CRM object of _car_. To fit your use case, you can update the _module.car_ references in the code to your object's name. After creating this module, you’ll then create the listing module to define the information that will appear on the general listing page. ## Create a listing module Depending on your use case, you may want the dynamic page's root URL to display a listing of your object's records. In our example, we would want the _https://website.com/cars_ page to display the listing of all of the available cars. To do this, create a new custom module that will act as a listing of the object records. Within this module, use the [dynamic_page_crm_object_type_fqn](/reference/cms/hubl/variables#crm-object-dynamic-pages) variable to access the fully qualified name of the page's selected object. Tip: naming the module something like _\[object type\] - listing_ can make it clear what this module does. For example, _Car - listing_. You can use and modify the example code below to create a listing module: ```hubl {% if dynamic_page_crm_object_type_fqn %}{# dynamic listing page #} {% set cars = crm_objects(dynamic_page_crm_object_type_fqn, "limit=200","vin, price, picture, location, body_type, date_received, image") %} {# To see all of the properties your object during development, print it to the page with the |pprint filter #} </div> </a> </div> {% endfor %} </div> {% elif dynamic_page_crm_object %} <!-- Listing module is hidden when viewing a dynamic pages detail page. --> {% else %}{# display simple listing for use on other pages #} {% set cars = crm_objects("p9307273_car", "limit=5","vin, price, location, body_type, date_received, image") %} {# To see all of the properties your object during development, print it to the page with the |pprint filter #} <section aria-labelledby="object-listing-heading"> <h3 id="object-listing-heading"> Available Cars </h3> <ul> {% for car in cars.results %} <li><a href="/cars/{{ car.vin }}">{{ car.body_type }} ({{ car.price }}) in {{ car.location }}</a></li> {% endfor %} </ul> </section> {% endif %} ``` In this code we are checking if the current page is a CRM object dynamic listing page. If it is, the module will display a listing of items. If it’s a detail page, the module won’t display anything. If it's another CMS page, we show a simple listing of results. ## Page setup With the modules created, you can now add them to a page, then select the object as the data source. 1. [Create a new page](https://knowledge.hubspot.com/website-and-landing-pages/create-and-customize-pages), selecting a page template that has a drag and drop area or flexible column. 2. In the page editor, click the **Settings** tab. 3. Under _Page URL_, click the **pencil icon** to edit the page's URL. Set the URL to where you want your listing page to appear. In our car example our listing page will be at: `/cars`. 4. Click **Advanced options**, then scroll to the _Dynamic pages_ section. 5. Under _Dynamic Pages_, click the **Data source** dropdown menu, then select your object. Then, click the **Dynamic page slug** dropdown menu and select the property to use as the page slug. Only single-line text properties set to `"hasUniqueValue":true` will be available for selection. In our example, the page URL will use the _VIN_ property, which will generate detail pages with the URL slug of: `/cars/[VIN value]` 6. Under _Metadata_, continue selecting the properties that will populate the page’s metadata. In our example, we’re setting the featured image to a property that contains an image URL for a picture of each car. 7. After setting up your metadata, at the top of the page, click the **Content** tab to return to the editor. 8. In the left sidebar, in the _Add_ tab, search for your detail and listing modules, then drag them into the page editor. 9. To preview your page, click **Preview** in the upper right. 10. When ready, publish your page by clicking **Publish** in the upper right. You've now successfully created dynamic pages based on your data source. With our example page set up, when a user updates a car record in HubSpot, the listing and detail pages will update automatically to reflect the changes. Newly created records of that object type will also automatically create new pages using the dynamic page slug and be linked to from the listing page. [Learn more about creating listings with the crm_objects function](/guides/cms/content/data-driven-content/crm-objects#displaying-the-properties-for-multiple-crm-objects-using-the-crm-objects-function). ## More CRM object resources - [Use CRM object data in CMS pages](/guides/cms/content/data-driven-content/crm-objects) - [Custom object API](/guides/api/crm/objects/custom-objects) # How To build a Dynamic Team Member Page with HubDB @ Building dynamic pages with HubDB on the HubSpot CMS. $ guides & cms/content/data-driven-content/dynamic-pages --- # How To build a Dynamic Team Member Page with HubDB In this tutorial, I’ll show you how to create a Dynamic Team Member Page on HubSpot’s CMS, using HubDB. You can reference more detailed HubDB documentation [here.](/guides/cms/storage/hubdb/overview) You’ll need: - **A HubSpot account with the CMS Add-on ** - Portals with _Website Starter_ or _without the Website Add-On_ do not support HubDB - You can check if your portal has the CMS Add-on by [signing in here](https://app.hubspot.com/l/account-dashboard/). - Approximately 3 hours - Some prior knowledge of HubSpot's CMS, HTML and CSS will be needed to customize your page At the conclusion of this tutorial, we will have a working Dynamic Team Member Page based on the requesting page's URL, like this example here: [Dynamic Team Member Example](https://designers.hubspot.com/team-members) ## 1. Build your HubDB Team Member Table This tutorial will focus on adding logic that allows your Team Member page to be dynamic, based on the requested CMS page's URL. If you are new to HubDB and need help generating an initial HubDB Team Member Table, take a look at this tutorial here: [Static Team Member Page](/guides/cms/content/data-driven-content/dynamic-pages/hubdb) If you are familiar with building HubDB tables you can import some testing data for walking through this example, here's a file you can use: [team-member.csv](https://cdn2.hubspot.net/hubfs/327485/HubDB/team-member-example.csv). Upload the file to HubDB, to follow along with this example. Once done, be sure to publish your table, making note of the ID of the table (the last number in the URL) and you will be ready to start using your data. ## 2. Create a Custom Module Okay, so we have our HubDB Table generated which will showcase our amazing team members. First, we will need to create a new template in Design Manager (if you haven't already) and a new Custom Module. You can name it something like "Dynamic Team Member Module". We want to generate a Custom Module to allow for easier use across multiple templates or allow for potential custom fields to be created, depending on your needs. In this example, we are going to incorporate two custom fields which will allow each page to apply different styling to our rendered Table (more on that later). Be sure to add your new Custom Module to your desired template as well. Within your new Custom Module, add the following code. This code is generating the framework for which our logic will be applied to, that allows our Team Member section to be dynamic. ```hubl {# Get Team Members from HubDB #} {% set table = hubdb_table_rows([Insert YOUR Table ID], queryparam)%} {% if table == [] %} <p class='align-center'>Sorry, no one found for that Search. Try changing your filter and search again.</p> {% else %} {% for row in table %} </div> </div> {% endfor %} {% endif %} ``` Click over to the preview of the template and you should see the simple Team Listing. If nothing appears, double-check that your table ID is correct, the table is published and that your variable names match what is set within your HubDB table. Let's breakdown what we are doing here. Don't worry about the 'queryparam' just yet, this will be needed for later in the tutorial when we filter our Team Member Table based on the requesting page's URL path! First, we add a little logic to render an error if for some reason our table returns an empty object. Next, we map out the rows for our Team Members. If done correctly, our preview should be rendering our entire Team. Feel free to modify the HTML structure as needed. It is worth noting that we are accessing the values of our HubDB rows via dot notation here. It is also perfectly acceptable to adjust this to use 'bracket' notation. ## 3. Add Custom Field to the Custom Module This example is using a custom field within the module to further enhance the functionality of the rendered Team Member page. Our Custom Module will ultimately be applied to multiple pages so we want the ability to easily adjust the HTML structure of Table, if one of our Locations only has, say, 2 employees verse another that might have 6 employees. We can handle this by including CSS that controls the size of our team member containers based on a custom class that is applied to the HTML! We can accomplish this by adding a Choice field to our module. As seen below. In order for this field to have an impact, on the rendered layout of our widget, you will need to generate some CSS that can handle this. For simplicity, here is some CSS to get you going: [team-member-card.css](https://cdn2.hubspot.net/hubfs/327485/HubDB/team-member-card.css) By default, our Module will set the 'half' class on our containers. Now, each card will render with 50% width. Now, if we generate a page, using this Module, and we know that we will have fewer team members rendered, we can dynamically adjust the CSS to make the Table display better! Be sure to include this CSS within the custom module too. Our updated module will now look like this. ```hubl {# Load Card Stylesheet #} {{ require_css([insert link to CSS]) }} {# Get Team Members from HubDB #} {% set table = hubdb_table_rows([Insert YOUR Table ID], queryparam)%} {% if table == [] %} <p class='align-center'>Sorry, no one found for that Search. Try changing your filter and search again.</p> {% else %} {% for row in table %} </div> </div> {% endfor %} {% endif %} ``` ## 4. Add Logic to Handle Dynamic Filtering by Page URL Now that we have our Team Members rendering, we need to write some logic to handle rendering the Table, based on the requesting page's URL path. We can do this with a few [If Requests](/reference/cms/hubl/if-statements) that will pass a 'queryparameter' variable into our HubDB function to control which rows are returned onto the page. Insert the following code into your Custom Module, after our CSS include, but before the main HubDB table code. ```hubl {# sets the different query parameters using request path for hubdb query #} {% set queryparam = "" %} {% if request.path == "/team-members/location/ma" %} {% set queryparam = queryparam ~ "&location=1" %} {% endif %} {% if request.path == "/team-members/location/nh" %} {% set queryparam = queryparam ~ "&location=2" %} {% endif %} {% if request.path == "/team-members/location/pa" %} {% set queryparam = queryparam ~ "&location=3" %} {% endif %} ``` Remember the `queryparam` variable from the beginning of this tutorial? This code puts it to use! Using the request object on the CMS page, specifically the **path**, we can control which team members are returned. Depending on the **path** of the page we set `queryparam` equal to a specific **value** from our HubDB table's location field. When this value is appended as our Table's filter option, our Team Member page will be dynamically updated to reflect only those members! ## 5. Generate your Team Member Pages Our Dynamic Team Member module is set and ready to go. All that is left to do is generate our pages. This example uses 4 pages. One that is the main Team Member 'Parent' page and three child pages. Feel free to utilize the example paths (slug) when generating your pages. Otherwise, you will need to remember to update the Custom Module to use the path's you are expecting your visitors to land on. This tutorial uses fairly simple logic so please feel free to modify or adapt anything you see here that is specific for your needs. Here is an example Dynamic Team Member Directory that we generated based on this tutorial: [Team Members Example](https://designers.hubspot.com/team-members) ## More HubDB focused tutorials - [How to join multiple HubDB tables](/guides/cms/storage/hubdb/overview)[](/guides/cms/content/data-driven-content/dynamic-pages/video) - [How to add videos to dynamic pages](/guides/cms/content/data-driven-content/dynamic-pages/video) - [How to build multilevel dynamic pages using HubDB](/guides/cms/content/data-driven-content/dynamic-pages/multilevel) - [Let's build a page with a map using HubDB](/guides/cms/storage/hubdb/overview) # Build dynamic pages using HubDB @ Learn how to build dynamic website pages using HubDB data. $ guides & cms/content/data-driven-content/dynamic-pages --- # Build dynamic pages using HubDB [Dynamic pages](/guides/cms/content/data-driven-content/dynamic-pages/overview) are CMS pages that get their content from a structured data source. Based on how you configure your dynamic page, HubSpot will pull data from the selected source and automatically create a set of pages. This includes a listing page that displays summaries of the data, and individual pages for each data source entry. Using a [HubDB table](/guides/cms/storage/hubdb/overview) as a data source, you can create a dynamic page which then generates a page for each row in the table. Each dynamic page includes its own unique, SEO-friendly URL, and offers page-specific analytics. - You can create up to 10 dynamic pages per data source. For HubDB dynamic pages, this means up to 10 dynamic pages can use the same HubDB table. The number of rows in the table (and therefore child pages) is not factored into this limit. Learn more about [HuDB technical limits](/guides/cms/storage/hubdb/overview#hubdb-technical-limits). - HubDB dynamic pages are limited to 50,000 [content search](/guides/cms/content/content-search) index updates per account per day. Any updates to these pages beyond this limit will not be reflected in content search that day. This tutorial walks through how to create a set of dynamic pages using HubDB as the data source. To follow this tutorial, you'll need: - _**Content Hub** Professional_ or _Enterprise_ - Some prior knowledge of HTML and CSS You can learn more about building data-based CMS pages in HubSpot Academy's [CMS Data-Driven Content course](https://app.hubspot.com/academy/tracks/1148948/intro). ## 1. Create a HubDB table To create a new HubDB table: - In your HubSpot account, navigate to **Content** > **HubDB**. - In the upper right, click **Create table**. - In the dialog box, enter the table **label** and **name**, then click **Create**. With the table created, you can set it to be used for dynamic page content: - In the upper right, click **Actions**, then select **Manage settings**. - In the right panel, click to toggle the **Enable creation of dynamic pages using row data** switch on. - You can optionally select the meta description, featured image, and canonical URL of the individual dynamic pages. If you leave these values empty, each page will inherit the respective values from its parent page. For a page to use the values from the meta description, featured image, and canonical URL columns, the page must include the following `page_meta` HubL variables rather than `content` variables: - `{{page_meta.meta_description}}` - `{{page_meta.featured_image_URL}}` - `{{page_meta.canonical_url}}` For example, HubSpot templates pull in their meta description from the `{{content.meta_description}}` tag by default. You'll instead need to use `{{page_meta.meta_description}}`. - Click **Save** to save your changes. After you update the table settings, the _Page title_ and _Page path_ columns will be added to the table. - **Page title:** the name of this page as seen in the HTML title tag. - **Page path:** the last segment of the URL for the dynamic page created by each row in the table. The following table is an example modeled after an "About us" page for members of a company's executive team. This table will be used to create dynamic pages with paths ending in `cfo-harlow`, `ceo-jeff`, `cto-bristow`, and `pd-hugo`. | Page title | Page path | Role | Name | Bio | | --- | --- | --- | --- | --- | | CFO Harlow | `cfo-harlow` | CFO | Harlow | This is Harlow, who is generally pennywise. | | CEO Jeff | `ceo-jeff` | CEO | Jeff | Jeff is the CEO, which means he usually runs things around here. | | CTO Bristow | `cto-bristow` | CTO | Bristow | This is our CTO, Bristow, who likes to tinker. | | Chief PD | `pd-hugo` | CPD | Hugo | Hugo, our Chief Product Designer, enjoys designing products. | Though you have to enter page paths as lowercase, the resulting URLs are case insensitive. In the example above, when someone navigates to `/CEO-Jeff` they will see the same page as `/ceo-jeff` instead of a 404 error. When you're ready to use the data from your table to build out your pages, click **Publish** in the top right. ## 2. Create a template Next, you'll create one template for both the listing page and the individual detail pages for each row, similar to how blog templates can be used for both listing and post detail pages. To create the page template: - In your HubSpot account, navigate to **Content** \> **Design Manager**. - In the left sidebar menu, navigate to the folder that you want to create the template in. To create a new folder, in the upper left click **File**, then select **New folder**. Then, click **File**, and select **New file**. - In the dialog box, use the **dropdown menu** to select **HTML + HubL** as the file type. - Click **Next**. - In the **File name** field, enter the name of the template. - Under _File location_, you can change where the template is located in your design manager by clicking **Change**_._ - Click **Create** to create the template. When a dynamic page is set to use this template and the end of the page URL matches the path column, you can access the `dynamic_page_hubdb_row` and `dynamic_page_hubdb_table_id` variables in the template. For example, for building an executive profile page, the code below demonstrates how you can use fields from `dynamic_page_hubdb_row` to display an executive's info: - `hs_name`: the associated _Page title_ for the HubDB row. - `name`: the executive's name. - `role`: the executive's role. ```hubl {% if dynamic_page_hubdb_row %} <h1>{{ dynamic_page_hubdb_row.hs_name }}</h1> <h2>{{ dynamic_page_hubdb_row.name }}</h2> <h3>{{ dynamic_page_hubdb_row.role }}</h3> <p>{{dynamic_page_hubdb_row.bio}}</p> {% endif %} ``` Next, you can add handling for the case in which someone loads your dynamic page without any additional paths from your table. Usually, this is used as a listing page, for listing links to the pages for the rows in your HubDB table. Replace your code with: ```hubl {% if dynamic_page_hubdb_row %} <h1>{{ dynamic_page_hubdb_row.hs_name }}</h1> <h2>{{ dynamic_page_hubdb_row.name }}</h2> <h3>{{ dynamic_page_hubdb_row.role }}</h3> <p>{{dynamic_page_hubdb_row.bio}}</p> {% elif dynamic_page_hubdb_table_id %} <ul> {% for row in hubdb_table_rows(dynamic_page_hubdb_table_id) %} <li><a href="{{ request.path }}/{{ row.hs_path }}">{{ row.hs_name }}</a></li> {% endfor %} </ul> {% endif %} ``` The code inside the `elif` block iterates over all the rows in the executive's table and displays each entry in a list, with a link to their unique path. - In the design manager, click **Preview** to preview the template. The preview will be blank, because it relies on the context of the page to set the `dynamic_page_hubdb_row` or `dynamic_page_hubdb_table_id` variables. - To test your code at the template level, add the following temporary code to the top of your template, ensuring that you remove it before publishing: `{% set dynamic_page_hubdb_table_id = <YOUR_HUBDB_TABLE_ID> %}` - After adding the above code, your template should now render a list of hyperlinks, pulling data from the HubDB table you built. - After previewing the template, remove the temporary code above. Then, click **Publish** in the top right to make it available for creating pages. ## 3. Create the dynamic page To create a dynamic page from your template: - With your new template open in the design manager, click the **Actions** dropdown menu at the top of the finder, then select **Create page**. - In the dialog box, select **Website page**, then enter a **page name**. - Click **Create page**. - At the top of the page editor, click the **Settings** tab. - In the **Page title** field, enter a page name, which you can use later to look up traffic analytics. - In the **URL** field, enter a **URL** of `/executives`. The URL will be the base URL for your dynamic page. - Click **Advanced Options** to expand additional settings. - Scroll down to the _Dynamic pages_ section, then click the **Data sources** dropdown menu. Select the **HubDB table** you created. - When you’re finished, click **Publish** in the upper right. Your pages are now ready to view. ## 4. View live pages Now you can visit your new dynamic page and all of its paths, as defined by your HubDB table. - Navigate to the dynamic listing page at the URL you set in the page editor. This tutorial uses `/executives` for its dynamic page URL, so in that case you would navigate to: `https://www.yourdomain.com/executives`. - From the listing page, click the **names** in the bulleted list to view the details page for that executive. ## 5. Add a new table row With your dynamic page loading HubDB data, navigate back to the table and add a new row. After publishing the table, you'll then see your live page dynamically update with the new HubDB data. - In your HubSpot account, navigate to **Marketing** > **Files and Templates** > **HubDB**. - Click the **name** of the table that you create. - Click **Add row**, then fill out each column. Below is a sample set of data. | Page title | Page path | Role | Name | Bio | | --- | --- | --- | --- | --- | | CMO Hobbes | cmo-hobbes | CMO | Hobbes | Hobbes is our go-to cat enthusiast. | - In the upper right, click **Publish**. - In another tab, navigate back to the listing page (`/executives` in this example). You should now see the new executive appear on the listing page, and clicking their name will reveal their details page. ## 6. View dynamic page data Once there are visits to your dynamic page, you can [measure individual page performance](https://knowledge.hubspot.com/website-and-landing-pages/analyze-individual-content-performance) or [view all page data in the traffic analytics tool](https://knowledge.hubspot.com/reports/analyze-your-site-traffic-with-the-traffic-analytics-tool). Even though the individual executive pages are built from the same dynamic page, traffic data, such as page views, will be attributed to each page. To view your page visit data in HubSpot: - In your HubSpot account, navigate to **Reporting** > **Marketing Analytics**. - In the left sidebar, navigate to **Web traffic analysis** > **Pages**. - View the table to see traffic data for the individual parent and child pages. Child pages will be denoted with **\> arrow icons** to show their relation to parent pages. Keep in mind the following if you're not seeing the traffic data you expect: - If you've [excluded your IP addresses in the account's report settings](https://knowledge.hubspot.com/reports/exclude-traffic-from-your-site-analytics), ensure that you're accessing your pages from outside your network for your page views to register. - It can take [up to 40 minutes](https://knowledge.hubspot.com/reports/how-often-do-analytics-in-hubspot-update) for new page data to appear in HubSpot. ## More HubDB focused tutorials - [How to join multiple HubDB tables](/guides/cms/storage/hubdb/overview)[](/guides/cms/content/data-driven-content/dynamic-pages/video) - [How to build a dynamic team member page with HubDB](/guides/cms/content/data-driven-content/dynamic-pages/dynamic-team-member-page) - [How to add videos to dynamic pages](/guides/cms/content/data-driven-content/dynamic-pages/video) - [How to build multilevel dynamic pages using HubDB](/guides/cms/content/data-driven-content/dynamic-pages/multilevel) - [Let's build a page with a map using HubDB](/guides/cms/storage/hubdb/overview) # How to build multilevel dynamic pages with HubDB @ Create dynamic pages with nested HubDB tables. $ guides & cms/content/data-driven-content/dynamic-pages --- # How to build multilevel dynamic pages with HubDB A [dynamic website page](/guides/cms/content/data-driven-content/dynamic-pages/overview) is a CMS page whose content changes based on the path of the URL requested by an end user. [HubDB](/guides/cms/storage/hubdb/overview) already allows you to store, filter, and display data in your HubSpot website pages. Multilevel dynamic pages take this concept further, allowing you to create up to five levels of pages within one dynamic template. Each dynamic page includes its own unique, SEO-friendly URL, and offers page-specific analytics. That this tutorial assumes you already have multiple HubDB tables created. Please see the [HubDB documentation](/guides/cms/storage/hubdb/overview) if you are unfamiliar with HubDB or want to create your first HubDB tables. ## 1. Enable child tables in your table's settings. - In your HubSpot account, navigate to **Marketing** \> **Files and Templates** > **HubDB**. - Click the **name** the table that will act as the parent for any other tables that will be used for your nested child tables. - In the top right, click the **Actions** dropdown menu, then select **Manage settings**. - If you haven't yet enabled the table for dynamic pages, click to toggle the **Enable creation of dynamic pages using row data** switch on, then select the columns to populate the page data. - Click to select the **Allow child tables** and **Automatically create listing pages for child tables** checkboxes. - Click **Save**. With your changes saved, you'll then see a **Child table** column added to the table. In this column, use the **dropdown menu** to select another HubDB table to pull data from. To streamline this process, keep the child table columns and their internal names the same. If they are not the same, you'll need to use conditional logic later to render unique content for a given table. ## 2. Select child tables for each row In the parent table, use the **Child table** column dropdown menus to select **tables** to pull data from. You can only select tables that are enabled for dynamic page content creation. If you've enabled a table for dynamic page creation but aren't seeing it in the **Child table** dropdown menu, ensure that you've clicked **Publish** in the child table. A parent table cannot reference a child table which also references the parent table. This will create a loop that results in an error when trying to select the child table within the parent table. In the above example, the first row will be pulling its food data from the _Foods_ child table, which contains data about available foods, as shown below. When setting multilevel dynamic pages, the page paths for each row in the child table will be `parent_path/child_path`. For example, the page path for the `banana` row will be `page_path/foods/banana`. By turning on the **Automatically create listing pages for child tables** setting, HubSpot will also automatically create intermediate listing pages for the child table rows (`page_path/foods` and `page_path/beverages`). If you would rather have those intermediate routes not resolve and return a 404 page, deselect the **Automatically create listing pages child child tables** checkbox in the table's settings. ## 3. Create the multilevel template Through child tables, you can create up to five levels of pages with one dynamic template. You can configure each level by using the `dynamic_page_route_level` HubL variable. The top-level template starts at a value of `0` and increments for each table layer. ```hubl {% if dynamic_page_route_level == 0 %} Top Level Template {% elif dynamic_page_route_level == 1 %} Parent table template (/food /beverage) {% elif dynamic_page_route_level == 2 %} Child table template (/food/banana etc., /beverage/soda etc.) {% endif %} ``` ## 4. Populate the top-level template For this tutorial, the goal is to list the child rows and group them by parent category. The example code below does the following: - First, it retrieves the category rows using `dynamic_page_hubdb_table_id`. - Then, the `hs_child_table_id` property of each category row retrieves the table IDs of the child tables. - Last, it uses those table IDs to list the child rows under each parent category. ```hubl {% if dynamic_page_route_level == 0 %} <h1>Categories</h1> {% set rows = hubdb_table_rows(dynamic_page_hubdb_table_id) %} {% for row in rows %} <h2><a href="{{ request.path }}/{{ row.hs_path }}">{{ row.hs_name }}</a></h2> {% set childRows = hubdb_table_rows(row.hs_child_table_id) %} {% for childRow in childRows %} <li><a href="{{ request.path }}/{{ row.hs_path }}/{{childRow.hs_path}}">{{ childRow.hs_name }}</a></li> {% endfor %} {% endfor %} {% endif %} ``` ## 5. Populate the dynamic-level templates After populating the top-level template, you'll then need to define templates for the subsequent levels in a similar fashion. While building out your templates, it can be useful to access parent table data within child templates. For example, when our example page resolves to `foods/banana`, the `dynamic_page_hubdb_row` variable will be set to the `banana` row. However, you may want to access data from the `food` row. You can use `hs_parent_row` value on the `dynamic_page_hubdb_row` to retrieve the parent row: ```hubl {% if dynamic_page_route_level == 1 %} <h1>Categories</h1> <h2>{{dynamic_page_hubdb_row.hs_name}}</h2> {% set rows = hubdb_table_rows(dynamic_page_hubdb_row.hs_child_table_id) %} {% for row in rows %} <li><a href="{{ request.path }}/{{ row.hs_path }}">{{ row.hs_name }}</a></li> {% endfor %} {% elif dynamic_page_route_level == 2 %} <h1>Categories</h1> <h2>{{dynamic_page_hubdb_row.hs_parent_row.hs_name}}</h2> <h3>{{dynamic_page_hubdb_row.hs_name}}</h3> {% endif %} ``` Continue building out templates for each needed level. Then, you'll need to l ink the parent table to a page. ## 6. Link parent table to page The final step is to create a page from the multilevel template and link the top-level parent table to the page. To do so, you'll [create a page](https://knowledge.hubspot.com/website-and-landing-pages/create-and-customize-pages#create-pages), then access its settings. In the _Advanced Options_ settings of the page editor, click the **Data source** dropdown menu and select the **Parent table**. # Dynamic pages overview @ Learn how to create CMS pages that use content from a structured data source, such as HubDB or CRM objects. $ guides & cms/content/data-driven-content/dynamic-pages --- # Dynamic pages overview Dynamic pages are CMS pages that get their content from a structured data source, such as HubDB or CRM objects. Based on how you configure your dynamic page template or modules, HubSpot will use that data to automatically create and populate a set of pages. This includes a listing page that displays summaries of the data, and individual pages for each data source entry (HubDB row or CRM object record). Depending on the data source you choose, there are different prerequisites, advantages, and content creation experiences. Below, read about each data source and how to choose which one is best for you. You can learn more about building data-based CMS pages in HubSpot Academy's [CMS Data-Driven Content course](https://app.hubspot.com/academy/tracks/1148948/intro). ## CRM object dynamic pages In HubSpot, CRM objects represent the different types of records and relationships your business has. Standard CRM objects include contacts, companies, deals, and tickets. With an _Enterprise_ subscription, you can also [create custom objects](/guides/api/crm/objects/custom-objects). Using any of these CRM objects, you can create a listing page and individual details pages for each record of the object type you choose. For example, a car dealership could store their inventory as records with a custom _Car_ object. Then, using CRM object dynamic pages, they could list their inventory online with a unique page automatically created for each car. When a new record is created under the _Car_ object, a new page will be created automatically, keeping the inventory and website in sync. You may want to use CRM objects as your data source if: - You want to associate records to other CRM objects, such as contacts. - You want to create automation or personalization based on the object. - It simplifies your businesses processes and record keeping. ### Requirements To build CRM object dynamic pages, you’ll need: - _**Content Hub Professional**_ or _**Enterprise**_. - To build using Custom Objects you'll need either **_Content_ _Hub_ _Enterprise_**, or **_Marketing Hub_ _Enterprise_** with **_Content Hub_ _Professional_**. - An understanding of how to [create custom modules](/guides/cms/content/modules/quickstart). - A standard or custom object as a data source. You can create up to 10 dynamic pages per data source. For CRM object dynamic pages, this means up to 10 dynamic pages per CRM object. For example, you can create up to 10 dynamic pages using the contacts object and 10 dynamic pages using the companies object. ### Content creation After you create your dynamic content modules, they can be inserted into any page that includes a [drag and drop area](/guides/cms/content/templates/drag-and-drop/tutorial). To edit how the data is displayed, you'll need to update the modules or template you've created. If the template includes any other drag and drop areas, you can edit the page's non-dynamic content within the page editor. Any updates made to the dynamic or non-dynamic content will apply to both the listing page and the details pages, similar to editing a template. Because the dynamic page content is coming from [CRM object records](https://knowledge.hubspot.com/records/use-custom-objects), you manage dynamic page content the same way you would manage other CRM records. For example, you can edit dynamic page content by [editing](https://knowledge.hubspot.com/records/update-a-property-value-for-a-record) or [deleting](https://knowledge.hubspot.com/records/delete-crm-records) individual records in HubSpot. Similarly, you can manage content in bulk by [importing](https://knowledge.hubspot.com/import-and-export/import-objects) or [bulk editing records](https://knowledge.hubspot.com/records/bulk-edit-records). The type of pages you can create depends on the object you choose: - Public pages can be built using the following objects: - Products - Marketing events - Custom objects - Private pages ([password protected](https://knowledge.hubspot.com/website-pages/password-protect-a-page) or [member registration](https://knowledge.hubspot.com/website-pages/require-member-registration-to-access-private-content)) can be built using the following objects: - Contacts - Companies - Deals - Quotes - Tickets It’s important to be aware of automation that’s set up for the CRM object you choose. For example, if you have a workflow that automatically updates a custom object record’s name based on associated deal stage, your dynamic page content will also be updated any time the record’s name changes. ### Start building To start building CRM object dynamic pages, check out the [developer guide for building CRM object dynamic pages](/guides/cms/content/data-driven-content/dynamic-pages/crm-objects). If you plan to use custom objects as your data source, learn how to [create and manage custom objects through HubSpot’s API](/guides/api/crm/objects/custom-objects). ### More resources - [How to build dynamic pages using CRM objects](/guides/cms/content/data-driven-content/dynamic-pages/crm-objects) - [CRM objects in CMS](/guides/cms/content/data-driven-content/crm-objects) - [Manage your CRM database](https://knowledge.hubspot.com/get-started/manage-your-crm-database) ## HubDB dynamic pages Using [HubDB](/guides/cms/storage/hubdb/overview), you can generate dynamic pages from the rows of a HubDB table. You can also use child tables to create nested pages that map to separate blogs or listing pages of your website. When you enable a HubDB table as a data source, you can select which columns to use for the meta description, featured image, and canonical URL. You may want to use HubDB as the data source for your dynamic pages if: - You don’t need to associate data from your tables with your CRM data. - The [HubDB technical limits](/guides/cms/storage/hubdb/overview#hubdb-technical-limits) are not an issue for your use-case. ### Prerequisites To build HubDB dynamic pages, you’ll need: - _**Content Hub**_ _Professional_ or _Enterprise_. - An existing HubDB table, or learn how to get started and [create your first HubDB table](/guides/cms/storage/hubdb/overview#creating-your-first-table). - An understanding of how to [create custom modules](/guides/cms/content/modules/quickstart). You can create up to 10 dynamic pages per data source. For HubDB dynamic pages, this means up to 10 dynamic pages per HubDB table. Learn more about [HuDB technical limits](/guides/cms/storage/hubdb/overview#hubdb-technical-limits). ### Content creation After you create and enable your HubDB table for dynamic page content, you manage the content of your pages by creating, editing, and deleting HubDB rows. You can edit HubDB tables directly in your HubSpot account, or you can edit your schema externally then upload the data via a CSV or through the [HubDB API](/guides/api/cms/hubdb). To edit how the data is displayed, you'll need to update the modules or template you've created. If the template includes any other drag and drop areas, you can edit the page's non-dynamic content within the page editor. Any updates made to the dynamic or non-dynamic content will apply to both the listing page and the details pages, similar to editing a template. ### Start building To start building dynamic pages using HubDB,check out the [developer guide for building HubDB dynamic pages](/guides/cms/content/data-driven-content/dynamic-pages/hubdb). ### More resources - [How to build multi-level dynamic pages with HubDB](/guides/cms/content/data-driven-content/dynamic-pages/multilevel) - [How to add videos to HubDB dynamic pages](/guides/cms/content/data-driven-content/dynamic-pages/video) - [How to join multiple HubDB tables](/guides/cms/storage/hubdb/overview) ## Creating dynamic referral URLs with query string parameters If you have a setup where you'll need unique versions of each page URL while the content of the page remains the same across all versions, you should use query string parameters instead of dynamic pages. When using dynamic pages, instances of your content are generated for every instance of a HubDB table or CRM object, but if the content is identical across all your pages, you will be flagged for duplicate content by search engines. This will lead to a scattered sitemap, a confusing website search experience, and extra entries in your website's page analytics. By using query string parameters, you can accomplish the same goal of having a unique URL per referrer. You can even dynamically show unique content on a page if needed, but there won't be a dynamically generated page per instance of a HubDB table or CRM object; instead, there will only be one page with dynamic content that's shown based on the query string parameter. Depending on whether you're using a HubDB table or CRM object as your data source, you'll need to use a different HubL function to query for the associated data in your account. Consult the code snippets below, which both parse the logo ID from a URL such as `https://example.com/my-page?logo_identifier=123`. ### CRM objects The HubL code below checks whether a `logo_identifier` parameter is present in the query string, then uses the `crm_object` HubL function to fetch the data that corresponds to the associated logo record, which was created as a custom object. ```hubl {% if request.query_dict.logo_identifier %} <h3> The logo query string is present</h3> {% set logo_identifier = request.query_dict.logo_identifier|string %} {% set query = "logo_id=" + logo_identifier %} {% set logo = crm_object("logos", query, "logo_name, logo_id, logo_url") %} Logo data: {{ logo }} <br> Logo id: {{ logo.logo }} <br> Logo name: {{ logo.name }} {% else %} <h3>The logo query string is NOT present</h3> {% endif %} ``` Learn more about the `crm_object` HubL function in [this article](/reference/cms/hubl/functions#crm-object). ### HubDB table The HubL code below parses the `logo_identifier` parameter in the query string, then uses the `hubdb_table_rows` HubL function to fetch the logo's URL from the associated HubDB table (e.g., a table with an ID of `10181541`) using the logo ID from the query string. ```hubl {# get logo ID from query string#} {% set logo_identifier = request.query_dict.logo_identifier|string %} {% set query = "identifier=" + logo_identifier %} {% set logo_row = hubdb_table_rows(10181541, query)[0] %} <img src="{{ logo_row.logo.url }}" style="max-width: 500px;"> ``` Or you can also invoke the `hubdb_table_row` function, though you'll need to pass the row ID in the query parameter. ```hubl {% set row_id = request.query_dict.logo_identifier|string %} {% set logo_row = hubdb_table_row(10181541, row_id) %} <img src="{{ logo_row.logo.url }}" style="max-width: 500px;"> ``` Learn more about the `hubdb_table_row` HubL function in [this article](/reference/cms/hubl/functions#hubdb-table-row). # How to add videos to dynamic pages in HubDB @ How to add videos to dynamic pages powered by HubDB. $ guides & cms/content/data-driven-content/dynamic-pages --- # How to add videos to dynamic pages in HubDB A [dynamic website page](/guides/cms/content/data-driven-content/dynamic-pages/overview) is a CMS page whose content changes based on the path of the URL requested by an end user. [HubDB](/guides/cms/storage/hubdb/overview) already allows you to store, filter, and display data in your HubSpot website pages. Multilevel dynamic pages take this concept further, allowing you to create up to five levels of pages within one dynamic template. Each dynamic page includes its own unique, SEO-friendly URL, and offers page-specific analytics. This tutorial assumes you already have multiple HubDB tables created. Please see the [HubDB documentation](/guides/cms/storage/hubdb/overview) if you are unfamiliar with HubDB or want to create your first HubDB tables. ## 1. Add a 'VIDEO' column to your table. Navigate to [HubDB](https://app.hubspot.com/l/hubdb) in your HubSpot portal, and edit the table you would like to be a parent of other tables. Click **"Add new column"** and create a column with type **"Video"**. ## 2. Select videos for each row. You can now add a video to your row by clicking the **"choose"** button in the video column. Selecting a video will store the video `player_id` as the column value in the row. The file thumbnail is used to visually represent the video in the UI. ## 3. Add the video player widget to your dynamic template You can now reference the row data in your dynamic template to build a [`video_player`](/reference/cms/hubl/tags/standard-tags#video-player) tag. ```hubl {% if dynamic_page_hubdb_row %} {% video_player "embed_player" player_id="{{dynamic_page_hubdb_row.video}}" %} {% endif %} ``` # Create emails with programmable content @ $ guides & cms/content/data-driven-content --- # Create emails with programmable content This feature is currently in beta. By using this functionality you agree to the [developer beta terms](https://legal.hubspot.com/hubspot-beta-terms). This guide refers to functionality available only through that beta. [Opt-into the beta in your templates or modules.](https://knowledge.hubspot.com/marketing-email/create-programmable-emails) Using programmable content to personalize emails with data from your HubSpot account using HubL. The data sources you can use in a programmable email depend on your HubSpot subscription: - If you have a **_Marketing Hub_** _Professional_ subscription, you can use data from standard CRM objects, such as contacts, companies, and products. - If you have a **_Marketing Hub_** _Enterprise_ subscription, you can also use structured data sources such as [HubDB](/guides/cms/storage/hubdb/overview) tables and [custom objects](https://knowledge.hubspot.com/records/use-custom-objects). This data can be filtered based on the contact properties of the recipient. For example, a real estate website could have prospects fill out a form with their home needs. The form submission could then trigger a workflow that sends the prospect an email with homes they may be interested in. ## Email sending limits You can include the [crm_object](/reference/cms/hubl/functions#crm-object), [crm_objects](/reference/cms/hubl/functions#crm-objects), and [crm_associations](/reference/cms/hubl/functions#crm-associations) CRM HubL functions in a programmable email, but any email that includes these specific functions are subject to the following limits that are based on the number of recipients you're sending to: <table> <tbody> <tr> <td> <strong>Total recipients</strong> </td> <td> <strong>Maximum number of CRM HubL functions</strong> </td> </tr> <tr> <td>500,000</td> <td>1</td> </tr> <tr> <td>250,000</td> <td>2</td> </tr> <tr> <td>165,000</td> <td>3</td> </tr> <tr> <td>125,000</td> <td>4</td> </tr> <tr> <td>100,000</td> <td>5</td> </tr> </tbody> </table> Sending an email that meets or exceeds one of the limits above will delay or cancel the sending of your email. In addition to the limits outlined above, be aware of the additional caveats listed below: - If you clone a programmable email, it cannot be sent while the original is still in a processing state. You should wait at least 1 hour between each email send. - You cannot conduct an A/B test for a programmable email that includes a `crm_object`, `crm_objects`, or `crm_associations` HubL function. Emails sent using these functions have significantly slower send times than other emails, which can produce inaccurate test results. ## 1. Create a programmable email module To create the email module to access your HubDB or custom object data: - In your HubSpot account, navigate to **Marketing** > **Files and Templates** > **Design Tools**. - In the upper left, click **File**, then select **New file**. - In the dialog box, select **Module** for the file type, then click **Next**. Then, select the **Emails** checkbox and enter a **name** for the file. - Click **Create**. - To enable programmable email for the module: - In the inspector on the right, toggle the **Enable module for programmable email beta** switch on. - You can also enable programmable email for a coded email template by adding `isEnabledForEmailV3Rendering: true` to the top of the file. With the module created, you'll then add code to access data from your CRM. The following examples below demonstrate how to query from different data sources. ### Standard objects You can use the HubL functions such as [crm_object](/reference/cms/hubl/functions#crm-object), [crm_objects](/reference/cms/hubl/functions#crm-objects), and [crm_associations](/reference/cms/hubl/functions#crm-associations) to query data from standard objects in your account, such as contacts, companies, or products. The code below uses the [`crm_object`](/reference/cms/hubl/functions#crm-object) HubL function to query the data from a product with an ID of `2444498793` and render the name, description, and price: ```hubl {% set product = crm_object("product", 2444498793, "name,description,price") %} ``` ### Custom objects If you have a _**Marketing Hub** Enterprise_ account, you can query data from a custom object you've created in your account. The code below retrieves data from a custom object named _Property_, returning values (e.g. location, price) stored in the custom object's properties. Note that the example below uses the custom object's fully qualified name as the first argument when invoking the `crm_objects` [HubL function](/reference/cms/hubl/functions#crm-objects). - The fully qualified name begins with the HubSpot account ID (prefixed by `p`), followed by an underscore and the lower-cased plural name of the custom object (e.g., `properties`). - You can retrieve an object's `fullyQualifiedName` by making a `GET` request to the [CRM Objects schema API](/reference/api/crm/objects/custom-objects). ```hubl {% set real_estate_listings = crm_objects("p2990812_properties", "", "listing_name,location, price, address, type") %} {% for home in real_estate_listings.results %} {{ home.address}} <br> {{ home.price }} <br> <img alt="{{ home.name }}" src="{{ home.hero_image }}" style="outline: none; max-width: 100%;" width="260px" /> <br> <hr> {% endfor %} ``` If the name of your custom object contains hyphens (e.g., _My-Custom-Object_), its properties can <u>not</u> be rendered in a programmable email. You can recreate the custom object with the hyphens omitted [directly in your HubSpot account](https://knowledge.hubspot.com/object-settings/create-custom-objects), or you can use the [custom object API](/guides/api/crm/objects/custom-objects). To filter the data returned for each recipient, you can add a `query` parameter, which will filter the results by the recipient's contact properties. View the [full list of filter options](/guides/api/cms/hubdb#filter-returned-rows). ```hubl {% set query = "price__lte="~contact.budget_max|int~"&price__gte="~contact.budget_min|int~"&city="~contact.city"&order=listing_name" %} {% set real_estate_listings = crm_objects("p2990812_Property", query, "listing_name,location, price, address, type") %} {% for home in real_estate_listings.results %} ... {% endfor %} ``` ### HubDB If you have a **_Marketing Hub_** _Enterprise_ account, you can use data from a HubDB table in your email. The code below uses the [`hubdb_table_rows`](/reference/cms/hubl/functions#hubdb-table-rows) HubL function to retrieve all data from the table. This will list all the real estate properties in the email, outputting the details of each property along with their image. ```hubl {% set real_estate_listings = hubdb_table_rows(1234567) %} {% for home in real_estate_listings%} {{ home.address}} <br> {{ home.price }} <br> <img alt="{{ home.name }}" src="{{ home.hero_image.url }}" style="outline: none; max-width: 100%;" width="260px" /> <br> <hr> {% endfor %} ``` To filter the data returned for each recipient, you can add a `query` parameter, which will filter results by the specified contact properties. View the [full list of filter options](/guides/api/cms/hubdb#filter-returned-rows). ```hubl {% set query = "price__lte="~contact.budget_max|int~"&price__gte="~contact.budget_min|int~"&persona="~contact.hs_persona.value~"&order=listing_name" %} {% for home in real_estate_listings %} ... {% endfor %} ``` In the above example, the contact property _Budget max_ is referenced with `contact.budget_max`, while _Persona_ is referenced with `contact.hs_persona.value`. This is because _Persona_ is an [enumeration property](https://knowledge.hubspot.com/properties/property-field-types-in-hubspot), which requires an additional `.value` to parse the property's value, while other property types do not. ## 2. Add the module to an email With the module published, you'll now add it to the body of the drag and drop email. - In your HubSpot account, navigate to **Marketing** > **Email**. - Select the **email** that you created. - In the left sidebar, under _Content_, click **More**. Find your programmable email module, then drag it into the email body. If you've set up the module to filter data by specific contact properties, the email preview will appear blank. This is because the email tool hasn't been set to preview the email by a specific contact. To preview what the email will look like for a specific contact: - In the upper right, click **Actions**, then select **Preview**. - On the next screen, click the **Preview as a specific contact** dropdown menu, then select a **contact**. You should now see only the data relevant to the recipient, filtered by their contact properties. If an email template or module contains custom code with unresolved HubL, a placeholder module will appear in its place. It's important to always have fallback data to send in the case that there are no HubDB rows or custom object records that meet the criteria you've set. Otherwise, the recipient might receive a blank email. This beta may cause issues with existing templates. It's important to test any changes thoroughly before publishing and sending a live email. ## More HubDB focused tutorials - [Building dynamic pages with HubDB](/guides/cms/content/data-driven-content/dynamic-pages/hubdb) - [How to join multiple HubDB tables](/guides/cms/storage/hubdb/overview) - [How to build a dynamic team member page with HubDB](/guides/cms/content/data-driven-content/dynamic-pages/dynamic-team-member-page) - [How to build multilevel dynamic pages using HubDB](/guides/cms/content/data-driven-content/dynamic-pages/multilevel) ## HubSpot Academy - [Using HubDB and Custom Objects in CMS Hub](https://academy.hubspot.com/lessons/other_data_sources_hubdb_and_custom_objects) # Build a recruiting agency website using GraphQL (BETA) @ Learn how to use GraphQL to power a recruiting agency website (BETA). $ guides & cms/content/data-driven-content/graphql --- # Build a recruiting agency website using GraphQL (BETA) This functionality is in beta and requires your account to be ungated. You can request access by submitting [this form](https://www.hubspot.com/devday2021/private-betas). By participating in this beta you agree to HubSpot's [Developer Terms](https://legal.hubspot.com/developer-terms) and [Developer Beta Terms](https://legal.hubspot.com/developerbetaterms). Please note that this functionality is currently under development and is subject to change based on testing and feedback. This tutorial will walk you through how to set up a recruiting agency website, showcasing how GraphQL queries can pull contact and custom object properties from the HubSpot CRM and use the data to power dynamic website pages. All code referenced in this tutorial is available to browse in [this GitHub repository](https://github.com/HubSpot/recruiting-agency-graphql-theme). If you want to learn more about how GraphQL works and how to construct your own queries, check out the [GraphQL reference guide](https://developers.hubspot.com/docs/cms/data/use-graphql-to-query-your-hubspot-data). Before you get started, make sure you’ve [installed the HubSpot CLI tools](/guides/cms/setup/getting-started-with-local-development). After you’ve installed the CLI tools, create a new directory for this tutorial. Navigate into the new directory, and run `hs init` to configure the project. ## Create and associate CRM custom objects First, you'll need to create _Role_ and _Job Application_ custom objects and associate them so they can be queried from the corresponding website pages. - Download the schemas directory from the GitHub repository and copy it into your project directory. - Using the HubSpot CLI tools, create the two custom objects by running the following commands in the schemas directory: - `hs custom-object schema create job_application.json` - `hs custom-object schema create role.json` - After creating both custom objects, create an association between them: - On the command line, run `hs custom-object schema list` to get the objectTypeId of each object. - Make a `POST` call using the **objectTypeId** of the _Role_ object to `/crm/v3/schemas/{objectTypeId}/associations`. - In the body of your request, enter the **objectTypeId** of the Role and Job Application objects as the values for the _fromObjectId_ and _toObjectId_ fields. For example, if the objectTypeId of the Role object is 2-3206084, and the objectTypeId of Job Application is 2-3206072, then the request body would be: ```json // body example { "fromObjectTypeId": "2-3206084", "toObjectTypeId": "2-3206072" } ``` - Download the [role_data.json file from GitHub](https://github.com/HubSpot/sample-graphql-theme/blob/master/data/role_data.json), then populate the available roles by running the command `hs custom-object create role role_data.json`. - In your HubSpot account, create company records for _HubSpot_, _Spotify_, and _Tesla_, then [manually associate](https://knowledge.hubspot.com/crm-setup/associate-records#associate-individual-records) each of the roles with the corresponding example company. - Before you start creating the assets and pages for the site, download the [src directory from GitHub](https://github.com/HubSpot/sample-graphql-theme/tree/master/src), then upload the theme to your HubSpot account by running the command `hs upload src sample-graph-ql-theme`. ## Create the job application form After you've populated the job listings for your site, you can configure the job application form. This form will prompt users to select the role they're interested in, and submit a cover letter as part of their application. Before you create the form, update the _Job Application_ and _Role_ property settings to allow the _Role identifier_ and _Cover letter_ properties to be used in forms: - In your HubSpot account, click the **Settings icon** in the top right. - In the left sidebar menu, click **Properties**. - Click the **Select an object** dropdown menu, then select **Job Application properties**. - Hover over the _Cover letter_ property in the _Properties_ table, then click **Edit**. - In the right panel, select the **Use in forms, and bots** checkbox. - Click **Save**. - Click the **Select an object** dropdown menu again, select **Role properties**, then repeat the same steps to allow the _Role identifier_ property to be used in forms. - Now that the _Cover letter_ and _Role Identifier_ properties can be used in a form, you can create the form itself: - Navigate to **Marketing** \> **Lead Capture** > **Forms**. - In the upper right, click **Create form**. - Select **Embedded form**, then click **Next**. - Click **Start** to begin creating a form from a blank template. - Click and drag the **First name**, **Last name**, and **Phone number** fields onto the form preview on the right to include them on your form. - In the left panel, scroll down and click the **Job Application properties** and **Role properties** sections to expand them. - Click and drag all of the fields under _Job Application Information_, along with the **Role identifier** and **Title** fields under _Role Information_, onto your form. - Set the _Job title_, _Role identifier_, and _Title_ fields to be [hidden](https://knowledge.hubspot.com/forms/pass-contact-property-values-with-hidden-form-fields), since these fields will be auto-populated by a workflow you'll set up in the next step. When you're done configuring your form, enter a **name** for the form, then click **Update** in the top right. ## Set up a workflow for submitted applications Now that you've created the job application form, you need to set up a workflow to define the default state of a submitted job application. - In your HubSpot account, navigate to **Automation** > **Workflows**. - In the top right, click **Create workflow**. - In the left panel, on the _Start from scratch_ tab, select **Job application-based**, then click **Next**. - Click **Set Enrollment Trigger** to set the enrollment criteria: - In the right sidebar, under _Filter type_, select **Job application**. - Click **Object ID**, select **is known**, then select **Apply filter**. Click **Save**. - Click the **\+ plus** icon to create an action. - In the right sidebar, under _Property management_, select **Set property value**. - Click the **Property to set** dropdown menu, then select **Application Status**. - Click the **Application Status** dropdown menu, then select **Applied**. - Click **Save**. - Enter a **name** for your workflow, then click **Review and publish**. - Under _Enrollment_, select **No, only enroll contacts who meet the trigger criteria after turning the workflow on**. - In the top right, click **Turn on**. ## Create role listing and detail pages Once you've created your workflow, you can create the pages where users can view roles and submit applications for jobs they're interested in. To create the role listing page: - In your HubSpot account, navigate to **Marketing** > **Website** \> **Website Pages**. - In the top right, click **Create**, then select **Website page**. - Select the **Role listing - GraphQL** template. - In the dialog box, enter an **internal name** for the page. - Click the **Settings** tab. - Enter a **Page title**, then enter **roles** as the content slug. - Click **Publish** in the top right. With your role listing page ready to go, create the role details page: - In the top right of the website pages dashboard, click **Create**, then select **Website page**. - Select the **Role Details** template, then enter an **internal name** for your page in the dialog box. - Select the **Role details - GraphQL** template, then enter an **internal page name** in the dialog box. - Click the **Settings** tab. - Enter a **Page title**, then enter **roles/role** as the content slug. - Click **Publish** in the top right. ## Create applications page With your role pages published, the next step is to create a page where an applicant can view their submitted applications and check on the status of each one. Since this page will be private to each applicant, you'll need to create a membership list to handle registration. Learn more about how membership works in [this article](/guides/cms/content/memberships/overview). To create the membership list: - In your HubSpot account, navigate to **Contacts** > **Lists**. - In the top right, click **Create List**. - Enter a **name** for your list, then click **Next**. - Click **\+ Add filter**. - In the right panel, configure the filter: - Select **Job Application properties**. - Click **Object ID**. - Under _Object ID_, click the **dropdown menu** and select **is known**. - Click **Save Group**. - In the top right, click **Save list**. When a job application is submitted, the associated contact will automatically be added to the list you created. You can now create the application listing page and the application details page. To create the application listing page: - In your HubSpot account, navigate to **Marketing** > **Website** > **Website Pages**. - Click **Create page**, then select **Website page**. - Select the **Application Listing - GraphQL** template. - Enter an **internal name** for your page. - Click and drag the **Existing Application Listing** module into your template. - Click the **Settings** tab. - Enter a **page title**, then enter **my-applications** for the content slug. - Scroll down and click **Advanced options** to expand the section. - Under _Control Audience Access for this page_, select **Private - Registration required**. - Click the **Select lists** dropdown menu and select the membership list you created. - In the top right, click **Publish**. To create the application details page: - Back on the website pages dashboard, click **Create**, then select **Website page**. - Select the **Application details - GraphQL** template, then enter an **internal name** for the page. - Click and drag the **Application Details** module into your template. - Click the **Settings** tab. - Enter a **page title**, then enter **application-details** for the content slug. - In the top right, click **Publish**. ## Edit website header and menu items The final step is to finalize the website header and create a menu that links between each of the pages. All of the application and role pages share a header, so you can edit the global module and the changes will apply to each of the pages. - On the website pages dashboard, hover over one of the pages you created and click **Edit**. - Click the **website header**. In the dialog box, click **Open in global content header**. - In the left pane, click the **Content** tab, then click **Primary navigation**. - Click **Edit**. - Add two new menu items: - Click **Add menu item**, then select **Add page link**. - Enter **Roles** for the _Menu item label_, then click the **Select page** dropdown menu and select **Roles listing page**. - Repeat the same steps to add a menu item for the application listing page. - Click **Publish**. With your menu updated, your recruiting agency website is ready to go. To test out the site, open a new incognito window and navigate to the URL of the role listing page. You can submit an application for a role and confirm that the associated application appears on the application listing page you created. If you want to review the GraphQL queries that power this example site, you can find them in the _data-queries_ directory of the GitHub repository or you can navigate to the _sample-graphql-theme_ in the design manager. To learn more about GraphQL, check out the reference documentation [here](https://developers.hubspot.com/docs/cms/data/use-graphql-to-query-your-hubspot-data). # Query HubSpot data using GraphQL @ Learn how to use GraphQL to query your HubSpot data $ guides & cms/content/data-driven-content/graphql --- # Query HubSpot data using GraphQL [GraphQL](https://graphql.org/) is a data query language that provides a unified way to access your HubSpot CRM and HubDB data to create personalized and data-driven experiences. Based on your HubSpot subscription, you can use GraphQL to fetch data for the following tools: - If you have a _**Content Hub**_ _Professional_ or _Enterprise_ subscription, you can use GraphQL to fetch data for [website pages](/guides/cms/content/data-driven-content/graphql/use-graphql-data-in-your-website-pages). - If you have a _**Sales Hub**_ or _**Service Hub**_ _Enterprise_ subscription, you can [use GraphQL to fetch data](https://developers.hubspot.com/blog/hello-world-creating-your-first-react-graphql-custom-card-for-hubspots-crm) for [UI extensions (BETA)](/guides/crm/ui-extensions/create). Below, learn more about the benefits of using GraphQL, how to structure and test GraphQL queries, and what limits are placed on the complexity of your queries. If you want to see an example of how GraphQL queries work in practice, check out the [Sample GraphQL theme tutorial on GitHub](https://github.com/HubSpot/recruiting-agency-graphql-theme). GraphQL files are not currently supported for modules or templates used for [marketing emails](https://knowledge.hubspot.com/marketing-email/create-and-send-marketing-emails-with-the-updated-classic-editor). ## Advantages of using GraphQL The GraphQL API provides a single, unified way to fetch data from different data sources to power your website pages, instead of having to process and aggregate data from disparate REST API endpoints. You can specify the data within GraphQL queries, which allows you to decouple how you're fetching data from the presentational layer of your template and module code. You can then include queries as part of your theme, or connect them directly to your modules, templates, and pages. ## GraphQL schema and queries In HubSpot, the GraphQL schema describes the data sources you're fetching from and the underlying data types available to your HubSpot account. This schema is auto-generated and will automatically update if you add custom properties, create custom objects, or associate object types. You can construct GraphQL queries to fetch data by traversing the schema from your HubSpot account. This approach allows you to specify the data you need for a given page on your website. Each GraphQL query consists of a tree of nested fields: ```graphql query MyQuery { # Root query field CRM { # Child fields contact_collection { items { # Properties email firstname lastname } } } } ``` - **Root query field:** the top-level field of your query that represents your data source. The available data sources are: - **CRM:** records (e.g., contacts, companies, etc.) and other data from your CRM. - **HUBDB:** data from a [HubDB table](https://knowledge.hubspot.com/website-and-landing-pages/create-and-hubdb-tables). - **BLOG:** data from blog posts, authors, and tags. - **KB:** data from your knowledge base, including articles, categories, and tags. - **Child fields:** based on the data source in your root query field, you can then specify the child fields you want to fetch, along with any associated properties. Follow the instructions in the sections below to fetch data from the HubSpot CRM or a HubDB table. You can test and run your query interactively using [GraphiQL](#test-and-run-queries-interactively-using-graphiql), which provides autocompletion and allows you to browse all available fields you can include in your query. ## Query data from your CRM To query data from the HubSpot CRM, include `CRM` as a root query field, then enter a data type as a child field. You can include any of the following data types from the CRM: - Standard and custom objects - Engagements, including calls, tasks, meetings, and notes - Products and line items - Quotes and subscriptions - Marketing events When retrieving data for CRM objects and engagements, values in filters are case-insensitive, with the exception of the `IN` and `NOT_IN` operators. Learn more about filtering with the [CRM Search API.](/guides/api/crm/search#filter-search-results) You can retrieve a single instance of a data type, or enter a data type followed by `_collection` to retrieve a list of objects of that type. You can provide arguments to a query field to fetch a single object or filter a collection of objects. - **Fetch a single instance:** if you're fetching a single instance like an individual contact record, specify one of the object's properties and its associated value by including the property's name as the `uniqueIdentifier`, and the property's value as the `uniqueIdentifierValue`. For example, the following query would fetch a contact with an ID of _753:_ ```graphql query filteredContact { CRM { contact(uniqueIdentifier: "id", uniqueIdentifierValue: "756") { email } } } ``` - **Fetch and filter a collection:** to fetch a collection of instances, add `items` as a child field. To filter the objects in the response, include the `filter` argument, then provide a map of filter operators to their associated values. A filter operator consists of the property you want to filter by, followed by a suffix. For example, the query below would fetch and filter a collection of contacts with _hubspot.com_ email addresses: ```graphql query filteredContactCollection { CRM { contact_collection(filter: { email__contains: "hubspot.com" }) { items { email } } } } ``` If you're using the `__in` [operator](#query-arguments-and-filters) to filter on a [multiple checkbox property](https://knowledge.hubspot.com/properties/property-field-types-in-hubspot), the data in the response will include instances that partially match one or more of the values in your filter argument. For example, if your query field was `contact_collection(filter: {hs_buying_role__in: ["END_USER", "INFLUENCER"]})`, then the response would include contacts who had _End User_ selected as one of their buying roles, but didn't necessarily have _Influencer_ as one of their buying roles. After you've added a data type to your query, you can specify the properties, associations, or metadata you need by adding child fields: - **Add properties to fetch:** enter the internal name of a property to include it in the response of your query. Both default and custom properties are supported. - **Include associations to other data types:** you can also add a collection of instances of a data type associated with the parent data type, such as the companies associated with a contact. To add data from an associated data type: - Include `associations` as a child field of the original data type you entered. - As a child field of `associations`, enter the association you want to include by following the naming scheme of `{ASSOCIATED_DATA_TYPE}_collection__{LABEL_SUFFIX}`_._ - The associated data type can be a built-in association with a standard object, or an association with a custom object. Any associations to custom objects will be prefixed by `p_`. - For [primary associations](https://knowledge.hubspot.com/object-settings/create-and-use-association-labels#manage-company-associations), the label suffix is `primary`. If you [created a custom association label](https://knowledge.hubspot.com/object-settings/create-and-use-association-labels#create-and-edit-association-labels-professional-and-enterprise-only), the label suffix will be lower-cased, with each word in the label separated by an underscore. For example, if your custom association label is _My custom association label_, the label suffix would be `my_custom_association_label`. - For example, if you had a custom object named _House_ associated with the contact object, the association you'd include in your query would be _p_house_collection\_\_house_to_contact_. - Add `items` as a child field of your association, then include any properties you need from each item in the collection. ```graphql query contactsWithAssociatedCompany { CRM { contact_collection { items { firstname lastname associations { company_collection__primary { items { name address } } } } } } } ``` ## Query data from HubDB To query data from HubDB, include `HUBDB` as a root query field, then enter the name of a table to retrieve a single row from that table, or enter a name followed by `_collection` to retrieve multiple rows from that table. When retrieving data from HubDB, values in filters are case-sensitive. Learn more about [retrieving HubDB data](/guides/api/cms/hubdb#filter-returned-rows). You can provide arguments to a query field to retrieve a single row or filter multiple rows from the table: - **Retrieve a single row:** if you're retrieving a single row from a table, specify one of the table's columns as the `uniqueIdentifier` then provide the associated value of the row you want to retrieve as the `uniqueIdentifierValue`. For example, the following query would retrieve a row from an executives table with a role of _CEO_: ```graphql query filteredExecutive { HUBDB { executives(uniqueIdentifier: "role", uniqueIdentifierValue: "CEO") { name email description } } } ``` If the column you provide as the `uniqueIdentifier` has non-unique values, your query will only return the first row, ordered by row ID. - **Retrieve and filter multiple rows:** to fetch multiple rows from a table, add items as a child field. To filter the rows in the response, include the filter argument to your query field, then provide a map of filter operators to their associated values. A filter operator consists of the property you want to filter by, followed by a suffix. For example, the query below would retrieve and filter rows from the executives table whose role contains _O\_\_fficer_: ```graphql query csuiteExecutives { HUBDB { executives_collection(filter: { role__contains: "Officer" }) { items { email } } } } ``` After you've added a data type to your query, you can specify the columns or metadata you need by adding child fields: - **Add columns to retrieve:** enter the name of a column to include it in the response to your query. If one of your columns is a foreign ID and you want to include columns from the associated table: - Include `associations` as a child field of `items`. - As a child field under `associations`, enter the _name_ of the foreign table with the `_collection` suffix, followed by two underscores and the _name_ of the foreign ID column. For example, if the foreign ID column is _recent_posts_, and the name of the foreign table you want to reference is _blog_posts_, then the resulting field to include in your query would be `blog_posts_collection__recent_posts`. - Add `items` as a child field of the foreign table field you just entered, then include any columns you need from the foreign table. For example, to include additional columns from the _executives_ table from the _csuiteExecutives_ query above, along with the title and link from a _blog_posts_ foreign table, you'd update the query to the following: ```graphql query csuiteExecutives { HUBDB { executives_collection(filter: { role__contains: "Officer" }) { items { associations { blog_posts_collection__recent_posts { items { title link } } } email name description role } } } } ``` - **Include metadata fields:** internal properties of the table, such as the ID or the timestamp of the last update to a row, are prefixed with `hs_`. #### Retrieving data from child tables If you've [configured dynamic pages to be created from your HubDB tables](/guides/cms/content/data-driven-content/dynamic-pages/hubdb), you can include data from child tables in your GraphQL query. - Add `hs_child_table_collection` as a child field of the `items` of the parent table collection. - For each child table that you want to include, include a nested child fragment, which consists of the spread operator (i.e., `...`) combined with a _type condition_, which includes the `on` keyword and the _name_ of the child table collection. For example, if you wanted to update the `csuiteExecutives` query from above to include columns from the `team` and `recent_posts` child tables: ```graphql query csuiteExecutives { HUBDB { executives_collection(filter: { role__contains: "Officer" }) { items { email name description role hs_child_table_collection { ... on teams_collection { name email summary } ... on recent_posts_collection { title summary link } } } } } } ``` #### Retrieving unpublished data from a table You can include unpublished data from a table by providing `{draft: true}` as a query argument to the name of the table that you're retrieving. If you've added new columns since you last published the table, they will <u>not</u> be included. ```graphql query csuiteExecutives { HUBDB { executives_collection(draft: true, filter: { role__contains: "Officer" }) { items { email name description role hs_id hs_child_table_id } } } } ``` ## Query blog data To query blog data, include `BLOG` as a root query field, then enter a data type as a child field. You can include any of the following data types: - Post and post collection - Author and author collection - Tag and tag collection A complete list of the available blog fields is included in the [GraphiQL tool](/guides/cms/content/data-driven-content/graphql/query-hubspot-data-using-graphql#test-and-run-queries-interactively-using-graphiql). ```graphql query MyQuery { # Root query field BLOG { # Child fields post_collection { items { # Properties name post_body } } } } ``` - Retrieve a single blog post: retrieve the title and content of a single blog post by using its ID as the `uniqueIdentifier`. ```graphql query MyQuery { BLOG { post(uniqueIdentifier: "id", uniqueIdentifierValue: "123") { name post_body } } } ``` - Retrieve blog author fields from a post: retrieve the title and content of a single blog post, along with the blog author's name and email address, by using its ID as the `uniqueIdentifier`. ```graphql query MyQuery { BLOG { post(uniqueIdentifier: "id", uniqueIdentifierValue: 123) { name post_body blog_author { full_name email } } } } ``` - Retrieve a filtered collection of blog posts: retrieve a set of blog posts filtered by the post title. ```graphql query MyQuery { BLOG { post_collection(filter: { name__icontains: "blog" }) { items { name post_body } } } } ``` - Retrieve drafted blog posts: retrieve a collection of blog posts that are not currently published by specifying `draft: true`. ```graphql query MyQuery { BLOG { post_collection(draft: true, filter: { name__icontains: "blog" }) { items { name post_body } } } } ``` ## Query knowledge base data GraphQL querying is only supported for the [latest version of the knowledge base tool](https://knowledge.hubspot.com/knowledge-base/create-and-customize-a-new-knowledge-base). You will not be able to query data from older knowledge bases which weren't created using the latest toolset. To query blog data, include `KB` as a root query field, then enter a data type as a child field. You can include any of the following data types: - Knowledge base and knowledge base collection - Articles and article collection - Category and category collection - Tag and tag collection A complete list of the available blog fields is included in the [GraphiQL tool](/guides/cms/content/data-driven-content/graphql/query-hubspot-data-using-graphql#test-and-run-queries-interactively-using-graphiql). ```graphql query MyQuery { # Root query field KB { # Child fields knowledge_article_collection { items { # Properties name post_body } } } } ``` - Retrieve a single knowledge base article: retrieve the title and content of a single blog post by using its ID as the `uniqueIdentifier`. ```graphql query MyQuery { KB { knowledge_article( uniqueIdentifier: "hs_id" uniqueIdentifierValue: "179889003802" ) { hs_name hs_body } } } ``` - Retrieve a filtered collection of knowledge base articles: retrieve a set of articles filtered by the article title. ```graphql query MyQuery { KB { knowledge_article_collection(filter: { hs_name__contains: "cats" }) { items { hs_body hs_name hs_path } } } } ``` ## Test and run queries interactively using GraphiQL You can test queries by using GraphiQL, which is an open-source tool that allows you to run queries interactively and explore which fields are available. - In your HubSpot account, navigate to the [GraphiQL tool](https://app.hubspot.com/login?loginRedirectUrl=https%3A%2F%2Fapp.hubspot.com%2Fshortlink%2Fgraphiql%2F). - In the left pane, create and edit your query by specifying the objects and their associated fields you'll need for your website pages. - To view all available data types, properties, and filters, click **Explorer** at the top of the page to toggle the _Explorer_ view, which will list all available data types and their associated properties. - Click a **data** **type** to automatically add it to your query and view all its associated properties. - Select the **checkbox** next to a property to add it to your query. - If you're writing a query and you're entering an argument for one of your fields, the _Explorer_ pane will auto-populate a list of available filters. - When you're ready to test a query, click the **play icon** at the top of the page. The query's response will appear in the right pane. ## Filter and refine query results You can provide arguments to a collection field in your query to filter, order, or paginate the objects returned in the response. The table below lists the supported arguments: ### Query argument types | Argument | Type | Description | | --- | --- | --- | | `filter` | Input type | A set of filter options to apply on the collection items | | `limit` | Integer | The maximum number of items to fetch.<br /><br />If your query's data source is the `CRM`, the default limit is 10 items, and the maximum limit is 500.<br /><br />If you're using `HUBDB` as a data source, the default limit is 1000 rows, with no maximum limit.<br /><br />If you're using `BLOG` as a data source, the default limit for blog posts and tags is 20, while the default limit for authors is 1000. The maximum limit for blog posts is 300. There is no maximum limit for fetching blog authors or tags. | | `orderBy` | Enum array | The ordering to apply on the fetched items. The default ordering is ascending by the item's `id`. | | `offset` | Integer | The index from where to start fetching items. The default is 0. When paginating results, you should include the offset field as a query argument, as well as including it in the fields of your query. | Create a filter by combining a HubSpot property with a suffix that corresponds to a logical or mathematical operator, separated by two underscores (e.g., `email__contains` or `name__eq`). The supported operators are shown in the table below: | Filter | Postfix | Field types | Example | | --- | --- | --- | --- | | equal | `__eq` | String, Number, Date, DateTime | `email__eq: “user@domain.com”` | | not equal | `__neq` | String, Number, Date, DateTime | `firstname__neq: “Bob”` | | less than | `__lt` | Number, DateTime | `price__lt: 45` | | less than or equal | `__lte` | Number, DateTime | `started__lte: 1633606374` | | greater than | `__gt` | Number, DateTime | `salary__gt: 60000` | | greater than or equal | `__gte` | Number, DateTime | `birthdate__gte: 1633606374` | | contains | `__contains` | String | `name__contains: “Inc.”` | | does not contain | `__not_contains` | String | `address_not_contains: “Miami”` | | in given list | `__in` | Enumeration, Number | `hs_buying_role__in: [“END_USER”, "INFLUENCER"]` | | not in given list | `__not_in` | Enumeration, Number | `status__not_in: [“REJECTED”, "PENDING"]` | | has value / does not have value | `__null` | Any type | `email__null: false` | ### Paginating results By default, if the `limit` field isn't included in your query, the maximum number of items returned in the results will be 10. You can increase this limit by providing the `limit` query argument within the parantheses following the collection name of your query, subject to the data source maximums described in the [table above](#query-argument-types). You can also provide the `offset` query argument within the parantheses of your query collection name, which specifies the index from which to start fetching items. For example, the following query would start fetching results from an index of 10 and continue until the query's limit of 30 is reached: ```graphql CRM { contact_collection(offset: 10, limit: 30) { offset limit total items { firstname lastname } } } ``` By also including the `offset`, `limit`, and `total` fields within the query itself, these values will be returned in the results, which can help you make any additional queries for any remaining data. ### Combine multiple filters and use conditional logic You can combine multiple filters in your query by defining filter groups. Each filter group is defined with curly brackets (e.g., `{}`), and the filters within the resulting group will be `AND`ed together. #### Use OR logic when querying CRM data If you're querying for CRM data, you can apply OR logic to a list of filter groups using the `OR` operator, followed by a list of comma-separated filter groups within square brackets (e.g., `[]`). For example, if you want to filter a list of _Job_ custom objects to include records with a status of either "full-time" OR "contract", then the resulting filter would be: ```graphql CRM { p_job_collection(filter: { OR: [ {status__eq: "full-time"}, {status__eq: "contract"} ] }) { items { # Job child fields (e.g., name, id, etc.) } } } ``` Each additional condition that you want to `OR` together must be specified as a separate filter group, even if the filter name is the same (i.e., `status__eq` in the example above). The same format would apply if you want to `OR` together different filter types. For example, if you want to filter job records to include jobs with a "full-time" status _OR_ jobs in the department "engineering", then the resulting query would be: ```graphql CRM { p_job_collection(filter: { OR: [ {status__eq: "full-time"}, {department__eq: "engineering"} ] }) { items { # Job child fields (e.g., name, id, etc.) } } } ``` You can combine the implicit `AND` logic of filters within a filter group with the OR logic between different filter groups to create more complex filters. For example, if you wanted to represent the following logical expression as a GraphQL filter: `status="full-time" AND ((department="engineering" AND salary >= 80000) OR (department="sales" AND salary>=100000))` The resulting filter argument in your query would be: ```graphql CRM { p_job_collection(filter: { status__eq: "full-time", OR: [ {department__eq: "engineering", salary__gt: 80000}, {department__eq: "sales", salary__gt: 100000} ] }) { items { # Job child fields (e.g., name, id, etc.) } } } ``` - Although the `OR` operator appears as a checkbox in the _Explorer_ side panel of [GraphiQL](#test-and-run-queries-interactively-using-graphiql), selecting the checkbox will cause your query to be invalid due to the way GraphiQL autocompletes the query argument. You should instead follow the steps above to structure your query's filtering logic manually. - The `OR` operator is <u>not</u> supported when querying for HubDB data. ## Query complexity and account limits To ensure optimal performance for querying your data, HubSpot enforces several limits on your queries, including a a limit on the maximum items returned in a single query, as well as the aggregate complexity of your GraphQL queries. ### Limit on maximum items returned in a query Individual GraphQL queries that retrieve CRM data are subject to a limit of 500 items returned in a query (e.g., up to 500 contacts can be retrieved in an individual query). There is no maximum limit to retrieving rows from a HubDB table. Learn more about manually specifying a custom limit to the items returned in your query using the `limit` field in the _[Query argument types](#query-argument-types)_ table above. ### Query complexity and account limits HubSpot also enforces aggregate complexity limits, based on factors such as the total number of objects and their associated properties in your query. Likewise, since HubSpot needs to make a new internal API request for each top-level object, complex queries with nested associations incur an additional cost to execute. The following factors are assigned point values and used to calculate a resulting complexity score: - **Internal API request:** 300 points - **Object retrieved:** 30 points - **Requested property with a value:** 3 points - **Requested property without a value:** 1 point HubSpot will multiply the number of occurrences of each factor listed above by its corresponding point value, then sum each of these subtotals to arrive at a final complexity score. For example, the following query retrieves contacts, their associated companies, and the tickets associated with those companies: ```graphql query myQuery { CRM { contact_collection { items { hs_object_id firstname lastname email company associations { company_collection__primary { items { hs_object_id name domain country annualrevenue phone associations { ticket_collection__primary { items { hs_object_id createdate content created_by hs_pipeline hs_pipeline_stage closed_date } } } } } } } } } } ``` Assuming that the above query retrieves 10 contacts, with 6 companies per contact, and 15 tickets per company, the complexity score would be calculated as follows: - Points used for `contact_collection`: - One internal API request is made (300 points). - 10 contacts are retrieved (10 \* 30 = 300 points). - For each contact, five properties with values are retrieved (10 \* 5 \* 3 =150 points). - **Total points:** 750 - Points used for `company_collection__primary`: - One internal API is made per contact retrieved, totaling 10 requests (10 \* 300 = 3,000 points). - Six companies are retrieved (6 \* 30 = 180 points). - Five properties are retrieved with a value per company (6 \* 5 \* 3 = 90 points). - One property is returned with no value per company (6 \* 1 = 6 points). - **Total points:** 3,276 - Points used for `ticket_collection__primary`: - One internal API request is made per company per contact retrieved (10 \* 6 \* 300 = 18,000 points). - 15 tickets are returned (15 \* 30 = 450 points). - Seven properties are returned with a value per ticket (15 \* 7 \* 3 = 315 points). - **Total points:** 18,765 Adding the points from the three constituent parts of the query, the final complexity score for the query totals to 22,791 points. In addition to the complexity limits on individual queries, HubSpot also enforces an account-wide limit for the complexity of the queries you include when using the [external GraphQL API endpoint](#use-a-graphql-query-in-an-api-request): - Each individual query has a maximum of 30,000 points available. If this limit is reached while the query is running, all further execution is blocked and all object instances fetched up to that point are returned. - Each HubSpot account is allowed up to 500,000 points per rolling minute when using the [GraphQL API endpoint](#use-a-graphql-query-in-an-api-request). If this limit is reached, further requests to the endpoint will receive an HTTP status code of `429 Too Many Requests`. The only exception is that templates and modules with GraphQL queries loaded by site visitors <u>will continue</u> to function to preserve website user experience. All other ad-hoc uses of the endpoint will no longer function (e.g., during template/module development, using the GraphiQL UI, and UI extensions using GraphQL queries). Accumulated points from the previous minute are <u>not</u> carried over to the next. - The normal burst limits detailed in [HubSpot's API usage guidelines](/guides/apps/api-usage/usage-details#rate-limits) are <u>not</u> applicable to querying data using GraphQL. When HubSpot retrieves the data specified in your GraphQL query, the response contains details on the complexity score for your query within the `extensions` field. - The complexity score for the query you provided in your request will be broken down within the `query_complexity` field. - If you're using the [external GraphQL API endpoint](#use-a-graphql-query-in-an-api-request), the extensions field will also include account limit information within `rate_limit_info`. ```json "extensions": { "rate_limit_info": { "interval_in_seconds": 60, "max_points": 500000, "remaining_points": 477209, } "query_complexity": { "max_points": 30000, "used_points": 22791, "points_for_internal_api_requests": { "count": 71, "weight": 300, "used_points": 21300 }, "points_for_objects_retrieved": { "count": 31, "weight": 30, "used_points": 930 }, "points_for_properties_with_value": { "count": 185, "weight": 3, "used_points": 555 }, "points_for_properties_without_value": { "count": 6, "weight": 1, "used_points": 6 } } } ``` ## Use a GraphQL query in an API request In addition to using GraphQL in your [website pages](/guides/cms/content/data-driven-content/graphql/use-graphql-data-in-your-website-pages), you can also use a GraphQL query in your integration, a [Jamstack site](https://jamstack.org/), or in a [custom code action in the workflows tool](https://developers.hubspot.com/docs/reference/api/automation/custom-code-actions) to specify the data you need from HubSpot without having to make separate requests to multiple API endpoints. ### Scope requirements To make an API request to the `/collector/graphql` endpoint, the following [scopes](/guides/apps/authentication/working-with-oauth#scopes) are required: ``` collector.graphql_schema.read collector.graphql_query.execute ``` You must also include any scopes that correspond to the data sources in your query. For example, the `crm.objects.contacts.read` scope is required if you're fetching contacts in your query. ### Make an API request to the /collector/graphql endpoint Once a user has authorized the required scopes above, you can make a `POST` request to the `/collector/graphql` endpoint and include the following fields: | Parameter | Type | Description | | --- | --- | --- | | `operationName` | String | A label for the query included in the request | | `query` | String | The GraphQL query to execute | | `variables` | JSON object | A JSON object containing any variables you want to pass to the query | For example, if you created a _House_ custom object, and you wanted to query for a collection of _House_ records in Miami, the request body would contain: ```json // POST request body to https://api.hubapi.com/collector/graphql { "operationName": "houses" "query": "query houses ($city: String) { CRM {house_collection(filter: {city__eq: $city}) { items { asking_price bedrooms bathrooms city}}}}", "variables": {"city": "Miami"} } ``` Example return payload: ```json { "data": { "CRM": { "house_collection": { "items": [ { "asking_price": 800000, "bedrooms": 3, "bathrooms": 2, "city": "Miami" } { "asking_price": 940000, "bedrooms": 2, "bathrooms": 2, "city": "Miami" } ] } } } } ``` ## Further reading Once you're familiar with how to create a GraphQL query, check out [this guide](/guides/cms/content/data-driven-content/graphql/use-graphql-data-in-your-website-pages) to using a query on your website pages (**_Content Hub_** _Professional_ or _Enterprise_ only). And if you're enrolled in the [UI extensions beta](/guides/crm/ui-extensions/create), learn more about [using GraphQL to fetch data for UI extensions](https://developers.hubspot.com/blog/hello-world-creating-your-first-react-graphql-custom-card-for-hubspots-crm). # Use data from a GraphQL query in your website pages @ Learn how to use data from a GraphQL query in your website pages. $ guides & cms/content/data-driven-content/graphql --- # Use data from a GraphQL query in your website pages [GraphQL](https://graphql.org/) allows you to create data queries that access your HubSpot CRM and HubDB data to create personalized and data-driven experiences for your website visitors. Learn more about how GraphQL works and how to create a query [here](/guides/cms/content/data-driven-content/graphql/query-hubspot-data-using-graphql). You can learn more about building data-based pages in HubSpot Academy's [Data-Driven Content course](https://app.hubspot.com/academy/tracks/1148948/intro). Once you're familiar with how GraphQL works and you've written a query that specifies the data you'll need, you can use the query in your theme. - Save your query in a file with the .graphql extension. It's recommended you keep GraphQL query files in a separate directory at the root of your theme to keep them organized and make them easier to reference with a relative path. - Upload a theme that includes the query file [using the HubSpot CLI tools](/guides/cms/tools/local-development-cli#upload), or [through the design manager](https://knowledge.hubspot.com/design-manager/a-quick-tour-of-the-design-manager#creating-new-templates-and-files) in your HubSpot account. Once you've uploaded your GraphQL files, you can bind them to a module or template, which will make the query's data available to the rendering context of the module or template. ## Bind a data query to a template To bind a GraphQL query to a template so you can reference the data in your code, add a `dataQueryPath` template annotation and provide the path to the associated GraphQL query file. You don't need to include the file's _.graphql_ extension in the path. For example, if you want to bind a query file to a template that renders the query's results on the page, the top of your template's code would include the following: ```html <!-- templateType: page isAvailableForNewContent: true label: Contact profile page - GraphQL dataQueryPath: ../data-queries/contact_info --> ``` You can then reference the response to the data query in your template and module code from the `data_query` variable in HubL. The example below defines a HubL variable to store available roles: ```hubl {% set contactData = data_query.data.CRM.contact %} ``` ## Bind a data query to a module You can bind a GraphQL query to a module using the design manager. - In your HubSpot account, navigate to **Marketing** > **Files and Templates** > **Design Tools**. - At the top of the finder, click the File dropdown menu and select **New file**. - In the dialog box, click the **What would you like to build today?** dropdown menu and select **GraphQL**. - Click **Next**. - Enter a **file name**, then confirm the location for your GraphQL file. - Copy and paste your query into the editor, then click **Publish changes**. - Navigate to the module you want to use with your query. Under _Linked files_, click the **GraphQL file** dropdown menu, then select your query. You can then reference the response of your query in your module code using the `data_query` variable. The example below defines a HubL variable to store the queried data for a collection of contacts: ```hubl {% set contactCollectionData = module.data_query.data.CRM.contact_collection %} ``` If you're developing locally, you may prefer to bind a GraphQL query directly to a module by including a `data_query_path` parameter in your module's meta.json file. It's recommended you define your module within a _modules_ directory at the root of your theme to make it easier to reference your query with a relative path. - Using the HubSpot CLI tools, [fetch](/guides/cms/tools/local-development-cli#fetch) the module's directory. - Add the `data_query_path` parameter and specify the path to the associated GraphQL query file, relative to the location of the meta.json file. For example, if the query file is in a _data-queries_ directory at the root of your theme, your meta.json file would include the following: ```json // meta.json { "data_query_path": "../../data-queries/contactsQuery" } ``` - Upload the updated meta.json file to your HubSpot account. ## Pass dynamic context into a query If your query requires context that is dynamic to a given request, such as data from a contact who's visiting one of your pages, you can pass [HubL variables](/reference/cms/hubl/variables) in your GraphQL queries. - First, define the variable in a single line comment at the top of your query using the `#` character. The convention for GraphQL variable names is to prefix them with a `$` character. For example, to define a variable for the ID of a contact: `# $contact_id = "{{ request.contact.contact_vid || ''}}"` - Include your variable and its type as a parameter in your query definition. If you want your variable to non-nullable, add an `!` after its type. To pass the `$contact_id` variable above into a query to show contact info for a given contact, your query definition would begin with: `query contact_info($contact_id: String!)` - You can then reference your variable and provide it as an argument to any of the filters in your query. Using the `$contact_id` variable from above, the query below would provide the first name, last name, and email of a contact visiting a page: ```graphql # label: "Contact info page" # description: "Show a contact's name and email" # $contact_id: "{{request.contact.contact_vid }}" query contact_info($contact_id: String!) { CRM { contact(uniqueIdentifier: "id", uniqueIdentifierValue: $contact_id) { _metadata { id } firstname lastname email } } } ``` - You can also pass URL query string parameters as arguments to your GraphQL query using the `request.query_dict` HubL variable. For example, if you have a page URL that contained _/contacts?location=Boston&department=marketing_, and wanted to use the URL query parameters in your GraphQL query to filter contacts by location and department, you could use the following GraphQL query: ```graphql # label: "Contact listing page with filters" # description: "Filter contacts by location and department" # $location: "{{ request.query_dict.location || ''}}" # $department: "{{ request.query_dict.department || ''}" query contact_listing_page($location: String!, department: $String) { CRM { contact_collection(filter: {location__eq: $location, department_eq: $department) { items { _metadata { id } firstname lastname email } } } } ``` ## Create dynamic pages using query data You use directives in your GraphQL queries to dynamically generate pages based on certain properties from your CRM data. Each dynamic page will pull its metadata from the directives you annotate in your query. The values you define for each directive should uniquely identify the content of the page to stand out in search results. This metadata includes the following directives: | Dynamic page directive | Description | | --- | --- | | `web_page_canonical_url` | The canonical URL of the page | | `web_page_featured_image` | The page's featured image, which will appear when the page is shared. | | `web_page_meta_description` | The page's meta description | | `web_page_title` | The page's title | In addition to these directives, you'll also need to choose a custom property to use as a unique identifier in your query if you're querying CRM data. This property must be configured with `"hasUniqueValue": true`. Existing properties cannot be updated to have this parameter, so you'll need to [create a new property through the API](/guides/api/crm/properties) if you don't already have one. ### Create a query and module for your dynamic pages To create a query you can use with dynamic pages: - Include a `label` and `description` for your query as single line comments at the top of your query. Then add another single line comment for an identifier to associate with the dynamic slug of your page: - Set the name of your identifier to the `dynamic_slug` field provided by the `request.path_param_dict` HubL variable. - For example, if you're creating a query to dynamically create profile pages for your contacts, and you want to use a custom property named `profile_full_name` as the slug for each contact's profile page, the following comment would be included above the query definition: `#$profile_full_name: {{ request.path_param_dict.dynamic_slug }}` - Include an ID field in your query that matches your data source: - If you're querying `CRM` data, include the `hs_object_id` field. - If you're querying data from a `HUBDB` table, include the `hs_id` field. - Annotate the fields of your query with the corresponding metadata directive from the table above, prefacing each directive with the `@` character. For example, to configure the title, meta description, and featured image for a contact profile page: ```graphql # label: "Contact profile" # description: "Contact profile query for showing details for a single contact" # $profile_full_name: {{ request.path_param_dict.dynamic_slug }} query contact_profile_page($profile_full_name: String!) { CRM { contact( uniqueIdentifier: "profile_full_name" uniqueIdentifierValue: $profile_full_name ) { hs_object_id email profile_full_name @web_page_title profile_description @web_page_meta_description profile_picture_url @web_page_featured_image } } } ``` - Save your query in a file with the .graphql extension. As you design your module, you can access the data from your query using the `data_query` HubL variable. ### Create a listing page query and module Depending on your use case, you may want the dynamic page's root URL to display a listing of your object's records. In the example above, a website visitor who navigated to _https://website.com/profiles_ would be presented with a listing of all contact profiles. To set up a listing page: - Create another GraphQL query to retrieve a collection of the associated object records you'll need for the listing page. - Add a label, description, and any other variables you need to pass to your query as single line comments at the top of your query. - Include any [filters](#query-arguments-and-filters) you might need as arguments to your collection field. ```graphql # label: "Profile listing" # description: "Contact profile listing query for showing all profiles with a "hubspot.com" email address" query contact_listing { CRM { contact_collection(filter: { email__contains: "hubspot.com" }) { email profile_full_name } } } ``` - Save your listing page query with the .graphql extension, then follow the instructions above to bind your query to a listing page module. ### Create website pages Once you've created your queries and bound them to their associated modules, you can use the modules in a listing page and a details page. - To create a listing page: - Create a new page, selecting a page template that has a drag and drop area or flexible column. - In the page editor, click the **Settings** tab. - Enter a **page title**. - Under _Page URL_, enter a **content slug** that corresponds to the collection that you're retrieving for your dynamic pages. Following the example above, the content slug would be `/profiles`. - Enter a **meta description** for the listing page, then click the **Content** tab to return to the editor. - In the left sidebar, in the _Add_ tab, search for your listing page module, then drag it into the page editor. - After you're done designing your page, publish it by clicking **Publish** in the upper right. - Next, set up the details page: - [Create a new page](https://knowledge.hubspot.com/website-pages/create-and-customize-pages), selecting a page template that has a drag and drop area or flexible column. - In the page editor, click the **Settings** tab. - Enter a **page** **title**. - Under _Page URL_, click the **pencil icon** to edit the page's URL. Set the URL to where you want your listing page to appear. In the example above, the listing page will be at: `/profiles`, so the full content slug for the page should be `/profiles/[:dynamic-slug]`. - Click **Advanced options**, then scroll to the _Dynamic pages_ section. - Under _Dynamic pages_, click the **Data source** dropdown menu, then select the **label** that matches the one you included at the top of your GraphQL query. - Click the **Content** tab at the top of the page to return to the editor. - In the left sidebar, in the _Add_ tab, search for your details module and drag it into the page editor. - When you're ready, publish your page by clicking **Publish** in the upper right. ## Further reading Learn more about how to construct, test, and refine your GraphQL query [here](/guides/cms/content/data-driven-content/graphql/query-hubspot-data-using-graphql). # Get started with serverless functions @ Create your first serverless function on the HubSpot CMS, and get a response from it. $ guides & cms/content/data-driven-content/serverless-functions --- # Get started with serverless functions for the CMS Serverless functions provide a way to execute JavaScript through HubSpot on the server-side, preventing it from being exposed to the client. This can be especially important for making API requests that contain sensitive information, such as an API key or other credentials. When the function is invoked, HubSpot executes the function on its back-end, which also means you don't need to provide your own back-end servers to make requests for your CMS website. HubSpot’s serverless functions use the [NodeJS](https://nodejs.org/en/about/) runtime. In this guide, you'll walk through creating a simple serverless function that fetches a quote from a third-party service, then displays that quote on a website page. To build and deploy the serverless function to your account, you'll use a HubSpot developer project. While the previously documented method of uploading serverless functions to the design manager is still supported, it's recommended to use developer projects, as they include a private app for easier authentication while also allowing for third-party dependencies. The content of this guide has been updated to use the newest method. You can find reference information for design manager-based serverless functions in the [reference section](/reference/cms/serverless-functions/serverless-functions). For a high-level review of serverless functions, check out the [serverless functions overview](/guides/cms/content/data-driven-content/serverless-functions/overview). You can also check out the [HubSpot Developers YouTube channel](https://www.youtube.com/watch?v=N0arXQOyfXo) for a walkthrough of using serverless functions in a CMS project. ## Prerequisites Before starting this tutorial, you'll need: - A _**Content Hub** Enterprise_ account, or a [CMS developer sandbox account](https://app.hubspot.com/signup-hubspot/cms-developers). - The [HubSpot CLI](/guides/cms/tools/hubspot-cli/cli-v7#install-the-cli) (latest version recommended), which will need to be installed and authenticated with the account you're using. To check which accounts are authenticated, you can run `hs accounts list`. Learn more about [getting started with local development](/guides/cms/setup/getting-started-with-local-development). ## 1. Create a project locally Start by creating a project locally so that you can build and deploy it to your account. - In the terminal, navigate to the directory where you'll be storing your project using the `cd` command. ```shell cd Documents/Dev/serverless-function-project ``` - Run `hs project create` to create a new project. ```shell hs project create ``` - Follow the terminal prompts to create your project. For the template, select **Create an empty project (no template)**. - Navigate into the new project directory using the `cd` command. For example, if you named your project _my new project_: ```shell cd my-new-project ``` ## 2. Add a serverless function to the project Once your project has been created, open it in your preferred editor, such as [VS Code](/guides/cms/setup/install-and-use-hubspot-code-extension). HubSpot will have automatically created a project directory along with an empty `src` directory and a `hsproject.json` configuration file. To add a serverless function to the project, create an `app` directory inside the `src` directory, then add the following directories and files within it: - `app/app.json`: the app configuration file. - `app/app.functions`: the serverless function directory. You can use any name you'd like, as long as it ends in `.functions`. - `app/app.functions/function.js`: the JavaScript code that will be executed when the function is invoked. - `app/app.functions/serverless.json`: the serverless function configuration file. - `app/app.functions/package.json`: includes necessary dependencies. - Copy the example code below into your respective local `app.json`, `function.js`, `serverless.json`, and `package.json` files. Under each `.json` code block, you'll also find a table containing field definitions. ```json { "name": "Serverless function app", "description": "This app runs a serverless function to fetch a quote using the Zen Quotes API.", "scopes": ["crm.objects.contacts.read", "crm.objects.contacts.write"], "uid": "serverless-function-app", "public": false } ``` | Field | Type | Description | | --- | --- | --- | | `name` | String | The name of the app, which will display in HubSpot's UI. | | `description` | String | The app's description, which will display in HubSpot's UI. | | `scopes` | Array | The scopes that the app has access to for authenticating requests with the private app access token. The two scopes above are the minimum required scopes. No additional scopes are added for this tutorial, as you won't need to use the private app access token for the request you'll be making. | | `uid` | String | The app's unique identifier. This can be any string, but should meaningfully identify the app. HubSpot will identify the app by this ID so that you can change the app's `name` locally or in HubSpot without removing historical or stateful data, such as logs. | | `public` | String | Set to `false` for private apps. | This function will fetch a random quote from the [Zen Quotes API](https://zenquotes.io/). ```js const axios = require('axios'); exports.main = async (context) => { try { // Make GET request to the ZenQuotes API const response = await axios.get('https://zenquotes.io/api/random'); // Extract the quote data (first item in the array) const quoteData = response.data[0]; // Log the quote and author to console console.log(`"${quoteData.q}" — ${quoteData.a}`); // Return a properly formatted response with status code and body return { statusCode: 200, body: quoteData, headers: { 'Content-Type': 'application/json', }, }; } catch (error) { // Handle any errors that occur during the request console.error('Error fetching quote:', error.message); // Return an error response return { statusCode: 500, body: { error: 'Failed to fetch quote' }, headers: { 'Content-Type': 'application/json', }, }; } }; ``` ```js { "appFunctions": { "quote-function": { "file": "function.js", "secrets": [], "endpoint": { "path": "fetch-quote", "method": ["GET"] } } } } ``` | Field | Type | Description | | --- | --- | --- | | `quote-function` | Object | An object containing the serverless function's configuration details. This object can have any name, but it must match the associated field name you reference in the `appFunctions` field in your `serverless.json` file. | | `file` | String | The name of the JavaScript file containing the serverless function code to execute. | | `secrets` | Array | An array containing names of secrets that the function will use for authenticating requests. For this tutorial, no secrets are needed. | | `endpoint` | Object | An object containing details about the endpoint that you can hit to invoke the function. The `path` field defines the last value in the `/hs/serverless/<path>` endpoint path, while the `method` field defines the request method. | ```json { "name": "example-serverless-function", "version": "0.1.0", "author": "HubSpot", "license": "MIT", "dependencies": { "@hubspot/api-client": "^7.0.1", "axios": "^0.27.2" } } ``` | Field | Type | Description | | --- | --- | --- | | `dependencies` | Object | The dependencies included for the serverless function. | - After adding the above code, save your changes. ## 3. Upload the project to HubSpot With your changes saved, you can now upload the project to HubSpot to build and deploy the app and serverless function. - In the terminal, run `hs project upload`. ```shell hs project upload ``` - Confirm that you want to create the project in the account. You won't need to confirm again this for after initial creation. The terminal will display the current status of the build and deploy steps as they progress. - Once the upload completes, run `hs project open` to view the project in HubSpot. ```shell hs project open ``` In HubSpot, you're able to view the project details, build and deploy logs, serverless function logs, manage the project and app, and more. Learn more about [managing projects in HubSpot](/guides/crm/developer-projects/create-a-project#view-the-project-in-hubspot). ## 4. Test the function With the serverless function deployed, you can invoke it by hitting its public URL. Serverless functions built with developer projects have the following public URL structure: `https://<domain>/hs/serverless/<endpoint-path-from-config>`. - `<domain>`: you can use any domain connected to the account. For example, if both website.com and subdomain.brand.com are connected to the account, you could call the function using `https://website.com/hs/serverless/<path>` or `https://subdomain.brand.com/hs/serverless/<path>`. - `<endpoint-path-from-config>`: the value in the `path` field in `serverless.json`. Based on the example code provided in this tutorial, the public URL to invoke the function will be: `https://<domain>/hs/serverless/fetch-quote`. In the URL to call the function, the endpoint path is global rather than scoped to the app or project. If you have identical endpoint paths across multiple apps or projects, the most recently deployed endpoint function will take precedence. To view the function's output, navigate to that URL in your browser, replacing the domain with one of your HubSpot-hosted domains. If you haven't connected a custom domain, you can use one of the default domains that HubSpot provides: `<hubId>.hs-sites.com` (e.g., `123456.hs-sites.com`). Your browser should display the data returned by the Zen Quotes API. ## 5. Invoke the function from a website page Now that you've created the serverless function and confirmed that it returns data, implement it into a page to see the data returned in a more realistic context. For this tutorial, you'll create a page template containing some custom JavaScript and HTML to execute the function and display the response data. First, create a new page template: - In your local environment, create a new directory to contain your page template. For the purposes of this tutorial, create this directory outside of the project directory created by `hs project create`. - In the terminal, navigate into that directory using the `cd` command. ```shell cd Documents/Dev/serverless-page ``` - Run `hs create template "serverless-template"` to create a new template named `serverless-template`. ```shell hs create template "serverless-template" ``` - Select the **page** template type. - Open the newly created page template file in your editor, then replace the boilerplate code with the code below. Be sure to replace `<domain>` in the function endpoint URL (`fetch('http://<domain>/hs/serverless/fetch-quote')`) with a domain that's connected to your HubSpot account. ```html <!-- templateType: page label: Serverless function example template isAvailableForNewContent: true --> <!doctype html> <html> <head> <meta charset="utf-8" /> <title>{{ content.html_title }} {{ standard_header_includes }} {% module "page_template_logo" path="@hubspot/logo" label="Logo" %} {{ standard_footer_includes }} ``` - Save the file, then run `hs upload` to upload it to HubSpot. Follow the terminal prompts to select the local source and destination path. For this tutorial, you can just press **Enter** for each prompt to use the default paths. Next, create a new page from the template in HubSpot. - Navigate to the website pages dashboard of your HubSpot account by running the `hs open website-pages`. ```shell hs open website-pages ``` See the full list of available open shortcuts by running `hs open --list`. - In your browser, click **Create** in the upper right to create a new page. - In the dialog box, select a **domain** to use, then assign a **Page name**. The domain of the page will need to match the domain that you're using to invoke the serverless function to avoid cross-origin (CORS) errors. You can either select that domain from the dropdown menu, or update the endpoint URL in the page template code (`fetch('http:///hs/serverless/fetch-quote')`) to use the domain that you select in this step. - Click **Create page**. - On the template selection screen, search for your new template. You can use the label or the file name to search for the template (e.g., "Serverless function example page"). The template will appear under _Other templates_, as it's not part of a theme. - Click **Select template**. - In the page editor, click **Preview** in the upper right. - Click **Open in a new tab**. In the new tab, you should now be able to click the button to fetch and display quotes. ## Next steps Now that you've created and implemented a serverless function that interacts with a third-party API, there are a few ways to continue building up your serverless function usage, such as: - [Including authentication in a request](/reference/cms/serverless-functions/serverless-functions-in-projects#authentication) to make calls to HubSpot APIs or other third-party authenticated APIs. - Implement the button and JavaScript code into a module rather than hardcode it into the page template. This would give you a more portable option, enabling the function to be invoked from any page that the module is added to. To do so, you would create a module, then add the HTML into the module's `module.html` file, and the JavaScript into the `module.js` file. ```html ``` ```js const dataButton = document.getElementById('data-button'); const dataContainer = document.getElementById('data-container'); dataButton.addEventListener('click', function (e) { console.log('Button clicked!'); // Show loading state dataContainer.innerText = 'Loading...'; fetch('http://meowmix-2272014.hs-sites.com/hs/serverless/fetch-quote') .then((response) => { if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } // Parse the JSON response return response.json(); }) .then((data) => { console.log('Raw data received:', data); // Clear container dataContainer.innerText = ''; // Create the paragraph element const newDataElement = document.createElement('p'); newDataElement.innerText = `"${data.q}" — ${data.a}`; dataContainer.appendChild(newDataElement); }); }); ``` # Serverless functions for the CMS @ Serverless functions are written in JavaScript and use the NodeJS runtime. Use them to add new and advanced capabilities to HubSpot CMS websites. $ guides & cms/content/data-driven-content/serverless-functions --- # Serverless functions for the CMS Serverless functions provide a way to execute JavaScript through HubSpot on the server-side, preventing it from being exposed to the client. This can be especially important for making API requests that contain sensitive information, such as an API key or other credentials. When running a serverless function, HubSpot executes the function on its back-end, which also means you don't need to provide your own back-end servers to make requests for your CMS website. HubSpot’s serverless functions use the [NodeJS](https://nodejs.org/en/about/) runtime. For example, you can use serverless functions to: - Collect and store data in HubDB or the HubSpot CRM - Run complex data calculators - Dynamically fetch and display data from external systems - Send form data to external systems Below, learn more about how you can build and use serverless functions on HubSpot's CMS. You may also want to check out the [getting started with serverless functions guide](/guides/cms/content/data-driven-content/serverless-functions/getting-started-with-serverless-functions), as well as the [serverless functions reference documentation](/reference/cms/serverless-functions). ## Limits Serverless functions are intended to be fast and have a narrow focus. To maintain performance, HubSpot serverless functions are limited to: - 50 secrets per account. - 128MB of memory. - no more than 100 endpoints per HubSpot account. - the contentType `application/json` when calling a function. - 6MB per invocation payload, which you might encounter when trying to upload a file with a serverless function, for example. - 4KB for the amount of data that can be logged. When hitting this limit, it's recommended to log after individual actions, rather than the final output. ### Execution limits - Each function has a maximum of 10 seconds of execution time. - Each account is limited to 600 total execution seconds per minute. This means either of these scenarios can happen within one minute: - Up to 60 function executions that take 10 seconds each to complete. - Up to 6,000 function executions that take 100 milliseconds to complete. Functions that exceed those limits will throw an error. Execution count and time limits will return a `429` response. The execution time of each function is included in the [serverless function logs](#viewing-serverless-function-logs). ### Split dependencies Serverless functions don't support splitting JavaScript between multiple files, as they cannot import other functions. Instead, your serverless function JavaScript must be contained in one file. Alternatively, you can use [webpack](https://webpack.js.org/) to bundle your code. Learn more about using webpack as a solution on [HubSpot's Community](https://community.hubspot.com/t5/APIs-Integrations/Import-packages-in-serverless-functions/m-p/346620). ## Building functions Serverless functions consist of a `.functions` directory containing a JavaScript file, a `serverless.json` configuration file, and a `package.json` file. The `.functions` directory can be named anything, as long as it contains the `.functions` suffix. Files stored in this folder are not publicly accessible. - `serverless.json`: the serverless function's configuration file, which includes definitions for the function name, JavaScript file path, included secrets, and endpoint details. - `.js` file: the code that gets executed when the function is invoked. This file can have any name, as long as it ends in `.js`. You'll specify the file name in the `serverless.json` file. - `package.json` (project-based only): for serverless functions built with [projects](/guides/crm/developer-projects/create-a-project), this file configures dependencies needed to execute the function. There are two methods for building serverless functions and making them available for your CMS website: developer projects and the design manager. ### Developer projects Using a developer project, you can build and deploy a private app that executes functions when you hit specified endpoints. Using this method, the serverless functions are defined in and managed through the app locally. This is the recommended path, as project-based serverless functions enable you to include third-party dependencies, as well as use the private app for HubSpot scopes rather than manually managing app tokens through secrets. Within a developer project, the `.functions` directory should be placed within the app directory, as shown below. ```shell project-folder/ │── src/ ├──── app │ ├── app.json │ ├── app.functions/ │ ├── function.js │ ├── package.json │ └── serverless.json └─ hsproject.json ``` ### Design manager If you've built serverless functions for the CMS before HubSpot's developer projects platform was released, this is the method that you'll have used before. Similar to project-based serverless functions, these serverless functions are built locally, but they're then uploaded to the design manager instead of a project. While this method is still supported, including third-party dependencies is not supported, so it's recommended to build serverless functions using projects instead. For serverless functions created in the design manager, the `.functions` directory is uploaded directly to the design manager using the CLI, not through a developer project. The `.functions` directory should contain a `serverless.json` file and a JavaScript file containing the code to execute. For design manager functions, you can prevent accidental edits from within the design manager by locking the folder. To lock a folder, navigate to the design manager, then right-click the **folder** and select **Lock folder**. ## Authentication There are two ways to authenticate requests made by serverless functions: private app access tokens and secrets. - **Private app access tokens:** because project-based serverless functions are bundled inside a private app, you can reference the app's access token directly in your JavaScript code. Requests will have access to the scopes assigned to the private app. For serverless functions built in the design manager, you'll need to create a secret to store a private app access token value. Calls authenticated with a private app access token count against your API call limits. - **Secrets:** create secrets using the CLI to store credentials securely in HubSpot. You can make a secret available to a serverless function by including the secret name in the `serverless.json` file. Learn more about [managing secrets using the HubSpot CLI](/guides/cms/tools/hubspot-cli/cli-v7).
        • You should never return secret values through console logging or as a response, as it will expose the secret to the front-end.
        • Due to caching, it can take about one minute to see updated secret values. If you've just updated a secret but are still seeing the old value, check again after about a minute.
        ## Debugging ### Project-based serverless functions Log messages are produced every time HubSpot executes a serverless function. Below, learn how to access logs in HubSpot and locally using the CLI. #### In-app debugging In HubSpot, you can view a serverless function's log history, including successful requests and errors. To access a serverless function's logs in HubSpot: - In your HubSpot account, navigate to **CRM Development**. - In the left sidebar menu, navigate to **Private apps**. - Select the **private app** that contains the serverless function. - Click the **Logs** tab. - To view logs for a serverless app function, click the **Serverless functions** tab. To view logs for a serverless endpoint function, click the **Endpoint functions** tab. - In each tab, you can view logs for specific requests by clicking the **request**. You can also use the search bar to search by request ID. - In the right panel, you can then click **View log trace** for a more in-depth breakdown of the request. You can also include `console.log()` in your serverless function code for debugging purposes, then view its output in the function log details sidebar. Note that the message will not log directly to your browser console. #### Local debugging Log messages are produced every time HubSpot executes a serverless function. To view a serverless function's logs in the CLI, run the `hs project logs` command. Learn more about using the [hs project logs command](/guides/crm/developer-projects/project-cli-commands#view-logs). There are two types of log messages produced: - Log messages that record the execution of a function, along with its status and timing. For example: `2021-04-28T19:19:21.666Z - SUCCESS - Execution Time: 279ms` - Log messages that are produced through console statements in the function code. For example, your serverless function JavaScript might include: ```js exports.main = async (context) => { console.log('Log some debug info'); console.error('An error occurred'); return { statusCode: 200, body: { message: 'Hello World' }, }; }; ``` A log output for the above code would then produce the following: `2021-04-28T19:15:13.200Z INFO Log some debug info` `2021-04-28T19:15:14.200Z ERROR An error occurred` ### Design manager serverless functions To help with troubleshooting your serverless functions, the [CLI](/guides/cms/tools/local-development-cli) has an [`hs logs` command](/guides/cms/tools/local-development-cli#logs) which gives you the ability to view your function’s logs. In addition to individual function invocation responses, time of execution, and execution time, any `console.log` statement will also appear in function logs. Do not `console.log` secrets like API keys. # Alias mapping @ Learn how to use alias mapping to update module fields without breaking existing content. $ guides & cms/content/fields --- # Alias mapping Alias mapping enables you to create field mappings in a module so that you can move, rename, or replace its fields without impacting pages that are using the module. For example, a module is being used on a live page. You want to move some fields into the [_Styles_ tab](/guides/cms/content/fields/overview#style-fields), such as color or font, but a content creator has already selected values for those fields in the editor. If you were to move those fields without setting up alias mapping, HubSpot would not be able to relocate those fields and they would revert to their default values, which would undo the styling on the live page. Instead, you can include an `aliases_mapping` property to map the field to another one in `fields.json`. Then, when a value has not been set for the original field, HubSpot will check if a value exists in the mapped field. If no value exists in the mapped field either, it will use the default value instead. This property can be used to map field values between different versions of a module only when the stored data type of the old field is the same as the new field's stored data type. For a visual walkthrough of this feature, check out the video below. To migrate existing fields to aliases: 1. Create new fields and map them to old fields using the `aliases_mapping` property in the `fields.json` file. 2. Remove the old field definition. 3. Update the `module.html` file to use the new fields definition. - You cannot map fields that are of a different data type to each other. For example, you can't map a background gradient field to an image field. The stored value has to be a valid value for the new field's type. - When creating a new field with an alias mapping to an old field, the default values and required properties of both fields should be the same. Below are examples of implementing this for both simple and complex changes: - [Simple implementation](#simple-implementation): mapping a color field to another new color field. - [Complex implementation](#complex-implementation): mapping a number field to a font field's `size` subfield to control font size. ## Simple implementation In simple situations, the field type of the old field and the field type of the new field should be the same. For example: - Old color field to new color field. - Old text field to new text field. - Old spacing field to new spacing field. Below is an example of using `aliases_mapping` when moving a color field from the module's _Content_ tab to the _Styles_ tab. Original module code: ```json [ { "label": "Button Color", "name": "old_button_color_field", "type": "color", "required": true, "default": { "color": "#FFFFFF", "opacity": 100 } } ] ``` Updated module code: ```json [ { "label": "Styles", "name": "styles", "type": "group", "tab": "STYLE", "children": [ { "label": "Button Color", "name": "new_button_color_field", "type": "color", "required": true, "aliases_mapping": { "property_aliases_paths": { "new_button_color_field": ["old_button_color_field"] } }, "default": { "color": "#FFFFFF", "opacity": 100 } } ] } ] ``` ## Complex implementation In more complex situations, you can also map fields to subfields or other module field types as long as the data type is the same, and the new field's subfield type matches. Subfields are the properties within the field's stored value object. For example: - Mapping a _Rich text_ field to a _Text_ field, as the values in both fields are stored as strings. - Consolidating typography fields, such as changing from a number field for font size, to use a font field (which has a font size sub field). You can add an alias for the `size` subfield to map it to the old number field by using dot notation. Below is an example of changing the font sizing option from a number field to a font field which has a font size sub field. Original module code: ```json [ { "name": "my_number_field", "label": "Number field", "required": false, "locked": false, "display": "text", "step": 1, "type": "number", "min": null, "max": null, "inline_help_text": "", "help_text": "", "default": null } ] ``` Updated module code: ```json [ { "name": "my_font_field", "label": "font_field", "required": false, "locked": false, "inline_help_text": "", "help_text": "", "load_external_fonts": true, "type": "font", "aliases_mapping": { "property_aliases_paths": { "my_font_field.size": ["my_number_field"] } }, "default": { "size": 12, "font": "Merriweather", "font_set": "GOOGLE", "size_unit": "px", "color": "#000", "styles": {} } } ] ``` # Module and Theme Field Best Practices @ 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. $ guides & cms/content/fields --- # Module and theme fields best practices 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](/guides/cms/content/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](/guides/cms/content/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. Therefor, 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](/reference/cms/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](/reference/cms/fields/module-theme-fields#font) and [color](/reference/cms/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](/reference/cms/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](/reference/cms/fields/module-theme-fields) - [Module and theme fields overview](/guides/cms/content/fields/overview) - [Modules overview](/guides/cms/content/modules/overview) - [Themes overview](/guides/cms/content/themes/overview) #### Tutorials - [Getting started with modules](/guides/cms/content/modules/quickstart) - [Getting started with themes](/guides/cms/content/themes/getting-started) # Brand settings inheritance @ Learn how to use brand settings, such as logos and brand colors, within a theme or across HubL/HTML and CSS files. $ guides & cms/content/fields --- # Brand settings inheritance With [brand settings](https://knowledge.hubspot.com/settings/customize-branding-for-your-hubspot-content#customize-your-company-logo-and-colors), users can set up the company's brand colors, logos, and favicons to be used across HubSpot content. This enables you to access those brand settings with tokens in a theme's `fields.json` file and within HTML/HubL and CSS files. You can also access brand colors within a module's `fields.json` file. After adding these tokens within a `fields.json` file, content creators can edit their values within the theme settings editor. When adding these tokens in a HTML, HubL, or CSS, the values will be hardcoded and cannot be modified in the page editor by content creators. Below, learn about the available brand setting variables along with examples of implementation. ## Brand settings variables The following is a list of options that can be accessed from the brand settings area within the value of the `property_value_paths` object or within HTML/HubL and CSS files. ### Colors Brand colors can be accessed both within a theme or module's [fields.json file](#fields-json-color), and within HTML/HubL and CSS files by using the following HubL tokens: - **Primary:** - `{{brand_settings.primaryColor}}` - `{{brand_settings.colors[0]}}` - **Secondary:** `{{brand_settings.secondaryColor}}` - **Accent colors:** - `{{brand_settings.accentColor1}}` - `{{brand_settings.accentColor2}}` - `{{brand_settings.accentColor3}}` - **Additional colors:** - `{{brand_settings.colors[1]}}` - `{{brand_settings.colors[2]}}` - `{{brand_settings.colors[3]}}` To access a color's hex code directly, include a `hex` filter in the token. For example: `{{brand_settings.primaryColor.hex}}` To include brand settings colors in a theme or module's `fields.json` file, use the following format: ```json //Example of using the primary color in within a theme's // field.json file { "name": "branding_color", "label": "branding_color", "type": "color", "default": { "color": "#26ff55", "opacity": 60 }, "inherited_value": { "property_value_paths": { "color": "brand_settings.primaryColor" } } } ``` There are times when accounts may not have any additional colors configured in its brand settings. If your code is referencing an inherited color that does not exist in brand settings, the following fallback logic is used: - `secondaryColor` falls back to the first additional color (`colors[1]`). - `accentColor1` falls back to the second additional color (`colors[2]`). - `accentColor2` falls back to the third additional color (`colors[3]`). - `accentColor3` falls back to the fourth additional color (`colors[4]`). - Additional colors (e.g., `colors[3]`) will fall back to the `default` value. If there is no default property color set, `primaryColor` will be used. ### Logos Brand logos can be accessed within a module's `fields.json` file, and within HTML/HubL and CSS files. You can use the following tokens to access the primary logo set within brand settings: - `{{brand_settings.primaryLogo}}` - `{{brand_settings.logos[0]}}` All additional logos can be accessed by using `{{brand_settings.logos[1-19]}}`. In addition, you can access the following logo attributes: - **Logo URL:** `{{brand_settings.primaryLogo.link}}` - **Logo alt text:** `{{brand_settings.primaryLogo.alt}}` - **Logo height:** `{{brand_settings.primaryLogo.height}}` - **Logo width:** `{{brand_settings.primaryLogo.width}}` - **Link to the logo's image:** `{{brand_settings.primaryLogo.src}}` ### Favicons Brand favicons can be accessed only within HTML/HubL and CSS files. You can use the following tokens to access the primary logo set within brand settings: - `{{brand_settings.primaryFavicon}}` - `{{brand_settings.favicons[0]}}` All additional favicons can be accessed by using `{{brand_settings.favicons[1-19]}}`. You can access the URL of the logo directly by including an `src` filter. For example: `{{brand_settings.primaryFavicon.src}}`. # Add custom fonts to a theme @ Learn how to include custom fonts in a theme that content creators can use in rich text fields. $ guides & cms/content/fields --- # Add custom fonts to a theme When developing a theme, you can add custom fonts to enable content creators to use them in font fields and rich text fields when building website content. After adding a custom font to the theme, HubSpot will [generate CSS](/guides/cms/content/fields/overview#generated-css) to render the font styling. At a high level, to add a custom font to a theme you'll need to add the following to your theme files: - A `fonts` folder that contain the custom font files and `font.json` config file. This folder and its contents can be named and structured in any way. - A `font.json` file within the `fonts` folder that defines the font and its variations for use in the theme. - In `theme.json`, a `custom_fonts` array containing a list of paths to the font folders within the theme. Below, learn more about adding fonts to your theme files. ## Font files In your theme folder, you'll first add a `fonts` folder to store the needed font files if you're hosting them locally. While there's no set way to structure the `fonts` folder and its contents, it's important to include several font formats to optimize browser compatibility and to provide content creators with various font weights. You can include font variations by adding individual files for each variation, or by using a [variable font](#variable-fonts) where a single file contains all variations. The method you choose will depend on the font files available to you, and the `font.json` file will need to be configured to match. ## Set up the font config (font.json) In the `fonts` folder, include a `font.json` file that defines the font and its variations for use in the theme. ```json { "name": "Noah", "default": "Regular", "fallback": "arial, sans-serif", "variants": [ { "name": "Regular", "files": [ { "file": "./TTF/Noah-Regular.ttf", "format": "truetype" }, { "file": "./WEB/Noah-Regular.woff", "format": "woff" }, { "file": "./WEB/Noah-Regular.woff2", "format": "woff2" } ], "styles": { "font-weight": 400 } }, { "name": "Bold", "files": [ { "file": "./TTF/Noah-Bold.ttf", "format": "truetype" }, { "file": "./WEB/Noah-Bold.woff", "format": "woff" }, { "file": "./WEB/Noah-Bold.woff2", "format": "woff2" } ], "styles": { "font-weight": "bold" } } ] } ``` | Parameter | Type | Description | | --- | --- | --- | | `name` | String | The name of the font. | | `default` | String | The name of the font variant to use as the default, as specified in the `variants` array. If no default is specified, HubSpot will automatically designate the first variant in the `variants` array as the default. | | `fallback` | String | Comma-separated fallback fonts to be appended to the end of the custom font CSS. For example, `"fallback:": 'Times New Roman', serif"` will result in: `font-family: 'Custom font', 'Times New Roman', serif`. | | `variants` | Array | A list of font variants, including the default font. For each variant, include the following fields:
        • `name`: the name of the variant.
        • `files`: an array of file paths and formats. Paths can be relative or absolute. You can reference font files stored locally or by external URL, but you cannot reference stylesheets containing `@font-face` declarations.
        • `styles`: an object that specifies which CSS styles should be applied to the variant. If not included, no additional CSS will be applied. This field overrides conflicting styles from the top-level `styles` object.
        | ### Variable fonts If your custom font is a variable font, where the main font and its variations are contained in one file, you'll define your font variants in the same way as a non-variable font where each variant is defined separately. Each variant will then reference the same file within the `file` property. For example, if you had a single variable font file (`myfont.ttf`), which contained the bold and italic versions, you would format your `font.json` file as follows: ```json { "name": "My font", "default": "Regular", "fallback": "arial, sans-serif", "variants": [ { "name": "Regular", "files": [ { "file": "./TTF/myfont.ttf", "format": "truetype-variations" } ], "styles": { "font-weight": 400 } }, { "name": "Bold", "files": [ { "file": "./TTF/myfont.ttf", "format": "truetype-variations" } ], "styles": { "font-weight": "bold" } }, { "name": "Italics", "files": [ { "file": "./TTF/myfont.ttf", "format": "truetype-variations" } ], "styles": { "font-style": "italic" } } ] } ``` To include a variant not contained in the variable font file, you can include a separate font file within the `files` array for that variant instead of `styles`. ```json { "name": "My font", "files": [ { "file": "./myfont.ttf", "format": "truetype-variations" } ], "variants": [ { "name": "Bold", "styles": { "font-weight": 700 } }, { "name": "Italic", "styles": { "font-style": "italic" } }, { "name": "My special variant", "files": [ { "file": "./myfontvariant.ttf", "format": "truetype" } ] } ] } ``` ## Add fonts to the theme config (theme.json) After adding your font files and setting up your `font.json` file, you'll add a `custom_fonts` array listing the paths to the `font` folder within the theme. ```json { "label" : "Atomic_Lite", … "custom_fonts": [ "./fonts/Noah"] } ``` ## Limits When using the rich text editor in the design manager, font options will not appear in the font selector dropdown menu. # oEmbed @ oEmbed is a standardized format for allowing content to be embedded on a website. $ guides & cms/content/fields --- # oEmbed ## What is oEmbed and how does it work? [oEmbed](https://oembed.com/) is a standardized format for allowing content to be embedded on a website. The end-user’s (consumer) browser sends a GET request to a provider's API endpoint with the following parameters. - URL (required) - maxWidth (optional) - maxHeight (optional) - format (optional) The provider then returns structured data in the specified format. This data is then used to display the embedded content on the end-users (consumers) page. For more information on oEmbed, please refer to the official documentation for the [oEmbed specification](https://oembed.com/). ## How does HubSpot make use of oEmbed? We offer two video embed modules (email and pages), one Embed field (Media URL) for custom modules, and one function (email only) that makes use of oEmbed for helping to implement embedded content on your website. Inside of our modules, you will see a "Media URL" field. This field is equal to the "URL" parameter as mentioned above in the oEmbed specification. Our modules do all the heavy lifting of requesting the information, known as the [oEmbed Response](#the-oembed-response-explained), from the URL you specify here and making it available on your site. When using the `oembed()` function inside of email, you simply provide a request string that includes the URL, Max Width, and Max Height. You can see [an example of this function on our developer documentation.](/reference/cms/hubl/functions#oembed) ### The oEmbed response explained An oEmbed Response is JSON data that is returned by a provider (such as YouTube, Flickr, or any [others that support oEmbed](https://oembed.com/#section7.1)) which contains various information that is related to the item being fetched. You can view the response parameters on the [oEmbed documentation under Section 2.3.4](https://oembed.com/#section2). Each response type (photo, video, link, and rich) has specific parameters that are associated with them. An example of an oEmbed Response from YouTube is below. If you look closely, you will see that the video type provides an HTML parameter that returns the code you would want to use when embedding the video player on your website. ```json // oEmbed Response from YouTube { "title": "HubSpot Themes Challenge Workshop - Oct 1, 2020", "author_name": "HubSpot Developers", "author_url": "https://www.youtube.com/channel/UCT4M2Uw9rBDpa3JcKaz5UMQ", "type": "video", "height": 113, "width": 200, "version": "1.0", "provider_name": "YouTube", "provider_url": "https://www.youtube.com/", "thumbnail_height": 360, "thumbnail_width": 480, "thumbnail_url": "https://i.ytimg.com/vi/Y4v6cKm2wqE/hqdefault.jpg", "html": "" } ``` ## Recommended usage of Embed Fields in your modules. ### Email only modules If your module is being used strictly for email and you’re looking to make use of the embed field, we recommend using the embed field in conjunction with the `oembed()` function. Below is an example of this in use: ```hubl {{ oembed({ url: module.embed_field.oembed_url}) }} ``` This function returns a dictionary containing the parameters of the oEmbed response for the specified url. Individual parameters can be accessed using dot notation. Below is an example of this using a YouTube video as a media url source. ```hubl // gets the html code for embedding {{ oembed({ url: module.embed_field.oembed_url}).html }} // gets the video title {{ oembed({ url: module.embed_field.oembed_url}).title }} // gets the thumbnail URL of the video {{ oembed({ url: module.embed_field.oembed_url}).thumbnailURL }} //gets the author name {{ oembed({ url: module.embed_field.oembed_url}).authorName }} ``` ### Page and/or Blog Modules If your module is being used inside of pages and/or blogs, we recommend making use of javascript to fetch your oEmbed response. This is the recommended approach as when we fetch results through our embed field naturally, we cache them on our side which could cause the oEmbed data to become stale over time. Below is an example of how you can fetch the results of the oEmbed response using javascript so your results are never stale (cached). Note: this all takes place in the HTML + HubL section of the custom module and is using the `{% require %}` tags in order to place the correct CSS and JavaScript in the proper areas based on the conditional at the start of the module code. ```hubl {% if module.embed_field.source_type == "oembed" %} {% require_css %} {% end_require_css %} {% require_js %} {% end_require_js %} {% else %} {% require_css %} {% end_require_css %} {% endif %} ``` ### Email and Page (or Blog) Modules: If your module is being used for email in conjunction with page and/or blog, you also have the ability to use the `oembed_response` object. Once the url is entered in the Media URL field and pulled this data is then cached on our end for use with an `oembed_response` object. Below is a sample of how you can view the `oembed_response` dict: ```hubl // outputs the oembed response {{ module.embed_field.oembed_response }} ``` You can use dot notation to then grab child elements from the dict. For example when using a YouTube video as a source you can do the following: ```hubl // gets the html code for embedding {{ module.embed_field.oembed_response.html }} //outputs the title {{ module.embed_field.oembed_response.title }} // outputs the authors name {{ module.embed_field.oembed_response.author_name }} //outputs the thumbnail url (for use in the tag) {{ module.embed_field.oembed_response.thumbnail_url }} ``` In order to update the data in the cached `oembed_response`, you would have to modify your media URL (remove a letter and re-add) and re-apply the changes to your module. ## Troubleshooting your Video Embed Module and Embed Field. When adding "Media URL" links into your video embed module or embed field, you may run into an error that states "You can’t embed this media URL for sharing.” When you receive this error, there are a few things you will want to check with the provider. - **Does the Asset provider have oEmbed functionality in their platform?** The oEmbed official documentation contains [a list of providers that support this specification](https://oembed.com/#section7.1). You may also want to reach out to your provider directly to see if they support this as well. - **Is the URL "Private"?** Many platforms allow users to hide their content from being viewed publicly. For example, if you have an Instagram account that is private, you may not be able to embed content from your posts if it is not available publicly (without requiring the user to sign in to the platform or be a follower/friend). ## HTTP Status Codes for oEmbed The following are status codes that are defined as per the oEmbed Specification: - 404 Not Found: There is no response found for the requested URL. - 501 Not Implemented: The requested response format cannot be returned. - 401 Unauthorized: The requested URL contains a private (non-public) resource. HubSpot does not have direct control over how third-party providers implement their status codes or how they align to the oEmbed Specification. Because of this, we return either a 404 where applicable or a 400 status code and similar messaging to what you see above. The messaging is contained inside of the response object in the `message` property. # Module and Theme Fields Overview @ HubSpot modules and themes support customization by content creators through fields. Developers create and organize fields through a fields.json file. $ guides & cms/content/fields --- # Module and theme fields overview Within [modules](/guides/cms/content/modules/overview) and [themes](/guides/cms/content/themes/overview), fields are used to enable content creators to control module and theme styling and functionality on your website. When developing a module or a theme, you'll include fields in a `fields.json` file, which will then translate to the theme and content editors. Below, learn more about how to create and manage options for module and theme fields. To learn more about specific field types, check out the [module and field types reference guide](/reference/cms/fields/module-theme-fields). ## Creating and managing fields You can add fields to a [module's](/guides/cms/content/modules/overview) `fields.json` file locally through the [HubSpot CLI](/guides/cms/tools/local-development-cli) and in the in-app module editor. To add fields to a [theme](/guides/cms/content/themes/overview), you must update the theme's `fields.json` file locally using the CLI. ### HubSpot CLI When [building locally](/guides/cms/setup/getting-started-with-local-development), module and theme fields can be edited through a `fields.json` file inside of the module or theme's folder. For modules, this file will automatically be created when using the [`hs create module`](/guides/cms/tools/local-development-cli) command. All of the field options available in the module editor are available as properties you can add or edit in the `fields.json` file. This includes repeater fields, groups, and conditions. One of the benefits of editing locally is that it makes it easier to [include your modules in version control systems like git](/guides/cms/setup/github-integration). ### Module editor The [design manager](/guides/cms/tools/design-manager) has a built-in module editor UI that enables you to [create](https://knowledge.hubspot.com/design-manager/create-and-edit-modules), group, and [edit](https://knowledge.hubspot.com/design-manager/create-and-edit-modules) module fields. The module editor contains a module preview which enables you to see what the module looks like on its own, as well as test your fields. Since modules do not live in a vacuum you should always test them on a template you plan to use, to see what template level styles may affect it. Be aware if a module is contained in a locked folder it cannot be edited this way. If you are working mostly locally but want to use the module editor to configure fields, make sure to [fetch](/guides/cms/tools/local-development-cli#fetch) your changes. This is especially important for those using version control systems like git. ## Side by side fields By default, module fields in content editors stack vertically. However, you can place module fields side by side by adding a `display_width` property to fields in the `fields.json` file with a value of `half_width`. A single field with a `display_width` of `half_width` will appear as half-width in the content editor. When the field above or below that field in the `fields.json` file is set to `half_width`, they'll be placed side by side. ```json // Example module fields.json file [ { "name": "number_field", "label": "Number", "required": false, "locked": false, "display": "slider", "min": 1, "max": 10, "step": 1, "type": "number", "prefix": "", "suffix": "", "default": null, "display_width": "half_width" }, { "label": "Description", "name": "description", "type": "text", "required": true, "default": "Add a description", "display_width": "half_width" } ] ``` ## Field groups When fields are related to each other, it often makes sense for them to be grouped together visually. You can do so by creating field groups, which are supported in both modules and themes. To create a field group locally, in `fields.json` create an object with the `type` of `"group"`. Then, include a `children` array to contain the fields you want to group together. ```json [ { "id": "9c00985f-01db-ac5d-73f5-80ed6c0c7863", "name": "my_text", "display_width": null, "label": "Text", "required": true, "locked": false, "validation_regex": "", "allow_new_line": false, "show_emoji_picker": false, "type": "text", "default": "Add text here" }, { "type": "group", "name": "recipe_summary_group", "label": "Recipe summary", "expanded": true, "inline_help_text": "Summarize the recipe (title and description)", "children": [ { "type": "text", "label": "Recipe title", "name": "recipe_title", "placeholder": "Burrata salad" }, { "type": "text", "label": "Recipe description", "name": "recipe_description", "placeholder": "fig mostarda, hazelnuts, arugula, vincotto, prosciutto, toasted sourdough" } ] } ] ``` You can create further field groupings inside a group by adding another `"group"` type object within the first `children` parameter. Then, build the field group in the same way as above, using `children` to contain the fields. You can nest field groups up to a depth of three. ```json [ { "type": "group", "name": "recipe_summary_group", "label": "Recipe summary", "inline_help_text": "Summarize the recipe (title and description)", "display": "inline", "children": [ { "type": "text", "label": "Recipe title", "name": "recipe_title", "placeholder": "Burrata salad" }, { "type": "text", "label": "Recipe description", "name": "recipe_description", "placeholder": "fig mostarda, hazelnuts, arugula, vincotto, prosciutto, toasted sourdough" }, { "type": "group", "name": "secondary_group", "label": "Secondary group", "expanded": false, "children": [ { "name": "bg_color", "label": "Background color", "sortable": false, "required": false, "locked": false, "type": "color", "default": { "color": "#ff0000", "opacity": 100 } } ] } ] } ] ``` #### Field group display options You can customize the following field group display behavior: - **Expansion:** by default, field groups will display as collapsed in the editor. Groups that contain nested groups will display as drilldown buttons that open the group in its own view with the innermost group displaying dividers. - **Display type:** by default, groups that don't contain nested groups will display as collapsible sections with visual dividers around its children. Groups that do contain nested groups will display as drilldown buttons that open the group in its own view with the innermost group displaying with dividers. - **Group icon:** if desired, you can include a Font Awesome icon that displays to the left of the label. ```json [ { "type": "group", "name": "recipe_summary_group", "label": "Recipe summary", "display": "drilldown", "inline_help_text": "Summarize the recipe (title and description)", "icon": { "name": "star", "set": "fontawesome-5.14.0", "type": "SOLID" }, "children": [ { "type": "text", "label": "Recipe title", "name": "recipe_title", "placeholder": "Burrata salad" }, { "type": "text", "label": "Recipe description", "name": "recipe_description", "placeholder": "fig mostarda, hazelnuts, arugula, vincotto, prosciutto, toasted sourdough" } ] } ] ``` | Parameter | Type | Description | | --- | --- | --- | | `display` | String | The display style of the field group. Can be one of the following:
        • `drilldown`: displays the collapsed group with a button to open the group's children in its own panel. This is the default display for groups that contain nested groups.
        • `accordion`: displays the collapsed group with a button to expand the group's children below as an expandable section. This is the default display for groups without nested groups.
        • `inline`: displays the group and its children inline with no option to collapse the group.
        | | `icon` | Object | Adds an icon to the left of the label. Contains the following parameters:
        • `name`: the Font Awesome icon identifier.
        • `set`: the Font Awesome [icon set](/reference/cms/fields/module-theme-fields#icon).
        • `type`: icon style (e.g., `SOLID`, `REGULAR`).
        | | `expanded` | Boolean | Whether the field group is expanded by default. | #### Outputting field values within field groups Field groups create dicts that contain the field values you want to output. If you nest field groups the nested field group is a dict inside of the outside field group dict. To access that data you will traverse the tree from either the root theme or module variable depending on your context. ```html ``` ```css /* Printing a Font field's color value, when the font field is within a field group. `typography` is the field group, `h1_font` is the field */ h1{ color: {{ theme.typography.h1_font.color }}; } ``` #### Featured items in field groups For situations where a field group is repeated, you can specify one or more of those occurrences as featured, enabling you to style the item separately to make it stand out. For example, this can be particularly useful for a product page where you might have a featured product that you want to highlight. You can specify a maximum number of featured items per field group. In the editor, content creators can then mark items as featured as needed. To enable featured items in a field group, include the `group_occurrence_meta` property in the field group configuration. This property stores the following properties: - `featured_enabled`: set to `true` to enable featured items. - `featured_limit`: the maximum number of featured items to allow. The field group must also include the `occurrence` property. ```json // Field group example { "label": "Card", "name": "card", "type": "group", "occurrence": { "default": 2, "min": 1, "sorting_label_field": "card.title" }, "group_occurrence_meta": { "featured_enabled": true, "featured_limit": 3 }, "children": [ { "label": "Image", "name": "image", "type": "image", "responsive": true, "resizable": true, "show_loading": true, "default": { "loading": "lazy" } }, { "label": "Content", "name": "text", "type": "richtext", "enabled_features": [ "advanced_emphasis", "alignment", "block", "emoji", "font_family", "font_size", "lists", "standard_emphasis" ], "default": "

        This is a title

        Contextual advertising can be profitable. It can either pay for your hosting and maintenance costs for you website or it can pay for a lot more.

        " } ], "default": [ { "image": { "loading": "lazy" }, "text": "

        This is a title

        Contextual advertising can be profitable. It can either pay for your hosting and maintenance costs for you website or it can pay for a lot more.

        " }, { "image": { "loading": "lazy" }, "text": "

        This is a title

        Contextual advertising can be profitable. It can either pay for your hosting and maintenance costs for you website or it can pay for a lot more.

        " } ] } ``` To check whether an item in a repeated group is featured, you can query the `hs_meta` property. The code below uses a [for loop](/reference/cms/hubl/loops) to check for field group items that are set to featured, then displays the title of each as an `h3` header. `{{ repeated_group_item.hs_meta.occurrence.featured }}` ```hubl ``` ## Style fields Style fields are a special field group type in a module or theme's `fields.json` file that give content creators control over a module or theme's styling in the page and theme editor. Below, learn how to add style fields to a module or theme. Learn about [best practices for using and organizing style fields](/guides/cms/content/fields/best-practices). ### Module style fields Style fields added to a module will appear on the _Styles_ tab of the page editor when editing the module: When adding style fields to a module's `fields.json` file, you add them within one styles group. That group, however, can contain multiple groups within it, as shown below: ```json // Module style fields [ { "type": "group", "name": "styles", "tab": "STYLE", "children": [ { "name": "img_spacing", "label": "Spacing around image", "required": false, "type": "spacing", "default": { "padding": { "top": { "value": 10, "units": "px" }, "bottom": { "value": 10, "units": "px" }, "left": { "value": 10, "units": "px" }, "right": { "value": 10, "units": "px" } }, "margin": { "top": { "value": 10, "units": "px" }, "bottom": { "value": 10, "units": "px" } } } } ] } ] ``` The following fields can be used as style fields in modules. Learn about each of the field types in the [module and field types guide](/reference/cms/fields/module-theme-fields). - [Alignment](/reference/cms/fields/module-theme-fields#alignment) - [Gradient](/reference/cms/fields/module-theme-fields#gradient) - [Spacing](/reference/cms/fields/module-theme-fields#spacing) - [Background Image](/reference/cms/fields/module-theme-fields#background-image) - [Border](/reference/cms/fields/module-theme-fields#border) - [Boolean](/reference/cms/fields/module-theme-fields#boolean) - [Choice](/reference/cms/fields/module-theme-fields#choice) - [Number](/reference/cms/fields/module-theme-fields#number) - [Color](/reference/cms/fields/module-theme-fields#color) - [Icon](/reference/cms/fields/module-theme-fields#icon) - [Image](/reference/cms/fields/module-theme-fields#image) - [Font](/reference/cms/fields/module-theme-fields#font) - [Text Alignment](/reference/cms/fields/module-theme-fields#text-alignment) Learn more about [module and theme field types](/reference/cms/fields/module-theme-fields). View the [CMS boilerplate](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/modules/card.module/fields.json) for an example of a style fields within a module's `fields.json` file. ### Theme style fields Style fields added to a theme will appear in the left sidebar of the theme editor: All style fields within a theme's `fields.json` file will be added to the left sidebar of the theme editor, as opposed to needing to put them under a styles group, as shown below: ```json // Theme style fields [ { "label": "Global colors", "name": "global_colors", "type": "group", "children": [ { "label": "Primary", "name": "primary", "type": "color", "visibility": { "hidden_subfields": { "opacity": true } }, "default": { "color": "#494A52" } }, { "label": "Secondary", "name": "secondary", "type": "color", "visibility": { "hidden_subfields": { "opacity": true } }, "default": { "color": "#F8FAFC" } } ] }, { "label": "Global fonts", "name": "global_fonts", "type": "group", "children": [ { "label": "Primary", "name": "primary", "type": "font", "visibility": { "hidden_subfields": { "size": true, "styles": true } }, "inherited_value": { "property_value_paths": { "color": "theme.global_colors.primary.color" } }, "default": { "fallback": "sans-serif", "font": "Lato", "font_set": "GOOGLE" } }, { "label": "Secondary", "name": "secondary", "type": "font", "visibility": { "hidden_subfields": { "size": true, "styles": true } }, "inherited_value": { "property_value_paths": { "color": "theme.global_colors.primary.color" } }, "default": { "fallback": "serif", "font": "Merriweather", "font_set": "GOOGLE" } } ] }, { "name": "branding_color", "label": "branding_color", "type": "color", "default": { "color": "#3b7bc0", "opacity": 60 }, "inherited_value": { "property_value_paths": { "color": "brand_settings.primaryColor" } } }, { "name": "secondary_branding_color", "label": "Secondary Branding color", "type": "color", "default": { "color": "#ff6b6b", "opacity": 60 }, "inherited_value": { "property_value_paths": { "color": "brand_settings.colors[2]" } } } ] } ] ``` The following fields can be used as style fields in themes. Learn about each of the field types in the [module and field types guide](/reference/cms/fields/module-theme-fields). - [Boolean](/reference/cms/fields/module-theme-fields#boolean) - [Border](/reference/cms/fields/module-theme-fields#border) - [Choice](/reference/cms/fields/module-theme-fields#choice) - [Color](/reference/cms/fields/module-theme-fields#color) - [Font](/reference/cms/fields/module-theme-fields#font) - [Image](/reference/cms/fields/module-theme-fields#image) - [Number](/reference/cms/fields/module-theme-fields#number) - [Spacing](/reference/cms/fields/module-theme-fields#spacing) Learn more about [module and theme field types](/reference/cms/fields/module-theme-fields). View the [CMS boilerplate](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/fields.json) for an example of a style fields within a theme's `fields.json` file. If you're a marketplace provider, you should not replace existing content fields with style fields in existing modules. Changing the hierarchy of fields in a `fields.json` file can result in existing module instances losing their data. Instead, you should add new style fields, or create a new listing that has the fields appropriately grouped. This will prevent your updates from being breaking changes for customers relying on your themes. To advocate for migration paths for old modules, [check out the HubSpot Ideas forum](https://community.hubspot.com/t5/HubSpot-Ideas/Create-a-path-to-make-it-possible-to-migrate-existing-fields-to/idi-p/450311#M85938). ### Generated CSS Some style fields provide a way to output css directly based on the field's value. This is especially helpful with fields that can control more complicated styling like gradients. The following style fields have a generated `.css` property: - [Background Image](/reference/cms/fields/module-theme-fields#background-image) - [Border](/reference/cms/fields/module-theme-fields#border) - [Color](/reference/cms/fields/module-theme-fields#color) - [Font](/reference/cms/fields/module-theme-fields#font) - [Gradient](/reference/cms/fields/module-theme-fields#gradient) - [Spacing](/reference/cms/fields/module-theme-fields#spacing) - [Text alignment](/reference/cms/fields/module-theme-fields#text-alignment) ```hubl {% require_css %} {% end_require_css %} ``` ## Repeaters When creating modules that format information, often there are types of information that repeat. A recipe module for example, might have a field for "Ingredient". Well, most recipes have more than 1 ingredient. You could give them a rich text field, but then you lose your ability to force consistent styling and add functionality around each ingredient. That's where repeaters come in, HubSpot has two forms of repeaters: Repeating fields, and Repeating groups. ### Repeating fields This makes it so the content creator can add as many ingredients as they wish. From the developer perspective, you get an array that you can loop through to print out that list of ingredients, applying the formatting and functionality you want. Repeating fields are best used for very simple situations. Often times [repeating groups](#repeating-groups) make more sense. It's not currently possible to set the default order of repeating fields. #### Repeating fields in fields.json ```json // Repeating field example { "name": "ingredient", "label": "Ingredient", "required": false, "locked": false, "occurrence": { "min": 1, "max": null, "sorting_label_field": null, "default": 1 }, "allow_new_line": false, "show_emoji_picker": true, "type": "text", "default": ["1 cup water"] } ``` #### Loop through items in module HTML+HubL ```html
          {% for item in module.ingredient %}
        • {{ item }}
        • {% endfor %}
        ``` ### Repeating groups The quantity of an ingredient would be critical to the shopping list. While someone could provide that in the text field, the module would then need to parse the text field and hope we are successfully separating the quantity from the ingredient. This is where repeating groups come in handy. The output of these fields is an object that can be looped through. #### Repeating groups in fields.json ```json // Repeating field group example { "id": "ingredients", "name": "ingredients", "label": "Ingredients", "required": false, "locked": false, "occurrence": { "min": 1, "max": null, "sorting_label_field": "ingredients.ingredient", "default": null }, "children": [ { "id": "ingredients.ingredient", "name": "ingredient", "label": "Ingredient", "required": false, "locked": false, "validation_regex": "", "allow_new_line": false, "show_emoji_picker": false, "type": "text", "default": "Water" }, { "id": "ingredients.quantity", "name": "quantity", "label": "Quantity", "required": false, "locked": false, "display": "text", "min": 0, "step": 1, "type": "number", "default": 1 }, { "id": "ingredients.measurement", "name": "measurement", "label": "Measurement", "help_text": "Unit of measurement (cups, tbsp, etc.)", "required": false, "locked": false, "allow_new_line": false, "show_emoji_picker": false, "type": "text", "default": "cups" } ], "type": "group", "default": [ { "ingredient": "Water", "quantity": 1, "measurement": "cups" } ] } ``` #### Looping through repeating fields in modules ```html

        Ingredients

          {% for ingredient in module.ingredients %}
        • {{ ingredient.quantity }} {{ ingredient.measurement }} {{ ingredient.ingredient }}
        • {% endfor %}
        ``` ### Repeater options ```json "occurrence" : { "min" : 1, "max" : 4, "sorting_label_field" : "ingredients.ingredient", } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `max` | Integer | Maximum number of occurrences of this group. Prevents the content creator from adding more than this number of items in the UI. | `null` | | `min` | Integer | Minimum number of occurrences of this field group. Prevents users from having less than this number of items in the UI. | `null` | | `sorting_label_field` | String | This is the field id, of the field to pull text from to show in the UI on the draggable cards. The default for this is the first field in the group. | | ## Inherited fields The `inherited_value` property can be configured to make a field inherit its default value from other fields. To set a field's entire default value from another field's value, set the `default_value_path` to the field name path of the target field. When `default_value_path` is set, it'll ignore any `default` set on the field. To access values from other fields, the paths must include `module.` at the beginning, similar to when accessing the value in the module's HubL code. ```json // Inherited fields { "name": "body_font", "type": "font", "default": { "font": "Helvetica", "color": "#C27BA0" } }, { "name": "h1_font", "type": "font", "default": {}, "inherited_value": { "default_value_path": "module.body_font", "property_value_paths" : { "font": "module.body_font.font", "font_set": "module.body_font.font_set" } } } ``` Because font family is determined by a combination of `font` and `font_set`, you must include both for font field inheritance. Learn more about the [font field](/reference/cms/fields/module-theme-fields#font). For complex fields (fields whose values are objects), users can have more granularity over which properties get inherited through `property_value_path`. Any paths referred in `inherited_value` can also include keys from a field's value for complex fields. For example, color fields have object values that contain the color itself as well as opacity. So to get a color's actual color value without the opacity, the path would end in `.color`. For example, a font field can inherit just its color from a separate color field: ```json // Inherited fields with objects { "name": "secondary_color", "type": "color", "default": { "color": "#C27BA0", "opacity": 100 } }, { "name": "h1_font", "type": "font", "default": { "font": "Helvetica", "size": 12, "size_unit": "px" }, "inherited_value": { "property_value_paths": { "color": "module.secondary_color.color" } } } ``` You can also combine the effects of `default_value_path` and `property_value_paths` to inherit a default value from one field while inheriting a specific property value from a different field: ```json // combining default_value_path and property_value_paths { "name": "body_font", "type": "font", "default": { "font": "Helvetica", "color": "#000000" } }, { "name": "secondary_color", "type": "color", "default": { "color": "#C27BA0", "opacity": 100 } }, { "name": "h1_font", "type": "font", "default": {}, "inherited_value": { "default_value_path": "module.body_font", "property_value_paths": { "color": "module.secondary_color.color" } } } ``` If a field inherits from another field but then gets directly overridden at the page level or in theme settings, its connection to the controlling field gets severed. Any other fields attached via `default_value_path` or `property_value_paths` will no longer affect the value of the field. ## Field visibility When defining custom module and theme fields, you can configure when a field appears by adding the `visibility` object to the field in the `fields.json` file. For example, you can set a form module to display a rich text area when the thank you message is selected, but a page selector when a redirect is selected. You can set visibility based on the value of a `controlling_field_path`, or based on a specific property within that field using the `property` parameter. You can also apply visibility to an individual field, or to a group of fields to control visibility for all elements in the group. ```json "visibility" : { "controlling_field_path" : "field_name", "controlling_value_regex" : "regular_expression_in_controlling_field", "property": "src", "operator" : "EQUAL" } ``` | Parameter | Type | Description | | --- | --- | --- | | `controlling_field_path` | String | The doth path of the field that controls the display condition.
        • If the field is not nested inside a field group, use the field's name (i.e. `field_name`).
        • For fields nested in groups, the path should match its grouping structure, separated by a period. For example:
          • `field_group_name.field_name`
          • `parent_group.child_group.field_name`
        | | `controlling_value_regex` | String | The regular expression in the controlling field that needs to be present for the field to display. The regex must match the entire string (not a subset) and is run case-sensitively. | | `operator` | String | The operator that defines how the `controlling_value_regex` value needs to be met. Operators can be one of:
        • `NOT_EQUAL`
        • `EQUAL`
        • `EMPTY`
        • `NOT_EMPTY`
        • `MATCHES_REGEX`
        | | `property` | String | Sets visibility based on a specific property of the target field. For example, you can enable visibility when an image field's `src` property is equal to a specific value. By default, if no value is provided for this field, visibility is based on the stringified value of `controlling_value_regex`. | You can also include an `occurrence_options` object inside the `visibility` object to target a repeated field's value count. This object should include the `count` to compare with and an `operator` definition. For example, to show a text field only when another repeated field has at least two items, you could define `visibility` as follows: ```json [ { "type": "group", "name": "ingredients", "label": "Recipe ingredients", "display": "drilldown", "children": [ { "name": "ingredient", "label": "Ingredient", "occurrence": { "min": 1, "max": null, "default": 1 }, "type": "text", "default": ["1 cup water"] }, { "type": "text", "label": "Conditional field", "name": "conditional_field", "visibility": { "controlling_field_path": "ingredients.ingredient", "occurrence_options": { "count": 2, "operator": "GREATER_THAN_OR_EQUAL" } } } ] } ] ``` You can use any of the following `operater` values: - `"NOT_EQUAL"` - `"EQUAL"` - `"EMPTY"` - `"NOT_EMPTY"` - `"GREATER_THAN"` - `"GREATER_THAN_OR_EQUAL"` - `"LESS_THAN"` - `"LESS_THAN_OR_EQUAL"` ### Advanced visibility The `visibility` attribute can support only one criteria at a time. To include multiple criteria with multiple operators, as well as order of operations, you can use `advanced_visibility`. ```json "visibility_rules" : "ADVANCED", "advanced_visibility" : { "boolean_operator" : "AND", "criteria" : [{ "controlling_field_path" : "field_name", "controlling_value_regex" : "regular_expression_in_controlling_field", "operator" : "MATCHES_REGEX" }, { "controlling_field_path" : "field_name", "controlling_value_regex" : "regular_expression_in_controlling_field", "operator" : "EQUAL" }] } ``` | Parameter | Type | Description | | --- | --- | --- | | `visibility_rules` | String | By default, this value is set to `SIMPLE`. To use `advanced_visibility`, set to `ADVANCED`. | | `boolean_operator` | String | The boolean operator for the conditional criteria. Can be `AND` or `OR`. | | `criteria` | Array | An array of visibility objects that defines the conditional criteria that needs to be met for the field to display. | | `controlling_field_path` | String | The dot path of the field that controls the display condition.
        • If the field is not nested inside a field group, use the field's name (i.e. `field_name`).
        • For fields nested in groups, the path should match its grouping structure, separated by a period. For example:
          • `field_group_name.field_name`
          • `parent_group.child_group.field_name`
        | | `controlling_value_regex` | String | The value in the controlling field that needs to be met to display the field. When using the `MATCHES_REGEX` operator, the regex must match the entire string (not a subset) and is run case-sensitively.A field with a `controlling_field_path` but no `controlling_value_regex` is visible if the controlling field has any non-null, non-blank value. | | `operator` | String | The operator that defines how the `controlling_value_regex` value needs to be met. Operators can be one of:
        • `NOT_EQUAL`
        • `EQUAL`
        • `EMPTY`
        • `NOT_EMPTY`
        • `MATCHES_REGEX`
        Regex syntax is required when using `MATCHES_REGEX`. | As an example, below is the first portion of code from the [default payments module](/reference/cms/modules/default-modules). To review the full code, you can clone the module in HubSpot, then download into your local environment to view the module's `fields.json` file. ```json [ { "id": "payment", "name": "payment", "display_width": null, "label": "Payment", "required": true, "locked": false, "type": "payment", "default": { "id": null, "properties": {} } }, { "id": "checkout_location", "name": "checkout_location", "display_width": null, "label": "Checkout behavior", "required": false, "locked": false, "visibility": { "controlling_field_path": "payment", "controlling_value_regex": "id\":\\d+", "operator": "MATCHES_REGEX" }, "display": "radio", "choices": [ ["new_tab", "Open in a new tab"], ["overlay", "Sliding overlay"] ], "type": "choice", "default": "new_tab" }, { "id": "button_text", "name": "button_text", "display_width": null, "label": "Button text", "required": true, "locked": false, "validation_regex": "", "visibility": { "controlling_field_path": "payment", "controlling_value_regex": "id\":\\d+", "operator": "MATCHES_REGEX" }, "allow_new_line": false, "show_emoji_picker": false, "type": "text", "default": "Checkout" }, { "id": "icon", "name": "icon", "display_width": null, "label": "Icon", "required": false, "locked": false, "visibility_rules": "ADVANCED", "advanced_visibility": { "boolean_operator": "AND", "criteria": [ { "controlling_field_path": "payment", "controlling_value_regex": "id\":\\d+", "operator": "MATCHES_REGEX" }, { "controlling_field_path": "add_icon", "controlling_value_regex": "true", "operator": "EQUAL" } ], "children": [] }, "children": [ { "id": "icon.icon", "name": "icon", "display_width": null, "label": "Icon", "required": true, "locked": false, "icon_set": "fontawesome-5.0.10", "type": "icon", "default": { "name": "hubspot", "type": "SOLID", "unicode": "f3b2" } }, { "id": "icon.position", "name": "position", "display_width": null, "label": "Position", "required": true, "locked": false, "display": "select", "choices": [ ["left", "Left"], ["right", "Right"] ], "type": "choice", "default": "left" } ], "tab": "CONTENT", "expanded": false, "type": "group" } // rest of fields.json code ] ``` The above code results in the following behavior: - The first field (`payment`) is a required field (dropdown menu) that lets the content creator select a specific payment link. In HubSpot, a content creator will see the following when first adding the module to the page: - Once a payment link is selected, the three fields that follow (`checkout_location`, `button_text`, and `icon`) will appear. This is because the fields have a `visibility` attribute which is controlled by the `payment` field and requires an ID value in the payment field's `id` parameter. The `icon` field itself uses `advanced_visibility` to appear only when there's a payment link present in the `payment` field AND when the `add_icon` checkbox is selected. In addition to setting visibility within `fields.json`, You can also set visibility in the design manager by editing a field's _Display conditions_ options. After setting visibility in the design manager, you can [fetch](/guides/cms/tools/local-development-cli#fetch) the module [using the CLI](/guides/cms/setup/getting-started-with-local-development) to view the `visibility` attribute in the module's `fields.json` file. ## Conditional field disabling You can add conditions to a field to prevent editing when the specified conditions are met. You can also set a message to display above the field when disabled to provide context in the content editor. The conditions and message are set in the field's `disabled_controls` object. The conditions for making a field editable are set within the `rules` object, which follows the same format as [advanced_visibility](#field-visibility). The code below shows both a simple and advanced implementation of `rules` criteria: - The `simple_page` field includes logic to disable the field if the `text_field` is set to `testing`. - The `fancy_page` field includes logic to disable the field if either `text_field` or `text_field_2` is set to any value not equal to `testing` and `testing2` respectively. ```json // example fields.json [ { "type": "text", "name": "text_field", "label": "Text field" }, { "type": "text", "name": "text_field_2", "label": "Text field 2" }, { "type": "page", "label": "Simple Page", "name": "simple_page", "disabled_controls": { "message": "This field is disabled", "rules": { "criteria": [ { "controlling_field_path": "text_field", "operator": "EQUAL", "controlling_value_regex": "testing" } ] } } }, { "type": "page", "label": "Fancy Page", "name": "fancy_page", "disabled_controls": { "message": "This field is disabled", "rules": { "boolean_operator": "OR", "criteria": [ { "controlling_field_path": "text_field", "operator": "NOT_EQUAL", "controlling_value_regex": "testing" }, { "controlling_field_path": "text_field_2", "operator": "NOT_EQUAL", "controlling_value_regex": "testing2" } ] } } } ] ``` | Parameter | Type | Description | | --- | --- | --- | | `message` | String | The message to display in the content editor when the field is disabled. | | `rules` | Object | The conditions for enabling the field for editing. | | `criteria` | Array | An array of condition objects that defines the criteria that needs to be met for the field to display. This array can contain multiple condition objects separated by `AND` or`OR` logic through the `boolean_operator` parameter. | | `boolean_operator` | String | The boolean operator for the conditional criteria. Can be `AND` or `OR`. When not specified, defaults to `AND`. | | `controlling_field_path` | String | The dot path of the field that controls the display condition.
        • If the field is not nested inside a field group, use the field's name (i.e. `field_name`).
        • For fields nested in groups, the path should match its grouping structure, separated by a period. For example:
          • `field_group_name.field_name`
          • `parent_group.child_group.field_name`
        | | `controlling_value_regex` | String | The value in the controlling field that needs to be met to display the field. When using the `MATCHES_REGEX` operator, the regex must match the entire string (not a subset) and is run case-sensitively.A field with a `controlling_field_path` but no `controlling_value_regex` is visible if the controlling field has any non-null, non-blank value. | | `operator` | String | The operator that defines how the `controlling_value_regex` value needs to be met. Operators can be one of:
        • `NOT_EQUAL`
        • `EQUAL`
        • `EMPTY`
        • `NOT_EMPTY`
        • `MATCHES_REGEX`
        Regex syntax is required when using `MATCHES_REGEX`. | ## Theme editor field highlighting When in the theme editor, preview highlighting can help content creators understand which fields are controlling which page elements. Preview highlighting works by mapping the theme fields to the CSS selectors that they affect, adding a box around those elements when hovering over the field in the theme editor. To configure preview highlighting for theme fields, you'll include an `editor-preview.json` file in the root directory of the theme to map theme fields to a list of CSS selectors. In the file, you'll include an array for each style field you want to highlight containing the relevant CSS selectors, using the following format: ```json // editor-preview.json { "selectors": { "theme.settings.path.1": [ ], "theme.settings.path.2": [ ], } } ``` For example, the code below will highlight which page elements are controlled by the primary font field. You can view the full example in the `editor-preview.json` file of the default Growth theme. ```json // editor-preview.json { "selectors": { "fonts.primary": [ "button, .button, .hs-button", "form input[type='submit'], form .hs-button", ".error-page:before", "p", "blockquote > footer", "form td.is-today .pika-button", "form .is-selected .pika-button", "th, td", ".blog-listing__post-tag", ".blog-listing__post-author-name, .blog-post__author-name", ".pagination__link-icon svg", ".tabs__tab", "a", ".button.button--simple", ".pagination__link .pagination__link-icon svg", ".card--dark", ".card--light", ".card--light summary, .card--light p, .card--light h1, .card--light h2, .card--light h3, .card--light h4, .card--light h5, .card--light h6, .card--light a:not(.button), .card--light span, .card--light div, .card--light li, .card--light blockquote", ".card--light .accordion__summary:before", "tfoot th, tfoot td", ".header__language-switcher-current-label > span", ".header__language-switcher-child-toggle svg", ".header__language-switcher .lang_list_class a:not(.button)", ".header__menu-link", ".header__menu-item--depth-1 > .header__menu-link:not(.button)", ".header__menu-item--depth-1 .header__menu-child-toggle svg", ".header__menu-toggle svg", ".header__language-switcher .header__language-switcher-current-label > span", ".header p, .header h1, .header h2, .header h3, .header h4, .header h5, .header h6, .header a:not(.button), .header span, .header li, .header blockquote, .header .tabs__tab, .header .tabs__tab, .header .tabs__tab, .header .tabs__tab", ".footer .hs-menu-wrapper a", ".footer h1, .footer h2, .footer h3, .footer h4, .footer h5, .footer h6, .footer p, .footer a:not(.button), .footer span, .footer div, .footer li, .footer blockquote, .footer .tabs__tab, .footer .tabs__tab, .footer .tabs__tab, .footer .tabs__tab", ".footer hr", "form label", "#email-prefs-form, #email-prefs-form h1, #email-prefs-form h2", "form legend", "form input[type='text'], form input[type='email'], form input[type='password'], form input[type='tel'], form input[type='number'], form input[type='search'], form select, form textarea", ".backup-unsubscribe input[type='email']", "form .legal-consent-container, form .legal-consent-container .hs-richtext, form .legal-consent-container .hs-richtext p", "form .hs-richtext, form .hs-richtext *, form .hs-richtext p, form .hs-richtext h1, form .hs-richtext h2, form .hs-richtext h3, form .hs-richtext h4, form .hs-richtext h5, form .hs-richtext h6", "button, button, button, .button, .button, .button, .hs-button, .hs-button, .hs-button", "button, .button, .hs-button", ".button.button--secondary, .button.button--secondary, .button.button--secondary", ".button.button--secondary", ".header__menu-item--depth-1 > .header__menu-link, .header__menu-item--depth-1 > .header__menu-link", ".header__menu-item--depth-1 > .header__menu-link", ".header__menu-submenu .header__menu-link, .header__menu-submenu .header__menu-link", ".header__language-switcher .lang_list_class a, .header__language-switcher .lang_list_class a", ".header__menu-submenu .header__menu-link:not(.button)", ".footer .hs-menu-wrapper a, .footer .hs-menu-wrapper a", ".footer .hs-menu-wrapper a", "form .hs-richtext a", ".header__menu-item--depth-1 > .header__menu-link--active-link:not(.button)", ".footer .hs-menu-wrapper .active > a" ] } } ``` To get started generating this file, run the following [CLI command](/guides/cms/tools/local-development-cli#generate-theme-field-selectors-for-in-app-highlighting) to create the file. During file creation, a script will run to set up the initial field-selectors mappings. ```shell hs theme generate-selectors ``` | Parameter | Description | | ---------------------- | -------------------------------- | | `theme-directory-path` | The path to the theme directory. | After running the command, you'll need to review and refine the `editor-preview.json` file to ensure that fields and selectors are mapped properly. While the [generate-selectors](/guides/cms/tools/local-development-cli#generate-theme-field-selectors-for-in-app-highlighting) command will make a rudimentary guess about which fields affect which selectors, you'll need to make corrections based on how your theme is built. For example, this command cannot detect when modules are overriding styling or when you're using macros. To test these mappings, upload the theme to an account, then view the theme editor in that account (**Settings** \> **Website** > **Themes** \> **View theme**). # Write module and theme fields using JavaScript @ Learn how to write a module's fields.json file using JavaScript. $ guides & cms/content/fields --- # Write module and theme fields using JavaScript Typically, module and theme fields are configured in a `fields.json` file. However, if you prefer to not use JSON, you can configure your fields with a JavaScript file instead. Writing fields with JavaScript enables you to abstract fields that you use often, dynamically generate new fields, and more easily update existing fields. For example, when building a set of modules, you can abstract your module fields into partials which are then pulled into individual modules. By building modules that pull from one source of truth, you’ll no longer need to update those common fields in each module individually. Below, learn how to write fields using JavaScript, including the necessary CLI commands, along with some examples to get you started. ## Overview At a high level, to write fields using JavaScript: - In the module folder or project root, include a JavaScript fields file instead of the `fields.json` file. You can use any of the following extensions: - `fields.js` - `fields.cjs` - `fields.mjs` (requires Node 13.2.0+ to be installed). - Optionally, check your JavaScript before uploading to HubSpot by running `hs cms convert-fields`, which will save the output of the fields file locally as `fields.output.json`. - Upload the asset to HubSpot through the CLI by running `hs upload` or `hs watch` with a `--convertFields` flag. On upload, HubSpot will convert the JavaScript file to a `fields.json` file. When HubSpot converts the JavaScript file into the `fields.json` file, it does not store the original JavaScript file. To maintain a source of truth, it's recommended to use a version control system like Git. ## Requirements To use a JavaScript field file to generate fields: - The JavaScript fields file must be contained in a module folder or at the project root to be treated as the global `fields.json` file. - The module or theme must not also include a `fields.json` file. Including both will prompt you to select one or the other at upload. - The JavaScript fields file must export a function as its default export. The exported function can accept an array of strings called `fieldOptions`, and must return an array of objects. Each entry in the returned array must be one of the following: 1. A JavaScript object with [valid field key-value pairs](/reference/cms/fields/module-theme-fields). 2. A JavaScript object with a `.toJSON()` method. When run, this method must return a value that meets criterion 1. 3. A `Promise` that resolves to a JavaScript object that meets criterion 1. ## CLI commands Use the following CLI commands when writing module fields with JavaScript. ### Check JavaScript file locally Check your work locally by running `hs cms convert-fields`, which saves the output of your JavaScript fields file as `fields.output.json` in the same directory as the JavaScript fields file. `hs cms convert-fields` accepts the following flags: | Flag | Description | | --- | --- | | `--src` | The location of the JavaScript fields file, or a directory. | | `--fieldOptions=[options]` | Accepts a space-separated list that will then be passed to every exported JavaScript fields function as an array before compile-time.For example, you can configure fields to have different labels depending on whether a `--fieldOptions=qa` flag is set.[View an example of this below](#change-fields-based-on-passed-options). | ### Upload to HubSpot Upload to HubSpot to begin the process of converting the JavaScript file to a `fields.json` file by running either `hs upload` or `hs watch`. `hs upload` and `hs watch` accept the following flags: | Flag | Description | | --- | --- | | `--convertFields` | Enables JavaScript fields functionality. | | `--saveOutput` | Saves the JavaScript fields file output as `fields.output.json` in the same directory as the JavaScript fields file. If not included, HubSpot will save the output of the JavaScript fields file to a temporary directory then delete it after upload. | | `--fieldOptions=[options]` | Accepts a space-separated list that will then be passed to every exported JavaScript fields function as an array before compile-time.For example, you can configure fields to have different labels depending on whether a `--fieldOptions=qa` flag is set.[View an example of this below](#change-fields-based-on-passed-options). | ## Examples Below, review examples of using JavaScript to write fields files. ### Regular object The following `fields.js` file writes a regular object: ```js module.exports = (fieldOptions) => { return [ { required: true, locked: false, help_text: '', inline_help_text: '', name: 'button_text', label: 'Button text', type: 'text', default: 'Add a button link here', }, ]; }; ``` ### Common fields The following `fields.js` file creates a set of common fields to be used across multiple modules: ```js const setFieldParams = (field, params) => { return { ...field, ...params }; }; const defaultRichTextField = { type: 'richtext', enabled_features: [ 'font_size', 'standard_emphasis', 'block', 'font_family', 'alignment', ], display_width: null, required: false, locked: false, }; module.exports = (fieldOptions) => { let fields = [ setFieldParams(defaultRichTextField, { name: 'tier', label: 'Product tier', default: '

        Free

        ', }), setFieldParams(defaultRichTextField, { name: 'description', label: 'Product description', default: '

        For teams that need additional security, control, and support.

        ', }), ]; return fields; }; ``` ### Change fields based on passed options The following `fields.js` file changes the module's fields based on whether the `--fieldOptions=[qa]` flag was included when running `hs cms convert-fields`, `hs upload`, or `hs watch`: ```js module.exports = (fieldOptions) => { let fields = [...] if(fieldOptions.includes('qa')) { fields = fields.map((field) => { field["name"] += "_qa"; return field; }) } } return fields } ``` ### Add JSON from other files The following `fields.js` file includes styles from a `styles.json` file: ```js const fs = require('fs') module.exports = (fieldOptions) => { const fields = [...] const styles = JSON.parse(fs.readFileSync('../../json/styles.json')) return [fields, styles] } ``` ### Make async calls The following `fields.js` file includes an asynchronous call by setting the exported function as `async`. If you return a Promise, the CLI will wait for the Promise to resolve before writing the `fields.json` file. ```js module.exports = async (fieldOptions) => { const httpField = fetch('https://example.org/example.json').then((resp) => resp.json() ); return [httpField]; }; ``` ### Use external field object libraries The following `fields.js` file includes an external library. Libraries such as [@iGoMoon/hubspot-fields-js](https://github.com/iGoMoon/hubspot-tools/tree/main/packages/hubspot-fields-js) are supported, as their fields objects expose a `.toJSON()` function. ```js const { Field, Group } = require('@iGoMoon/hubspot-fields-js'); module.exports = (fieldOptions) => { return [ Field.text() .name('button_text', 'Button text') .required(true) .default('Add a button link here'), new Group().children([ Field.boolean() .id('1') .name('enable', 'Enable Field') .set('display', 'toggle') .default(true), Field.number().name('number', 'Number Fields'), Field.text() .name('css_class_name', 'CSS Class') .set('validation_regex', '-?[a-zA-Z]+[a-zA-Z0-9- ]+') .inlineHelpText('Enter a CSS class for additional styling'), ]), ]; }; ``` # HubSpot forms @ Learn how to add forms to your HubSpot website or external pages using modules, HubL, or the embed code. Then, learn about how to style them. $ guides & cms/content --- # HubSpot forms Use HubSpot forms to capture information from website visitors, which you can then access throughout HubSpot. You can share links to forms directly with users, [submit form data via the API](/guides/api/marketing/forms), and embed them on your website pages using the CMS. Forms are a core part of the HubSpot and can be created in HubSpot accounts of any subscription level. Not only are forms important for customer conversion, but also because form data can be used in other HubSpot tools and assets, such as smart content, lists, workflows, content personalization, and more. After [creating a HubSpot form](https://knowledge.hubspot.com/forms/create-forms), you can add it to your templates and pages. There are a few ways to add a form to a template, depending on your use case: - [Using the default form module](#the-default-form-module) - [Adding a form field to a custom module](#form-fields-in-custom-modules)[](#using-the-form-hubl-tag) - [Embedding using the form embed code](#using-the-form-embed-code) ## The default form module If your template has [drag and drop areas](/reference/cms/hubl/tags/dnd-areas), content creators can add the [default form module](/reference/cms/modules/default-modules#form) to a page from the page editor, then configure the form options in the left sidebar. To code a form module directly into a template with drag and drop areas, reference it as a `dnd_module`. ```hubl {% dnd_area "dnd_area" class='body-container body-container__landing', label='Main section' %} {% dnd_section vertical_alignment='MIDDLE' %} {% dnd_column width=6, offset=6 %} {% dnd_row %} {% dnd_module path='@hubspot/form' %} {% end_dnd_module %} {% end_dnd_row %} {% end_dnd_column %} {% end_dnd_section %} {% end_dnd_area %} ``` To add a form module to a template outside of a drag and drop area, you'll instead reference it as a standard `module`. ```hubl {% module "form" path="@hubspot/form" form={ "form_id": "9e633e9f-0634-498e-917c-f01e355e83c6", "response_type": "redirect", "message": "Thanks for submitting the form.", "redirect_id": null, "redirect_url": "http://www.google.com" } %} ``` With either implementation, you can add parameters to the module tag to specify settings such as the form to use and redirect options, as shown in the code example above. See the [default modules documentation](/reference/cms/modules/default-modules#form) for more information on available parameters. ### Cloning the default module In addition to using the default module as-is, you can clone it to make it editable, enabling you to customize it as needed. For example, you could clone the default form module, add a color field, then wrap the module's HTML in a `
        ` tag with styling to add the color as a background: - In the left sidebar design manager, navigate to the **@hubspot** folder, then right click **form.module** and select **Clone module**. - In the right sidebar, click **Add field**, then select **Color**. - Add a `
        ` tag around the HTML content, then include styling to reference the color field, such as: `
        ` ## Form fields in custom modules When creating a custom module, you can include a form in it by adding a [form field](/reference/cms/fields/module-theme-fields#form), along with adding the field's code snippet to the module HTML. For example, you may want to add a consultation request form to a module that contains an image of a product and a descriptive value proposition. To add a form field to a custom module from the design manager: - In the right sidebar of the module editor, click **Add field**, then select **Form**. - After adding the field, hover over the field in the right sidebar, then click **Actions** and select **Copy snippet**. - Paste the snippet into the module's HTML field. ### Limit form options in the editor Once added to a page, content creators typically have control over many aspects of the form, including which form to use and the form fields themselves. However, you can limit the amount of control given in the page editor by modifying the form module’s `fields.json` file [locally](/guides/cms/setup/getting-started-with-local-development) to include the following fields: | Parameter | Type | Description | | --- | --- | --- | | `disable_inline_form_editing` | String | Set the `disable_inline_form_editing` property to `true`to hide all inline form editing controls in the form module. This includes the form fields, submit button text, data privacy and consent options, and CAPTCHA. | | `required_property_types` | Array | An array that specifies which forms can be selected based on the property types of the form fields. Values include: `"CONTACT"`, `"COMPANY"`, and `"TICKET"`. | For example, if you’ve built out a module that should only be used for forms that enable visitors to contact your company’s various services departments, you could allow content creators to only be able to select forms that use ticket properties. ```json // Form field { "id": "843b4f0f-0ed7-5b10-e86a-5cc8a0f11a0c", "name": "form_field_1", "display_width": null, "label": "Form", "required": false, "locked": false, "type": "form", "disable_inline_form_editing": true, "required_property_types": ["TICKET"], "default": { "response_type": "inline", "message": "Thanks for submitting the form." } } ``` ## Using the form embed code When a module or HubL tag isn't ideal, or if you want to add a form to an external page, you can instead embed it using the [form embed code](https://knowledge.hubspot.com/forms/how-can-i-share-a-hubspot-form-if-im-using-an-external-site#add-the-form-embed-code). You can also customize the form embed code to extend the form's functionality. View all form embed code customization options and more in the forms reference guide. - Form customization options are only available for forms created in HubSpot that have been [set as raw HTML](https://knowledge.hubspot.com/forms/how-can-i-share-a-hubspot-form-if-im-using-an-external-site#with-css-in-your-external-stylesheet-marketing-hub-professional-enterprise-or-legacy-marketing-hub-basic-only). The HubSpot account must have a _**Marketing Hub**_ or **_Content Hub_** _Professional_ or _Enterprise_ subscription. - HubSpot forms should only be loaded using the HubSpot-hosted JavaScript file. Making a copy of the form embed code and self-hosting it is not supported. Any improvements that HubSpot makes to improve security, anti-spam, accessibility, and performance will not propagate to your form if you decide to self-host the HubSpot form embed code. Below is a sample of a form embed code without any customization. ```html ``` | Parameter | Type | Description | | --- | --- | --- | | `portalId` | Number or string | The ID of the HubSpot account that the form was created in. This is used to retrieve the form definition. | | `formId` | String | The form's ID, which is used to retrieve the form definition. | | `region` | String | The [region](https://knowledge.hubspot.com/account-security/hubspot-cloud-infrastructure-and-data-hosting-frequently-asked-questions) of the account where the form was created. This is used to retrieve the form definition. Possible values are `na1` or `eu1`. | ### Add internationalization By default, HubSpot provides a number of validation messages in many different languages, which you can access using the `locale` parameter. You can also add custom languages or override the error messages and date picker months/days displayed on the form using the `translation` parameter. Learn more about internationalization and form validation error messages in the [forms reference documentation](/reference/cms/forms). ```js hbspt.forms.create({ portalId: '', formId: '', locale: 'en', translations: { en: { fieldLabels: { email: 'Electronic mail', }, required: "Hey! That's required!", submitText: 'Custom Submit Button Text', submissionErrors: { MISSING_REQUIRED_FIELDS: 'Please fill in all required fields!', }, }, }, }); ``` ## Form events Forms allow two ways to bind functionality onto events: callbacks in the HubSpot form embed code and global form events. Learn more about the available form embed code callbacks and global form events in the [forms reference documentation](/reference/cms/forms). ```js // Example form embed code callback hbspt.forms.create({ portalId: '', formId: '', onBeforeFormSubmit: function ($form) { // YOUR SCRIPT HERE }, }); // Example listener window.addEventListener('message', (event) => { if ( event.data.type === 'hsFormCallback' && event.data.eventName === 'onFormSubmitted' ) { someAnalyticsLib('formSubmitted'); console.log('Form Submitted! Event data: ${event.data}'); } }); ``` ## Form styling While HubSpot offers [form styling from a global setting](https://knowledge.hubspot.com/forms/set-global-form-colors-and-fonts) and [form specific setting level](https://knowledge.hubspot.com/forms/create-forms#style-and-preview-your-form), you can also style a form depending on how it's added to your CMS pages. All forms generated on the HubSpot CMS (excluding using the form embed code) will ignore any styling that is configured via the global form settings or the form's individual settings. ### Styling forms via the default form module or HubL tag HubSpot forms added to HubSpot pages can be styled using your website's CSS. HubSpot includes a number of different classes and attributes on forms that are generated where you can apply styling. As a starting point, refer to the [HubSpot Boilerplate's form CSS](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/css/elements/_forms.css), which represents the best practices for how to style forms. ### Styling forms via a custom module Forms inside custom modules can be styled by CSS in the module's CSS pane within the design manager. By keeping CSS scoped to the module, you can ensure that whenever the module is added to a page, the styling comes with it. It's recommended to add a wrapper to the form, then using that wrapper as the top-level selector for the CSS. This will prevent your custom module's form styling from being overwritten by additional styles in your websites main stylesheet. Below is a screenshot of the custom module from above with form styling added to the CSS Pane. ### Define custom styling for embedded forms If you have a **_Marketing Hub_** or **_CMS Hub_** _Professional_ or _Enterprise_ subscription, you can use CSS variable definitions to define global styles, customize buttons and other inputs, as well as style your multi-step forms and progress bars. Learn more about how apply [custom CSS definitions to your embedded form](/reference/cms/forms#define-custom-styling-for-embedded-forms-using-css). ### Styling forms added via the form embed code When using the form embed code, you can style the form using the [global form styling settings](https://knowledge.hubspot.com/forms/set-global-form-colors-and-fonts) or using your website's CSS. Using the global form styling settings enables you to configure default settings for every form in the account. You can also [override these styles on an individual form within the form editor](https://knowledge.hubspot.com/forms/create-forms#style-and-preview-your-form). If you have a **_Marketing Hub_** or **_CMS Hub_** _Professional_ or _Enterprise_ subscription, you can select the **Set as raw HTML form** option when creating a form. This setting makes the form render as HTML instead of an iframe, which makes it easier to style the embedded form with CSS. Learn more about [styling embedded forms on the Knowledge Base](https://knowledge.hubspot.com/forms/how-can-i-share-a-hubspot-form-if-im-using-an-external-site). # Global Content @ Global content is content that can be reused across your templates to create consistent pieces of information such as website headers, footers, sidebars, logos, or other components. $ guides & cms/content --- # Global content Global content is content that is shared across portions of a website. Common examples are website headers, footers, and sidebars. As a developer, you'll specify which components should be global by using global partials or by making modules global. HubSpot provides a different editing experience for content editors that makes it easy to edit the global content and preview the changes across pages before publishing. To learn more about how to edit your global content, check out [how to use global content across multiple templates](https://knowledge.hubspot.com/design-manager/use-global-content-across-multiple-templates) on HubSpot Knowledge Base. ## Overview Global content is best used to display the same information across multiple pages. For example, your website's header and footer, such as the header at the top of this page. Below are some additional examples of areas where you can use global content: - Secondary navigation for different sections of your website - Copyright footers (or sub footers) - Blog post sidebars (for showing recent posts, author listings, and more) Because global content is used in multiple places throughout a website, it is even more crucial to [design and build your global partials and modules for accessibility](/guides/cms/content/accessibility). ### Global partials vs global modules As a developer, you can create global partials and global modules, with a few key differences between them: - Global partials are a type of template built using HTML & HubL that can be reused across your entire website. The most common types of partials are website headers, sidebars, and footers. - Global modules are modules that are made up of single or multiple pieces of content that can be used across multiple pages on your site. Some common types of global modules can be items such as blog subscribe forms, secondary navigation elements, and calls-to-action. You should avoid including global modules within global partials, as it can create a negative content editing experience. All modules and fields inside of your global partials and global modules are easily editable inside of the [global content editor](https://knowledge.hubspot.com/design-manager/use-global-content-across-multiple-templates). ## Global partials ### Create a global partial A global partial is a type of template, which you can create locally through the HubSpot CLI by using the [create command](/guides/cms/tools/local-development-cli#create), as shown below. ```shell hs create template ``` When prompted to pick a type of template, select `global partial`. This will create your template in the desired directory with the following template annotations included in the HTML file. ```html ``` To see a sample of a global content partial, [please reference our boilerplate on GitHub.](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/partials/header.html) ### Add drag and drop areas to global partials You can enable drag and drop content capabilities inside of your global partials by adding in `dnd_area` tags similar to enabling them in page templates. View our [Drag and Drop Area documentation](/reference/cms/hubl/tags/dnd-areas) for more information. ### Include a global partial in your existing template To add a global partial into one of your existing templates, use the `global_partial` HubL tag while referencing the path to your partial. Below is an example from [the CMS boilerplate](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/layouts/base.html#L21) using this tag. ```hubl {% global_partial path="../partials/header.html" %} ``` ``` Do not use `global_partial` within the `` of a template. Doing so would result in invalid HTML. In most situations where you'd want to use a global partial in the header, it may make more sense to use a global module instead with a [`{%require_head%}`](/reference/cms/hubl/tags/standard-tags#require-head) to insert custom code into the head, and still provide module fields. ## Global modules You can create global modules like any other module using the CLI by running the `hs create` command, as shown below. ```shell hs create module ``` A global module is differentiated by the `global` flag in the module's `meta.json` file. ```json // meta.json file { "css_assets": [], "external_js": [], "global": true, "help_text": "", "host_template_types": ["PAGE"], "js_assets": [], "other_assets": [], "smart_type": "NOT_SMART", "tags": [], "is_available_for_new_content": false } ``` You can also [create global modules in HubSpot by using the design Manager](https://knowledge.hubspot.com/design-manager/create-and-edit-modules). Learn more about working with modules in the following related resources: - [Modules overview](/guides/cms/content/modules/overview) - [HubSpot's default modules](/reference/cms/modules/default-modules) - [Configuring modules](/reference/cms/modules/configuration) - [Using modules in templates](/reference/cms/modules/using-modules-in-templates) # Google Sign-In for Memberships @ Learn more about setting up memberships for private content to allow visitors to sign in with their Google account. $ guides & cms/content/memberships --- # Google Sign-In for Memberships You can allow members to access your private content using Google Authentication. You will need to [create a Google Cloud Console account](https://console.cloud.google.com/) to link Google Authentication with your HubSpot account. ## Create a project In your Google Cloud Console account, you can create a project to manage sign-ins to your private content in HubSpot. - Navigate to [Google Cloud Console](https://console.cloud.google.com/). - In the top left, click **Select a project**. - In the dialog box, click **New project** in the top right. - Enter a **name** and **location** for your project, then click **Create**. ## Add app information Once you've set up your project, you can add information from your HubSpot account to allow it to process private content sign-in data. - Navigate to [Google Cloud Console](https://console.cloud.google.com/). - In the top left, click **Select a project**. - In the dialog box, select the **project** you just created. - In the top left, click the **navigation menu**, then select **API & Services > Oauth consent screen**. - In the _User Type_ section, select the **type of user** who will be accessing your private content. - **Internal**: only employees of your organization will be access your content. - **External**: other visitors will be accessing your content. - If you select _External_, you will be prompted to enter an **app name**, **support email** for members to contact you, and **developer contact information** for Google to contact you. - In the _Scopes_ section, select the following scopes: - .../auth/[userinfo.email](http://userinfo.email/) - .../auth/userinfo.profile - openid - Publish your app. ## Create credentials Once you've set up your project, you will need to link the project to your HubSpot private content. - Navigate to [Google Cloud Console](https://console.cloud.google.com/). - In the top left, click the **navigation menu**, then select **API & Services > Credentials**. - At the top of the page, click **Create credentials**. **** - Select **OAuth client ID**. - Select **web application**, then enter a **name** for your application. - In a separate tab, locate your credentials in HubSpot: - In your HubSpot account, click the **settings icon** in the top navigation bar. - In the left sidebar menu, navigate to **Content > Private Content**. - In the _Sign-in Options_ section, click **Set up social sign-on**. - In the right panel, click **Copy** next to the redirect URI. - In Google Cloud Console, paste the **redirect URI** into the _Authorized redirect URIs_ section. - Click **Create**. - Copy the **Client ID** and **Client Secret** fields from Google Cloud Console. - In HubSpot, paste this information into the **Client ID** and **Client Secret** fields in your social sign-on settings. - In HubSpot, click **Verify**. - Repeat this process for each separate domain in your HubSpot account. To access a different domain's redirect URI, click the **default domain name** dropdown menu in your HubSpot social sign-on settings and select a different **domain**. # Microsoft Sign-In for Memberships @ Learn more about setting up memberships for private content to allow visitors to sign in with their Microsoft account. $ guides & cms/content/memberships --- # Microsoft Sign-In for Memberships You can allow members to access your private content using their Microsoft account by configuring an application in Microsoft Azure. ## Create an application In your Microsoft Azure account, you can create an application to connect to HubSpot. - In your Microsoft Azure account, open [Microsoft Entry ID (Azure Activity Directory)](https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/Overview). - In the top left, click the **Add** dropdown menu and select **App registration**. - In the _Name_ field, enter an **internal name** for your application. - In the _Supported account types_ section, select the **account types** that will be able to view your private content using a Microsoft sign-in. The _Accounts in any organizational directory...and personal Microsoft accounts_ option is recommended to support the widest variety of accounts. - In the _Redirect URI_ section, you can provide a URI for the authentication response. It's recommended to return to this step later. - Click **Register** to complete application creation. ## Set up application information - In your Microsoft Azure account, open your newly created application. - In the left sidebar menu, click to expand **Manage**, then select **Authentication**. - In the _Platform considerations_ section, click **\+** **Add a platform**. - In the _Web applications_ section, select **Web**. - In a separate tab, locate your credentials in HubSpot: - In your HubSpot account, click the **settings icon** in the top navigation bar. - In the left sidebar menu, navigate to **Content > Private Content**. - In the _Sign-in Options_ section, click **Set up social sign-on**. - In the right panel, click the **Microsoft** tab. - Click **Copy** next to the redirect URI. - In Microsoft Azure, paste the redirect URI. - Repeat the process for each separate domain you have connected to HubSpot. To change the domain you are setting up, click the **domain** dropdown menu above the redirect URI field and select a **domain**. - In Microsoft Azure, click **Overview** in the left sidebar menu. - Click to expand the **Essentials** section. - Copy the Application (client) ID, then paste the ID into the _Client ID_ field in HubSpot. - Click **Add new certificate or** **secret**. - Click **+** **New client secret**. - Edit your client secret description and expiration settings. - Copy the client secret, then paste the value into the _Client Secret_ field in HubSpot. - In HubSpot, click **Verify**. # Memberships @ Memberships is a feature that makes it possible to require visitors to have an account in order to access content. $ guides & cms/content/memberships --- # Memberships Memberships is a feature that makes it possible to require visitors to have an account in order to access content. The account system leverages the HubSpot CRM and CRM Lists together with the ability for a visitor to create a password for their account. Marketers can easily [create pages on their sites that only contacts on specific lists in the CRM can access](https://knowledge.hubspot.com/cms-pages-editor/control-audience-access-to-pages). You can additionally restrict access to [knowledge base articles](https://knowledge.hubspot.com/cms-pages-editor/control-audience-access-to-pages#set-up-membership-registration-for-your-knowledge-base) and [blogs](https://knowledge.hubspot.com/cms-pages-editor/control-audience-access-to-pages#set-up-membership-registration-for-your-blog) using memberships. ## Membership user flow When contacts are granted access to content, which can occur when they join lists or though manual assignment, they are sent an email to register for your website, where they set a password to access content they have permission to access. Imagine a gym, which wishes to allow visitors to register for classes and view the classes they have registered for. When a visitor registers for a class, the form submission creates a contact in the HubSpot CRM and that contact is added to a list based on the form submission, which is used to grant access to a "My Events" page. The visitor receives a membership registration email that allows them to create a password for their membership account. Now, when the visitors log in to their account, the user can log in to the private "My Events" page using the email and password they set. Because the visitor is logged in, the developer who created the private content can render data about the logged in contact using data from the CRM. ## Membership HubL variables For some businesses, it may make sense to show different content based on if a user is signed in or not. There are HubL variables which developers can use to check to see if a contact is currently logged in on a website. The HubL variable [`request_contact.is_logged_in`](/reference/cms/hubl/variables#website-pages-variables) indicates if the current visitor is signed in to the website through memberships. It can be used within an `if` statement to conditionally render certain content, allowing you to individually cater your visitor's experience. ```hubl {% if request_contact.is_logged_in %} You're signed in! {% else %} Log In {% endif %} ``` If you want to display different content on the same page based on list membership, you can check the signed-in contacts list memberships using [`request_contact.list_memberships`](/reference/cms/hubl/variables#website-pages-variables) HubL variable, which returns a dict of list IDs the logged in contact is a member of. To personalize content without using memberships, you can use the [contact variable](/reference/cms/hubl/variables#general-variables) if a visitor has submitted a form on your website. ## CRM object HubL functions In addition to general content displayed conditionally on a page, it’s possible to pull information about objects within your HubSpot account such as contacts, companies, deals, and products using the functions: - [CRM Associations](/reference/cms/hubl/functions#crm-associations) - [CRM Object](/reference/cms/hubl/functions#crm-object) - [CRM Objects](/reference/cms/hubl/functions#crm-objects) For security purposes, only product and marketing event objects can be retrieved on a publicly accessible page; to pull information about other object types, a page must be behind membership. ```hubl {% if request_contact.is_logged_in %} {% set membership_contact = crm_object('contact', request.contact.contact_vid, 'firstname,lastname') %} Welcome back, {{ membership_contact.firstname }} {{ membership_contact.lastname }} {% else %} Log In {% endif %} ``` ## Private blog posts with self-registration When you [enable self-registration for private blog content](https://knowledge.hubspot.com/blog/use-self-registration-for-private-blog-content), you can limit access to specific blog posts so that visitors must register to view them. On the blog listing page, posts enabled for self-registration will display with a lock icon when using the default blog listing module. In the blog post, the content above the Read More separator will display, then prompt the user to log in to keep reading. Visitors can then sign up to read the rest of the blog post. ### Customization with HubL If you're using HubSpot's default blog listing module, the lock icon styling is handled for you. However, if you want to build your own solution with HubL, you can use the [flag_content_for_access_check](/reference/cms/hubl/functions#flag-content-for-access-check) function to check whether or not a blog post is visible to the currently logged in visitor. If the user is not currently logged in, the function will check whether the blog post is private. When called, the function is replaced with the following attribute, which shows whether the visitor has access to the content: `hs-member-content-access=` When `true`, the content is private and cannot be viewed by a viewer who is not logged in. For example, you could update your blog listing template with the following. Note that the `lock-icon` class can be any value, as long as you use the same value in your CSS to style the locked post indicator. ```hubl {% for content in contents %}
        {% endfor %} ``` This would result in the following HTML output after the script runs to and finds that a blog post is private: ```html
        ``` You could then handle the locked/unlocked styling through CSS using the `hs-member-content-access` attribute. Note that the `lock-icon` class is just an example; you can use any value, as long as it matches the HTML class shown in the first code example above. ```css article[hs-member-content-access] .lock-icon { display: none; } article[hs-member-content-access='true'] .lock-icon { display: inline-block; } ``` ### Customization with JavaScript Behind the scenes, the `flag_content_for_access_check()` function is calling an API to check whether the current visitor has access to the content based on the visitor cookies that are passed along with the request. If you'd like to create your own JavaScript solution, you can call this API directly. To check whether or not a post is locked to the current visitor, you can make a `POST` request to `https://your-domain.com/_hcms/content-access/get-gated-content-ids-for-member`. The request body should include an object with a `contentIds` array containing the IDs of the blog posts that you want to check member access for. For example, if you wanted to check whether blog posts with the IDs of `10`, `11`, and `12` are locked to the visitor, your request body would be: ```json // Example request body { "contentIds": [10, 11, 12] } ``` The response will contain the IDs of the blog posts that are locked to the user that is currently viewing the page: ```json // Example response { "gatedContentIds": [10] } ``` See below for a full example of a custom JavaScript implementation. ```js // Custom JS example const hsFlaggedContentIds = // an array of target id's to test against const gatedAttributeName = 'hs-member-content-access'; const customEventName = "hsAccessCheckFinished" async function getGatedContentIds(idsToCheck) { const options = { method: 'POST', body: JSON.stringify({ contentIds: idsToCheck }), headers: { 'Content-Type': 'application/json', }, }; const fetchGatedIds = await fetch( '/_hcms/content-access/get-gated-content-ids-for-member', options ); const response = await fetchGatedIds.json(); return response['gatedContentIds']; } function setGatedElementAttribute(element, gatedContentIds) { const contentId = parseInt(element.getAttribute(gatedAttributeName)); if (contentId && gatedContentIds.includes(contentId)) { // content is gated element.setAttribute(gatedAttributeName, true); } else { // content is not gated element.setAttribute(gatedAttributeName, false); } return element } getGatedContentIds(hsFlaggedContentIds).then((gatedContentIds) => { const flaggedElementsToCheckIfGated = document.querySelectorAll( '[' + gatedAttributeName + ']' ); // split the original array into 2 arrays and wrap in an object // that will be passed into the custom event. Event handlers will have // access to both gated and ungated arrays. return [...flaggedElementsToCheckIfGated].reduce((acc, item) => { let processedElement = setGatedElementAttribute(item, gatedContentIds); if (processedElement.getAttribute(gatedAttributeName) === "true" ) { acc.gatedContentElements.push(item); } else { acc.ungatedContentElements.push(item); } return acc }, { gatedContentElements: [], ungatedContentElements: [] }); }).then(processedGatedElements => { document.dispatchEvent( new CustomEvent( customEventName, { detail: { gatedContentElements: processedGatedElements } }) ); }).catch(err => { console.error(err) }); ``` ## Register, login, and log out When a contact is granted access to any content on your website through memberships, they will receive an email to register for your website where they can set a password to access content they have permission to view. In the event you need to resend a contact a link to register, you can [resend their registration email](https://knowledge.hubspot.com/cms-pages-editor/control-audience-access-to-pages#resend-the-registration-email-to-a-specific-contact). The URL paths for user sign-in/out are consistent for all HubSpot CMS sites with the membership functionality. - `/_hcms/mem/login` - `/_hcms/mem/logout` When a visitor logs in to their website, a cookie is sent to their browser, allowing them to browse through your website and access pages they have access to through memberships without having to log in again. If a visitor logs out, or has never logged in to your website in their browser, they will be prompted to log in before being able to view the content. ## Membership templates Sites with memberships have a few special pages that are needed to facilitate the memberships functionality. These are dictated by special system templates. These system templates are editable, allowing you to control the look and feel of the various membership steps. To set which templates you are using, go to **Settings >** [**Private Content**](https://app.hubspot.com/l/settings/content-membership/all-domains/general) and choose the ["Templates" tab](https://app.hubspot.com/l/settings/content-membership/all-domains/templates). To create a template to set in these settings, go to **Marketing > Files and Templates > Design Tools**, then in the top left click **File > New File > HTML & HUBL** and select the appropriate Template Type from the dropdown. For a comprehensive list of the membership templates, refer to the [membership section of the templates documentation](/guides/cms/content/templates/overview#membership). Only [HTML + HubL templates](/guides/cms/content/templates/types/html-hubl-templates) can be membership templates. ## Membership audit logging In [Settings > Private Content](https://app.hubspot.com/l/settings/content-membership/all-domains/general), you can view an audit log of what visitors have been interacting with content behind memberships. This allows you to see which visitors are viewing private content. ## SSO for Memberships You can also manage all of your businesses access permission and authentication needs in a single system with [Single Sign-on (SSO) for Memberships.](/guides/cms/content/memberships/sso) ### Social logins You can provide users in your list a way to sign in using Google or Facebook instead of entering their email address and password. The social login provider sends the email address associated with the logged in social account. That email address is then used to validate if that contact is in a contact list with access to the content. This feature does not require you to have configured SSO settings. You need to have a page set to "Private registration required" with a contact list. Additionally your login template needs to have the `membership_social_logins` module. [Add social login to your membership pages](/guides/cms/content/memberships/social) ## Membership related articles and resources - [HubSpot Essentials for Developers: Getting Started with Memberships](https://developers.hubspot.com/blog/essentials-for-getting-started-with-memberships) - [Creating menus that adapt to whether the user is logged in or not](https://developers.hubspot.com/blog/creating-a-header-nav-that-adapts-to-if-the-contact-is-logged-in) # How to add social login for membership pages @ In this guide you will add social login capability to your login template. $ guides & cms/content/memberships --- # How to add social login for membership pages Remembering your password across the hundreds of sites we interact with day to day can be hard. Especially if you follow best practices and have separate passwords for all of your accounts. You can make it easier on users who have memberships with your website by providing the ability to login using social accounts like Facebook and Google. In this guide you will add social login capability to your login template ## What you need before you begin - Memberships functionality (requires CMS Enterprise) - A membership login template - A website page you wish to restrict access to - A contact list you will give page access to (a list which just contains your email would be good for testing before using a real list) ## 1. Open your login template In your theme, find and open your login template. Membership login templates are required to have the template annotation `templateType: membership_login_page` ## 2. Add the membership social login module Add the code for the `membership_social_logins` module to your template where you want the button(s) to appear. ```hubl {% module "social" path="@hubspot/membership_social_logins" %} ``` The module supports both Google and Facebook login. You can add both, or just one of them to your login page. The module supports both Google and Facebook login. You can add both, or just one of them to your login page. ### Add Google login button ### Add Facebook login button HubSpot does not have control over the UI that Google and Facebook provide. Should their UI change these instructions may become confusing or no longer work. The most important part is that you create a client/app, then get it's ID. Provide that id through the default module's parameter for that provider and their respective "enabled" parameter. Below is an example of what your code may look like. If you are only adding one of the providers, you would not need to include an id, and the enabled parameter for services you are not supporting. ```hubl {% module "social" path="@hubspot/membership_social_logins", clientid="1234567890-abc123def456.apps.googleusercontent.com" appid="12345678" facebook_enabled=true google_enabled=true %} ``` ## 3. Test the social login 1. [Create a contact list](https://knowledge.hubspot.com/lists/create-active-or-static-lists#set-up-a-new-list) with just your email address in it. _Email address must also be used for your Google or Facebook account._ 2. [Set a page to "Private registration required",](https://knowledge.hubspot.com/website-pages/require-member-registration-to-access-private-content#set-up-membership-registration-for-pages) choose your newly created test list. 3. Visit one of these pages using incognito mode so you are not signed in. You should see your login template with the social login functionality. 4. Attempt to log in using the social login buttons. If you're seeing any issues, look back through the instructions, ensure your client ID or app ID is entered correctly and passed to the module. Ensure if you have security set up for it that your site's domain is set as a trusted domain in the app settings. ## You're done! Congratulations you successfully added social login functionality to your login template! Your users can now use their Google and/or Facebook to log in. ## Related resources - [Memberships](/guides/cms/content/memberships/overview) - [Memberships SSO](/guides/cms/content/memberships/sso) - [CRM Objects in CMS Hub](/guides/cms/content/data-driven-content/crm-objects) - Membership social logins module reference # SSO for Memberships @ Manage all of your businesses access permission and authentication needs in a single system with single sign-on (SSO) for memberships. $ guides & cms/content/memberships --- # SSO for Memberships Manage all of your businesses access permission and authentication needs in a single system with single sign-on (SSO) for [memberships](/guides/cms/content/memberships/overview). This system allows you to manage access to your company’s applications across your stack, giving your end-users a single username and password combo for all of the applications and content they should have access to. This setup process must be done by an IT administrator with experience creating applications in your identity provider account. At this time, we only support SAML-based applications. ## Initial Setup Follow the steps below to begin setting up your SSO for memberships. The navigation instructions and field names described below may differ across identity providers. You can find more specific instructions for setting up applications in commonly used identity providers below: • [Okta](#okta) • [OneLogin](#onelogin) ### 1. Login to your identity provider. ### 2. Navigate to your applications. ### 3. Create a new SAML application specifically for HubSpot content access. To get the Audience URI and Sign on URL, ACS, Recipient, or Redirect values: - In your HubSpot account, click the settings icon settings in the main navigation bar. - In the left sidebar menu, select Private Content. - Select a domain from the “choose a domain to edit” picklist to open the settings for that domain. Note\* SSO must be enabled on a per-subdomain basis at this time. - In the _Single sign-on (SSO)_ section, click Set up. - In the right pane, click Copy next to the values as needed. If you are using Microsoft AD FS, click the Microsoft AD FS tab to copy the values needed. - Paste them into your identity provider account where required. ### 4. Copy the identifier or issuer URL, the single-sign on URL, and the certificate from your identity provider, and paste them into the corresponding fields in the SSO setup panel in HubSpot. ### 5. Click Verify. Once verification is complete, a “single sign on is enabled” notification will appear at the top of the General & Templates tabs for that domain and all template and email settings options that are no longer managed through HubSpot (because they are now managed through your IaP) will be disabled. ## SSO Enablement for Blogs ### 1. Navigate to settings > CMS > Blog in the Settings UI. ### 2. Select a blog that is currently hosted on an SSO enabled subdomain from the “select a blog to modify” list. ### 3. Locate the control audience access settings at the bottom of your blog’s general tab. Visit the [control audience access option settings section](/guides/cms/content/memberships/sso#control-audience-access-option-settings) for more information on these choices. ## SSO Enablement for Pages / Landing Pages ### 1. Navigate to marketing > website > website pages or landing pages. ### 2. Select a single page or landing page on an SSO enabled domain, or select multiple pages or landing pages on an SSO enabled domain using the checkbox option in the listing’s area, and click the “control audience access” option at the top of the table. These options are also available in the page and landing page editor > settings tab. Visit the [control audience access option settings section](/guides/cms/content/memberships/sso#control-audience-access-option-settings) for more information on these choices. ## SSO Enablement for Knowledge Articles **Note:** Articles must be set at the article level at this time. We will address global SSO settings options for the knowledge base at a later time. ### 1. Navigate to settings > knowledge base > articles ### 2. Select a single article on an SSO enabled domain or select multiple articles on an SSO enabled domain using the checkbox option in the listing’s area, and click the “control audience access” option at the top of the table. **Note:** These options are also available in the article editor > settings tab. Visit the [control audience access option settings section](/guides/cms/content/memberships/sso#control-audience-access-option-settings) for more information on these choices. ## Control Audience Access Option Settings If you would like all users in your IaP that have HubSpot as an assigned app to be able to see your private content, **select the Private - Single sign on(SSO) required** option. If you would like to segment users with the assigned HubSpot app in your IaP into smaller tiered groups, **select the Private - Single sign on (SSO) required with list filtering** option. - This option requires users to be both a member of your IaP with the assigned app AND a member of a contact list within HubSpot in order to view pages. The benefit of this option is that it allows you to further refine access if your business operates on a tiered subscription model, for example members get access to different content materials depending on their bronze, gold, or platinum subscription levels. - **Note:** This is also the default option for content previously marked as “Private - registration required”. If you have content previously marked as "Private - registration required" and would like to transition fully to SSO, please verify that all contacts currently in those assigned lists are added to your IaP before switching over to unfiltered SSO management. Failure to do so will result in contacts losing access to that content. ## Instructions for specific identity providers ### Okta You need administrative access in your Okta instance. This process is only accessible in the Classic UI in Okta. - Log in to Okta. Make sure you are in the administrative instance of your Okta developer account. - Click **Applications** in the top navigation bar. - Click **Add application**. - Click **Create new app.** - Select **Web** as your platform and **SAML 2.0** as your Sign On method then click **Create.** - Fill out the General Settings screen as desired then click **Next**. - On the Configure SAML screen, copy and paste the **Sign on URL** and **Audience URI** from HubSpot into Okta, then click **Next** and finish the app creation process. - Navigate to the **Assignments** tab. Assign the new app to any users that will have access to private content, including yourself. - Navigate to the **Sign On** tab. Click the **View setup instructions** button. - Copy the Identity Provider **Single Sign-On URL**, **Identity Provider Issuer** and **X.509 Certificate** from Okta into HubSpot. - Click **Verify**. You'll be prompted to log in with your Okta account to finish the configuration and save your settings. ### OneLogin You need administrative access in your OneLogin instance to create a new SAML 2.0 application in OneLogin, as required. - Log in to **OneLogin**. - Navigate to **Applications**. - Click **Add App**. - Search for **SAML Test Connector (Advanced)** and select the app. - Click **Save**. - Click the **Configuration** tab. - Copy and paste the **Audience** and **Recipient** from HubSpot into the corresponding fields in OneLogin. - In the upper right, click **Save**. - Click the **SSO** tab. - Copy the **Issuer URL** and **SAML 2.0 Endpoint** (Single Sign-on URL) into HubSpot. - Click **View Details** under the X.509 Certificate then copy and paste the certificate into HubSpot. - Click the **Users** tab in the top left and add yourself and any other users that should have access to private content to the OneLogin application you created. - Click **Verify**. You'll be prompted to log in with your OneLogin account to finish the configuration and save your settings. ## Frequently Asked Questions ### What happens to my content if I disable SSO for a domain? If you disable SSO for a domain that has published private content on it today, any Private - Single sign on (SSO) required pages will become fully public. Any content marked Private - Single sign on (SSO) required with list filtering will remain private and will be inaccessible to all users. ### Can I go back to the old Private - registration required option? How To go back to simple registration through HubSpot, our recommendation is to first change all sensitive content over to Private - Single sign on (SSO) required with list filtering, then disable SSO for that domain, and then to change all private content over to Private - registration required. During this switch we recommend reviewing any lists or workflows used to populate lists for registration purposes to ensure things are correct before saving the changes. # Menus and Navigation @ HubSpot has a few built-in tools to make the creation and management of these menus easy, and on the code side multiple solutions for displaying menus so you can pick the solution that makes the most sense for your website. $ guides & cms/content --- # Menus and Navigation Include navigation menus on your website to help users find the information that the need. Navigation menus are often located in the headers, sidebars, and footers of a website. HubSpot provides a few built-in solutions for adding menus to your pages, depending on your use case, but you can also create your own menus when needed. Your account also includes a [settings page for creating and managing menus](#navigation-settings), which the various menu methods can reference. - **Default menus:** HubSpot provides two default menu types that can be used as needed out of the box. These menus can be added as modules in the page editor in [drag and drop areas](/guides/cms/content/templates/drag-and-drop/overview) or in templates, or you can add them to custom modules using [HubL tags](#menu-hubl-tags). - **Menu:** commonly used for global navigation, such as in the website header or footer, the default standard menu enables you to select a menu that you've configured in your navigation settings, then configure it further with options such as maximum levels, display settings, and orientation. - **Simple menu:** commonly used for page-specific navigation, such as pillar pages, the simple menu module enables you to create menus at the page level. Rather than reference a menu that you've built in your navigation settings, simple menu items are managed in the content editor and have fewer configuration options than the standard menu. This enables content creators to update menus on specific pages as needed without impacting global navigation. - **Custom menus:** when the default menu options don't fit your needs, you can create your own custom solutions. This can range from building custom modules that include default menus using the standard [menu and simple_menu HubL tags](#menu-hubl-tags), to using the [menu() HubL function](#menu-hubl-function) to build out a completely custom solution using repeater groups or HubDB. That being said, when building a complicated custom menu, you should keep in mind the editor experience. In many cases, it may make more sense to use the `menu` and `simple_menu` fields in tandem with the `menu()` function so that there's a balance between custom solution and intuitive editing experience. Below, learn more about the different ways to include menus on pages and how to manage navigation settings in HubSpot. ## Navigation Settings In each account, HubSpot includes [navigation settings](https://knowledge.hubspot.com/cos-general/set-up-your-site-s-navigation-menus) so that you can create multi-level menus to reference in menu modules and tags. This creates a single source of truth for a set of menu items, so you'll only need to update a menu once to update all pages referencing that menu. You can create as many menus as needed, and each menu comes with options for cloning, deleting, renaming, and displaying revision history. To create and manage menus in HubSpot, navigate to **Settings** > **Website** > **Navigation menus**. Learn more about navigation settings in [HubSpot's Knowledge Base](https://knowledge.hubspot.com/website-pages/set-up-your-site-s-navigation-menus). When working with menus in HubL, you may need to reference the menu's ID. This does not apply to simple menus, as their menu items are set in the editor. When building a custom module, the easiest way to get the menu ID is by creating a menu field. This field enables the content creator to select a menu from a dropdown menu, and returns the menu ID. If you need to hardcode the menu ID, you can retrieve it from the URL when viewing the menu in the navigation settings page. Note that, when you first arrive at the page, the default menu ID will not display in the URL. To get the ID for that default menu, you'll need to select a different menu, then select the default menu again. A best practice for site headers, which often contain the lengthy main navigation, is to provide a ["skip to content" link](/guides/cms/content/accessibility#adding-a-skip-to-content-link). This helps users navigating by keyboard, skipping over lengthy menus. ## HubL menu tags Use the `menu` and `simple_menu` HubL tags to add menu functionality to custom modules. Adding the tag to a module will render the menu on the page. To enable content creators to configure the menu's options in the page editor, you'll need to include the [menu](/reference/cms/fields/module-theme-fields#form) or [simple menu field](/reference/cms/fields/module-theme-fields#simple-menu) in the module as well. Below, learn more about each type of menu tag. ### Standard menu The [HubL Menu tag](/reference/cms/hubl/tags/standard-tags#menu) generates standard menu HTML with class names already provided for depth levels, active states, and if the item has children. The menu tag can be used within [custom modules](/guides/cms/content/modules/overview) making it an easy way to create navigation menus for main nav's and sidebar navigation. This tag expects you to provide the [menu ID](#menu-id). ```hubl {% menu "menu" %} {% menu "my_menu" id=456, site_map_name='Default', overrideable=False, root_type='site_root', flyouts='true', max_levels='2', flow='horizontal', label='Advanced Menu' %} ``` ### Simple menu The [simple menu](/reference/cms/hubl/tags/standard-tags#simple-menu) tag functions just like the menu tag by generating [standard menu HTML](https://developers.hubspot.com/docs/cms/hubspot-menu-markup) with class names for depth levels, active states, and if the item has children. The difference is that this tag expects you to provide a dict of the menu structure instead of a menu ID. This is useful for when you want a module's fields to determine the structure of a menu instead of using the navigation settings. For example, you may want to use this type of module for the table of contents of a pillar page. ```hubl {% simple_menu menu_tree=[{"contentType": null, "subCategory": null, "pageLinkName": null, "pageLinkId": null, "isPublished": false, "categoryId": null, "linkParams": null, "linkLabel": "Home", "linkTarget": null, "linkUrl": "http://www.hubspot.com", "children": [], "isDeleted": false}, {"contentType": null, "subCategory": null, "pageLinkName": null, "pageLinkId": null, "isPublished": false, "categoryId": null, "linkParams": null, "linkLabel": "About", "linkTarget": null, "linkUrl": "http://www.hubspot.com/internet-marketing-company", "children": [{"contentType": null, "subCategory": null, "pageLinkName": null, "linkUrl": "http://www.hubspot.com/company/management", "isPublished": false, "children": [], "linkParams": null, "linkLabel": "Our Team", "linkTarget": null, "pageLinkId": null, "categoryId": null, "isDeleted": false}], "isDeleted": false}, {"contentType": null, "subCategory": null, "pageLinkName": null, "pageLinkId": null, "isPublished": false, "categoryId": null, "linkParams": null, "linkLabel": "Pricing", "linkTarget": null, "linkUrl": "http://www.hubspot.com/pricing", "children": [], "isDeleted": false}] %} ``` ## Default menu modules HubSpot provides default modules that you can add to coded templates, as well as pages through the page editor when a template includes [drag and drop areas](/reference/cms/hubl/tags/dnd-areas). Each module will have a different editing experience, with the standard menu having more configuration options than the simple menu. Because modules cannot be nested, you cannot place these modules inside of other modules. Instead, you should use the [menu or simple menu tags](#hubl-menu-tags). ```hubl {% module "main_nav" path="@hubspot/menu", label="Menu" id="123456" %} {% module "menu" path="@hubspot/simple_menu", label="Simple Menu" menu_tree=[{"contentType": null, "subCategory": null, "pageLinkName": null, "pageLinkId": null, "isPublished": false, "categoryId": null, "linkParams": null, "linkLabel": "Home", "linkTarget": null, "linkUrl": "http://www.hubspot.com", "children": [], "isDeleted": false}, {"contentType": null, "subCategory": null, "pageLinkName": null, "pageLinkId": null, "isPublished": false, "categoryId": null, "linkParams": null, "linkLabel": "About", "linkTarget": null, "linkUrl": "http://www.hubspot.com/internet-marketing-company", "children": [{"contentType": null, "subCategory": null, "pageLinkName": null, "linkUrl": "http://www.hubspot.com/company/management", "isPublished": false, "children": [], "linkParams": null, "linkLabel": "Our Team", "linkTarget": null, "pageLinkId": null, "categoryId": null, "isDeleted": false}], "isDeleted": false}, {"contentType": null, "subCategory": null, "pageLinkName": null, "pageLinkId": null, "isPublished": false, "categoryId": null, "linkParams": null, "linkLabel": "Pricing", "linkTarget": null, "linkUrl": "http://www.hubspot.com/pricing", "children": [], "isDeleted": false}] %} ``` ### Standard menu markup Default menu modules are powered by their respective HubL menu tags (`menu` and `simple_menu`) to generate standard menu HTML. Like other HubSpot modules, menu modules are wrapped in module wrapper markup. These `div` and `span` tags make the module editable with the content editor. The menu markup of both the menu and simple menu modules is the same, with the exception of some of the classes applied to the wrapper and menu containers. ```html ``` As shown above, the actual menu renders as a `ul` wrapped in a `div` with the `hs-menu-wrapper` class. This wrapper will have additional classes based on how the module is configured in the page editor, such as enabling flyouts. Learn more about [classes added by these settings](#classes-added-by-settings) below. Within the `ul`, each menu item is an `a` tag wrapped in a `li`. The `li` tag has a class that indicates the depth of the item in the menu tree (e.g., `hs-menu-depth-1`). When a menu item contains a nested child item, the corresponding `li` will have the additional class of `hs-item-has-children`. The child menu renders as a nested `ul` with the class `hs-menu-children-wrapper`. When you visit a page that is included in your menu tree, the class `active-branch` is added to the parent `li` items and a class of **`active`** is added to that page's particular `li` item. ### Standard menu styling At the module level, whether editing a menu module in the page editor or editing a menu field in a custom module, you'll have a few configuration options. The _Menu_, _Advanced menu type_, and _Max levels_ fields enable you to control the menu items are rendered as `li` in the page markup. But the orientation and flyouts options will impact the CSS selectors added to the menu wrapper `div`. You can then target these selectors in your CSS. Below, learn more about the classes that are added to the menu wrapper `div` depending on these field settings. | Class | Description | | --- | --- | | `hs-menu-flow-horizontal` | Added to the wrapper `div` when the menu is set to horizontal orientation. | | `hs-menu-flow-vertical` | Added to the wrapper `div` when the menu is set to vertical orientation. | | `flyouts` | Added to the wrapper `div` when _Enable flyouts_ is selected. | | `no-flyouts` | Added to the wrapper `div` when _Enable flyouts_ is not selected. | To get you started styling menus, below are some example CSS selectors that can be used to style the menu tag and default menu module. ```css /* Menus */ .hs-menu-wrapper ul { /* Targets all unordered lists within HubSpot menus */ } /* Horizontal Menu ========================================================================== */ .hs-menu-wrapper.hs-menu-flow-horizontal ul { /* Targets all unordered lists within horizontal menus */ } .hs-menu-wrapper.hs-menu-flow-horizontal ul li { /* Targets all list items within horizontal menus */ } .hs-menu-wrapper.hs-menu-flow-horizontal ul li a { /* Targets all links within horizontal menus */ } .hs-menu-wrapper.hs-menu-flow-horizontal > ul { /* Targets the top-level unordered list within horizontal menus */ } .hs-menu-wrapper.hs-menu-flow-horizontal > ul li.hs-menu-depth-1 { /* Targets top-level list items within the top-level unordered list */ } .hs-menu-wrapper.hs-menu-flow-horizontal > ul li a { /* Targets top-level list links within the top-level unordered list */ } .hs-menu-wrapper.hs-menu-flow-horizontal > ul li.hs-item-has-children { /* Targets list items with children with the top-level unordered list */ } .hs-menu-wrapper.hs-menu-flow-horizontal.flyouts > ul li.hs-item-has-children ul.hs-menu-children-wrapper { /* Targets second-level unordered lists when flyouts are enabled (for styling dropdowns) */ } .hs-menu-wrapper.hs-menu-flow-horizontal > ul li.hs-item-has-children ul.hs-menu-children-wrapper li a { /* Targets links within second-level unordered lists */ } .hs-menu-wrapper.hs-menu-flow-horizontal.flyouts > ul li.hs-item-has-children ul.hs-menu-children-wrapper li.hs-item-has-children ul.hs-menu-children-wrapper { /* Targets third-level unordered lists (for styling dropdowns)*/ } .hs-menu-wrapper.hs-menu-flow-horizontal.flyouts > ul li.hs-item-has-children:hover > ul.hs-menu-children-wrapper { /* Targets second-level unordered list when top-level menu item is hovered (use to reveal dropdowns) */ } .hs-menu-wrapper.hs-menu-flow-horizontal > ul li.hs-item-has-children.active-branch { /* Targets top-level active branch unordered list */ } .hs-menu-wrapper.hs-menu-flow-horizontal > ul li.hs-item-has-children.active-branch > ul.hs-menu-children-wrapper { /* Targets second-level unordered list within active branch */ } .hs-menu-wrapper.hs-menu-flow-horizontal li.active a { /* Targets the link within the active list item */ } /* Vertical Menu ========================================================================== */ .hs-menu-wrapper.hs-menu-flow-vertical ul { /* Targets all unordered lists within vertical menus */ } .hs-menu-wrapper.hs-menu-flow-vertical ul li a { /* Targets all list items within vertical menus */ } .hs-menu-wrapper.hs-menu-flow-vertical ul li a { /* Targets all links within vertical menus */ } .hs-menu-wrapper.hs-menu-flow-vertical > ul { /* Targets the top-level unordered list within vertical menus */ } .hs-menu-wrapper.hs-menu-flow-vertical > ul li.hs-menu-depth-1 > a { /* Targets top-level links in vertical menus */ } .hs-menu-wrapper.hs-menu-flow-vertical > ul li.hs-item-has-children { /* Targets top-level list items with children */ } /* No flyouts ========================================================================== */ .hs-menu-wrapper.hs-menu-flow-vertical.no-flyouts .hs-menu-children-wrapper { /* Targets child menus when flyouts are disabled */ } .hs-menu-wrapper.hs-menu-flow-horizontal.no-flyouts > ul li.hs-item-has-children ul.hs-menu-children-wrapper { /* Targets second-level child menus when flyouts are disabled */ } ``` ## HubL menu() function The [menu()](/reference/cms/hubl/functions#menu) function exists to enable you to create fully custom menu structures. It returns an object that you can iterate through to generate a menu, there are many [provided properties for the menu items](/reference/cms/hubl/variables#menu-node-variables). Be aware when you use the menu function [you are fully responsible for the accessibility of your menu](/guides/cms/content/accessibility), the structure, and the styling. ```hubl {% set node = menu(987) %} {% for child in node.children %} {{ child.label }}
        {% endfor %} {% set default_node = menu("default") %} {% for child in default_node.children %} {{ child.label }}
        {% endfor %} ``` The [HubSpot CMS Theme Boilerplate](https://github.com/HubSpot/cms-theme-boilerplate) contains an [example menu module built with the menu() function](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/modules/menu.module/module.html). You can modify that to meet your needs to make complicated menus. # Hide modules and sections in the page editor @ Learn how to hide modules and sections in the page editor for your custom theme. $ guides & cms/content/modules --- # Hide modules and sections When developing a theme, you can configure the `theme.json` to hide specific modules and sections from the editor. This enables you to curate the list of modules and sections available to content creators when building pages, blogs, and global content, rather than having all modules and sections appear for all content types. Using this feature, you can also hide [HubSpot default modules](/reference/cms/modules/default-modules) in favor of your own versions. You can hide modules and sections in the following ways: - **Within themes:** you can hide default modules, but not custom modules. Similarly, you cannot hide custom sections in a theme. To exclude a custom module or section from a theme, you should instead delete the module or section from the theme. - **Within templates:** you can hide default modules, custom modules, and sections. When hiding modules and sections, keep the following in mind: - When hiding HubSpot default modules in a template, ensure that the module is wrapped in single quotes (`'`). For example, `'@hubSpot/follow_me'`. This complies with YAML, which template annotations are based on. - You must use the relative path when specifying a hidden module. Using an absolute path will result in the module not being hidden. ## Hide default modules in a theme To hide all default modules, set `"hide_all_default_modules": true` in the `theme.json` file To hide specific default modules in a theme, you'll need to add a list of hidden modules to a `hidden_modules` array in the `theme.json` file. For example, if you wanted to hide HubSpot's default button and form modules from a theme, your code would look like the following: ```json // example theme.json { "label": "CMS Theme Boilerplate", "preview_path": "./templates/home.html", "screenshot_path": "./images/template-previews/home.png", "enable_domain_stylesheets": false, "license": "./license.txt", "responsive_breakpoints": [ { "name": "mobile", "mediaQuery": "@media (max-width: 767px)", "previewWidth": { "value": 477 } } ], "hidden_modules": ["@hubspot/button", "@hubspot/form"] } ``` To hide all default modules, set `"hide_all_default_modules": true` in `theme.json`. ## Hide modules and sections in a template To hide modules and sections in a specific template, you'll need to add a list of `hiddenModules` and `hiddenSections` in the template annotations. For example: ```html // example template file ``` # Modules Overview @ Understanding modules is key to understanding the HubSpot CMS and its power. Modules are reusable components that can be used in templates or added to pages through drag and drop areas and flexible columns. $ guides & cms/content/modules --- # Modules overview Understanding modules is key to understanding the HubSpot CMS and its power. Modules are reusable components that can be used in templates or added to pages through [drag and drop areas](/guides/cms/content/templates/drag-and-drop/tutorial) and [flexible columns](/reference/cms/hubl/tags/standard-tags#flexible-column). In addition to using the modules that HubSpot provides, developers can create their own modules for everything from testimonials to photo galleries. Modules are created [using the local development tools](/guides/cms/content/modules/quickstart) or [using the Design Manager](https://knowledge.hubspot.com/cos-general/create-and-edit-modules). A module has two parts: 1. A [user interface](#the-user-interface-for-editing) created through a list of fields that users will see when editing a module instance. 2. An HTML+HubL template fragment with associated CSS and JS that defines how HTML will be generated ## An Example To better understand what a module is, let's take a look at a simple "Team Member" module. The module consists of a photo, the team member's name, their title, and a short bio and when part of a CMS web page looks like: ### The User Interface for Editing The developer builds the user interface (UI) for modules using fields. The developer then chooses which fields to use based on the kind of module being built, the data that is needed, and the editing experience. In this case, the module includes: 1. an image field, for a team member photo 2. two text fields, for the team member's name and position 3. and a rich text field, for a short bio. When a content creator edits a module, the UI is constructed based on the fields that the developer has added to the module and how each field is configured. ## Module vs module instance There are two terms frequently used regarding modules. It's important to understand the difference between them. - **Module** - reusable building blocks that can be added to templates and pages. - **Module instance** - the individual rendered modules on the page. They can have separate field values and as a result look different from other module instances that are for the same module. ## Fields.json The fields for a module are defined in JSON as an array of objects. Each field has a name, type, and default value. Other properties are also available depending on the type of field that controls the editing experience. ```json // fields.json [ { "name": "team_member_photo", "label": "Team Member Photo", "required": true, "responsive": true, "resizable": true, "type": "image", "default": { "src": "", "alt": "" } }, { "name": "team_member_name", "label": "Team member name", "required": true, "type": "text", "default": "Joshua Beck" }, { "name": "team_member_position", "label": "Team member position", "required": true, "type": "text", "default": "CEO, Co-Founder" }, { "name": "team_member_bio", "label": "Team member bio", "required": true, "type": "richtext", "default": "

        Joshua has over 20 years of experience in the tech industry. He helped start this company in 2015 with the mission of helping people grow. In his spare time he loves hanging out with his kids, going to the beach, and cooking.

        " } ] ``` To learn more about all of the fields that are available, see [Module and Theme Fields](/reference/cms/fields/module-theme-fields). ### Using module field data to render HTML The values for each field are available in the HTML+HubL fragment for a module via a `module` variable. The data for each field can be accessed via the properties of the module variable. Using the team member module as an example, the team member name can be accessed via `{{ module.team_member_name }}`. ```hubl
        {{ module.team_member_image.alt }}

        {{ module.team_member_name }}

        {{ module.team_member_position }}

        ``` ## Using Modules in Templates Modules are added to templates using the [module](/reference/cms/modules/using-modules-in-templates#basic-module-syntax), [module_block](/reference/cms/modules/using-modules-in-templates#block-syntax), or [dnd_module](/reference/cms/hubl/tags/dnd-areas) tag and specifying the path to the module as a parameter. The default values for fields in a module can also be overridden at the template level through adding parameters to the module tag that corresponds to the field name as shown in the second part of the example below. ```hubl {% module "unique_identifier" path="/modules/team-member.module" %} {# override default values in a module instance #} {% module "unique_identifier" path="/modules/team-member.module", team_member_name="Brian Halligan", team_member_position="CEO" %} ``` Modules can't be nested inside of each other. The majority of the time you would want to do this, it is usually for layout reasons. Sections in [drag and drop areas](/guides/cms/content/templates/drag-and-drop/overview), are often the better course of action. ## Modules are a great tool in the accessibility toolbox Modules are used throughout a website, some times on multiple pages, even multiple times on a page. Because of this [building your module's HTML, CSS, and JS, with accessibility in mind](/guides/cms/content/accessibility) can have a profound effect on how usable your site is to those both with and without disabilities or impairments. ## Modules can make localization easier In a similar sense to accessibility, building modules so that all content in the module is based on fields, makes it possible to localize later. For example you may have a "Featured articles" module. Instead of hard-coding the text "Featured Articles" use a text or rich text field. Then the text can be changed for other languages. To learn more about localization on the CMS see [multi-language](/guides/cms/content/multi-language-content). ## Getting Started To get started, check out our [Getting started with modules](/guides/cms/content/modules/quickstart) tutorial. ## Going Further - [Configuring a module](/reference/cms/modules/configuration) - [Using modules in templates](/reference/cms/modules/using-modules-in-templates) - [Default modules](/reference/cms/modules/default-modules) - [The module editor](/guides/cms/content/modules/overview) # Getting started with Modules @ Create a component that is reusable across your pages and templates called a custom module. You will learn about fields, and using them with HubL. $ guides & cms/content/modules --- # Getting started with modules In this tutorial, you'll use the HubSpot CMS boilerplate theme to learn how to create a module and walk through how you can use it in templates and pages as part of a theme. By the end of the tutorial, you'll have created a customer testimonial module that includes a text field, an image field, and a rich text field. If this is your first experience with CMS Hub development we recommend: If you're looking to develop a module for use in a theme that you want to list on the HubSpot Asset Marketplace, check out the [Asset Marketplace module requirements](/guides/cms/marketplace/module-requirements). ## Before you get started Before getting started with this tutorial: - Set up a [HubSpot CMS Developer Sandbox](https://offers.hubspot.com/free-cms-developer-sandbox). You can use your existing account instead, but the sandbox will allow you to develop without impacting assets in your main account. - Install [Node.js](https://nodejs.org/en/), which enables HubSpot's local development tools. Versions 10 or higher are supported. If you want to jump ahead, you can preview the finished [project files](https://github.com/HubSpot/cms-modules-getting-started/tree/3b9159334f8c6204c7d2d34d8945d71b6ba402ea). Because the CMS boilerplate theme's code may change over time, only the most critical files that developers will need to create or edit during the tutorial are included. These files include: 1. **Testimonial.module:** the folder containing the files that make up the custom module you'll build as part of the tutorial. 2. **homepage.html:** the template that you'll be editing to include the custom module. 3. **images:** the folder containing the profile pictures you'll use in the module. ## Set up your local development environment During this tutorial, you'll be developing the module locally. To interact with HubSpot in your command line interface, you'll need to install the [HubSpot CLI](/guides/cms/setup/getting-started-with-local-development) and configure it to access your account. - In the terminal, run the following command to install the CLI globally. To install the CLI only in your current directory instead, run `npm install @hubspot/cli`. ```shell npm install -g @hubspot/cli ``` - In the directory where you'll be storing your theme files, authenticate your HubSpot account so that you can interact with it via the CLI. ```shell hs account auth ``` - Press **Enter** to open the personal access key page in your browser. - Select the **account** that you want to deploy to, then click **Continue with this account**. You’ll then be redirected to the personal access key page of the account. - Next to _Personal Access Key_, click **Show** to reveal your key. Then click **Copy** to copy it to your clipboard. - Paste the copied key into the terminal, then press **Enter**. - Enter a unique **name** for the account. This name will only be seen and used by you when running commands. Then, press **Enter**.To connect the local development tools to your account: You'll then see a success message confirming that the `~/.hscli/config.yml` file was created. ## Add the CMS boilerplate to your account - Run the command below to create a new theme named `my-theme`. A theme folder named `my-theme` will then be created in your working directory containing the boilerplate assets. ```shell hs create website-theme my-theme ``` - Upload the files to your account. ```shell hs upload ``` | Parameter | Description | | --- | --- | | | The path to your local files, relative to your current working directory. | | | The destination path in your HubSpot account. This can be a new folder. | For example, `hs upload my-theme my-new-theme` would upload the `my-theme` folder from your machine to a `my-new-theme` folder in HubSpot By default, HubSpot will upload to the default account in your `hubspot.config,yml` file. However, you can also specify an account by including a `--account=` flag in the command. For example, `hs upload --portal=mainProd`. Learn more in the [CLI command reference](/guides/cms/tools/local-development-cli). ## Create a module in HubSpot With the CMS boilerplate in your local environment, you'll now create a new module for the theme. For the purposes of this tutorial, you'll create the module in HubSpot, then pull it down into the theme using the CLI. However, you can also create modules from scratch locally using the [hs create module](/guides/cms/tools/local-development-cli#create-new-files) command. - Log in to your HubSpot account, then navigate to the design manager by navigating to **Marketing > Files and Templates > Design Tools**. - In the left sidebar of the design manager, open the **theme folder** that you just uploaded. - In the theme folder, open the **modules folder**. - In the upper left, click **File > New file** to create a new module in this folder. - In the dialog box, click the **dropdown menu**, then select **Module**, then click **Next**. - Select the **Pages** checkbox to make the module available for website and landing pages. - Name the module **Testimonial**, then click **Create**. ### Add fields to the module Next, you'll add three fields to the module: - A text field to store the name of the customer giving the testimonial. - An image field that will store the customer's profile picture. - A rich text field that will store the customer's testimonial. #### Add the text field for customer name - In the right sidebar of the module editor, click the **Add field** dropdown menu, then select **Text**. - In the right sidebar, click the **pencil icon** in the upper right to name the field. For this tutorial, enter the **Customer Name**. You'll then see the HubL variable name change to **customer_name**. - Set the **Default text** to **Sally**. - In the right sidebar, click the **breadcrumb** icon to return to the main module menu. #### Add the image field for customer profile picture - Add another field using the same method, this time selecting the **Image** field type. - Label the image field **Profile Picture**, then set the **HubL variable name** to **profile_pic**. - Under **Default image**, select the profile picture provided for Sally in the **images** folder of the completed [project files.](https://github.com/HubSpot/cms-modules-getting-started/tree/3b9159334f8c6204c7d2d34d8945d71b6ba402ea/images) - Set the **Alt text** to **Sally Profile Picture**. #### Add the rich text field for Sally's testimony - Add another field using the same method, this time selecting the **Rich text** field type. - Label the rich text field **Testimonial**. - Click the **Default rich text** box, then enter "I've had nothing but wonderful experiences with this company." So far, you've added data into several module fields. At this point, though, the module doesn't contain any HTML to render that data. In the module editor, this is reflected by the empty state of the `module.html` section. Next, you'll add HubL to `module.html` to display the field data. ### Add HubL to display field data To display data from the previously created fields, add the following HubL to the `module.html` pane: ```hubl {{module.customer_name}} {{module.profile_pic.alt}} {{module.testimonial}} ``` The above HubL tokens use dot notation to access data from each field. In this case, because we want to pull data from module fields, each token begins with `module`, followed by the field's HubL variable name. You can use dot notation to further access specific properties of a field, which you can see in the `profile_pic` tokens on line 3 above. - With the HubL added to your module, click **Preview** in the upper right to see what the module looks like so far. - Then, in the upper right, click **Publish changes**. Having created and published the module in HubSpot, you'll now use the CLI to pull the module down into your local environment so that you can view its contents and make further changes. ## View and modify the module locally To view the module locally, you'll first need to pull it down to your local theme: - In the terminal, run the following command: `hs fetch `: - `` represents the module's filepath in HubSpot. To get the filepath, you can right-click the module in the left sidebar of the design manager, then select **Copy path**. - `` represents the path to the local directory where your theme lives. If omitted, the command will default to the current working directory. For example, if you're in the working directory already, your fetch command may look similar to the following: ```shell hs fetch my-theme/modules/testimonial.module ``` ### Open the module folder in your local environment In your preferred code editor, navigate to the module folder you’ve just pulled down from your HubSpot account. Within your module folder, you will see five different files: | Parameter | Description | | --- | --- | | `fields.json` | A JSON object that contains your module’s fields. | | `meta.json` | A JSON object that contains meta information about your module. | | `module.css` | The CSS file that styles your module. | | `module.html` | The HTML and HubL for your module. | | `module.js` | The JavaScript for your module. | You can find more detailed information in our documentation about the [module file structure](/guides/cms/content/modules/overview), especially concerning the `fields.json` and `meta.json` files. In this tutorial, we’ll focus on the `fields.json`, `module.css`, and `module.html` files and how they are generated by, downloaded from, and uploaded to the module editor in the Design Manager. ### View the module’s fields.json file Open the module's `fields.json` file. Aside from some of the `id` numbers, the `src` attribute of the image field, and potentially the order of fields, the file should contain a JSON object similar to the following: ```json //fields.json [ { "label": "Customer Name", "name": "customer_name", "id": "2a025cd5-7357-007f-ae2f-25ace762588e", "type": "text", "required": true, "locked": false, "validation_regex": "", "allow_new_line": false, "show_emoji_picker": false, "default": "Sally" }, { "label": "Profile Picture", "name": "profile_pic", "id": "7877fb84-eb8a-d2c7-d939-77e6e9557d8f", "type": "image", "required": false, "locked": false, "responsive": true, "resizable": true, "default": { "src": "https://cdn2.hubspotqa.net/hubfs/101140939/profile-pic-sally.svg", "alt": "Sally Profile Picture", "width": 100, "height": 100, "max_width": 100, "max_height": 100 } }, { "label": "Testimonial", "name": "testimonial", "id": "c5524ece-1ab5-f92d-a5de-e2bf53199dfa", "type": "richtext", "required": false, "locked": false, "default": "

        I’ve had nothing but wonderful experiences with this company.

        " } ] ``` The values for the following fields will match the values you added in step 3: - `name`: the name of the field. - `label`: the field's label. - `default`: the field's default value. ### View the module's module.html file The `module.html` file should contain the HubL and HTML that you wrote in the HubL + HTML module editor previously. To make this code more interesting and ready for CSS styling, copy and paste the following code into the file: ```hubl ``` Writing your HTML as above uses the [BEM class structure](https://css-tricks.com/bem-101/) in accordance with the HubSpot CMS boilerplate theme's [style guide](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/STYLEGUIDE.md#css-code-formatting). ### View the module’s module.css file The `module.css` file should be empty at this point. To add styling, copy and paste the following code into the file: ```css .testimonial { text-align: center; } .testimonial__header { font-weight: bold; } .testimonial__picture { display: block; margin: auto; } ``` After adding the code, save the file. ## Push local changes to your HubSpot account After saving your local changes, push them back to your HubSpot account. - Navigate to your terminal and ensure that you're in the correct directory. - Run the watch command to push changes to HubSpot on save: `hs watch `. ```shell hs watch my-theme my-theme ``` ## Preview your local changes in HubSpot - In your HubSpot account, navigate to **Marketing** > **Files and Templates** > **Design Tools**. - In the left sidebar, navigate to the **theme** you've created, then open the **module** folder and select the **Testimonial** module. - With the module open, you should now see your changes in the `module.html` and `module.css` panes. - In the upper right, click **Preview**. A new tab will open to display the module preview. To recap this tutorial so far, you have: - created a module in HubSpot. - pulled that module down to your local environment. - made changes to the HubL + HTML and CSS using your local development tools. In the next part of this tutorial, learn how to use the module in a template. ## Add the module to a template For this part of the tutorial, you'll be working mostly within the `modules` and `templates` folders within your local theme files. By their most basic definition, modules are editable areas of HubSpot templates and pages. You can insert modules into templates in HubSpot by using the design manager, but here you'll be using HubL to add the module to the template in your local environment. - In your code editor, open the `templates` folder, then open the `home.html` file. - In the `home.html` file, navigate to the final `dnd_section`, which starts around line 28. You'll be adding your new module to this section. - Within this `dnd_section` and above the other `dnd_modules`, copy and paste the following HubL module tag: ```hubl {% dnd_module path= “../modules/Testimonial.module”, offset=0, width=4 %} {% end_dnd_module %} ``` This HubL tag references your new module by its relative file path. To get the module to fit evenly with the other two modules in the `dnd_section`, it also assigns a module `width` and `offset`: - HubSpot's CMS uses a [12-column grid system](/guides/cms/content/templates/drag-and-drop/overview#columns), so to space this module evenly with the other two, you'll need to update each module in the `dnd_section` to have a width of `4`. - Then, the first `dnd_module` in the group (`testimonial`) will have an offset of `0` to position it first. - The second `dnd_module` (`linked_image`) will have an offset of `4` to position it second. - The third `dnd_module` (`rich_text`) will have an offset of `8` to position it third. After setting the `offset` and `width` of each `dnd_module`, your code will look similar to the following: ```hubl {% dnd_section background_color="#f8fafc", vertical_alignment="MIDDLE" %} {% dnd_module path= “../modules/Testimonial.module”, offset=0, width=4 %} {% end_dnd_module %} {% dnd_module path="@hubspot/linked_image", img={ "alt": "Stock placeholder image with grayscale geometrical mountain landscape", "loading": "lazy", "max_height": 451, "max_width": 605, "size_type": "auto_custom_max", "src": get_asset_url("../images/grayscale-mountain.png") }, offset=4, width=4 %} {% end_dnd_module %} {% dnd_module path="@hubspot/rich_text", html="

        Provide more details here.

        Use text and images to tell your company’s story. Explain what makes your product or service extraordinary.

        " offset=8, width=4 %} {% end_dnd_module %} {% end_dnd_section %} ``` When adding a module to a Drag and Drop area, the module tag does not require a unique name. However, when adding a module to a HTML file outside of Drag and Drop areas, you must assign the module a unique name. You would also use slightly different syntax, such as: `{% module "testimonial_one" path="../modules/Testimonial.module" %}` Learn more about [using modules in templates](/reference/cms/modules/using-modules-in-templates). ## Preview your changes in HubSpot - If you haven’t kept the `watch` command running to track your saved work automatically, run `hs watch `. Ensure that this command keeps running as you complete the next steps. - In your HubSpot account, open the design manager (**Marketing** > **Files and Templates** > **Design Tools**). - In the left sidebar of the design manager, select the **home.html** file. - In the upper right, click **Preview**, then select **Live preview with display options** to open the template preview in a new window. - In the new tab, view the template preview, which should contain your newly added testimonial module. ## Customize the module in the template locally To make the homepage template more interesting: - Navigate back to your code editor, then open the `home.html` file. - Add the following parameters to the testimonial module tag: ```hubl {% dnd_module path='../modules/Testimonial.module', customer_name = "Mary", profile_pic = { src: "{{ get_asset_url('../images/profile-pic-mary.svg') }}", alt: "Mary Profile Picture" }, testimonial = "Wow, what a product! I can't wait to recommend this to all my family and friends!", offset=0, width=4 %} {% end_dnd_module %} ``` The above parameters override the default values that you originally assigned to the three fields. Each parameter uses the HubL variable name that you assigned to each field previously: - `customer_name`**:** this parameter passes the name `Mary` to the customer name field, overriding the original value of `Sally`. - `profile_pic`**:** this parameter is an object that contains two properties: - The `src` property uses the `get_asset_url` HubL function to retrieve the URL for the new profile picture. Note that the image's file path is relative to your working directory. You'll first need to add this image to the `images` folder in your local theme files. You can find the image in the `images` folder of the completed [project files.](https://github.com/HubSpot/cms-modules-getting-started/tree/3b9159334f8c6204c7d2d34d8945d71b6ba402ea/images) - The `alt` property assigns the image's alt text. - `testimonial`**:** this parameter passes new text into the testimonial field. Alternatively, for the rich text field you could use HubL block syntax to write a large block of HTML or text: ```hubl {% dnd_module path='../modules/Testimonial.module', customer_name = "Mary", profile_pic = { src: "{{ get_asset_url('../images/profile-pic-mary.svg') }}", alt: "Mary Profile Picture" }, offset=0, width=4 %} {% module_attribute "testimonial" %} Wow, what a product! I can't wait to recommend this to all my family and friends! {% end_module_attribute %} {% end_dnd_module %} ``` Learn more about [module block syntax](/reference/cms/modules/using-modules-in-templates). If you’ve kept the `watch` command running in your terminal, saving your changes will send them to HubSpot. You can then navigate back to the design manager to preview the template. ## Add two more testimonial modules to the template In this step, you'll add two more testimonial modules to the template using the same steps as before: - Navigate to your code editor, then open the `home.html` file. - Under the testimonial module you previous added, add another instance of the module by copying and pasting that module's code. In the new testimonial module, specify the following details using the same steps as above: - The customer's name is `Ollie`. - Ollie's testimonial reads: `I can't believe that I ever conducted business without the use of this product!` - For Ollie's picture, add the relative file path for the file within the `images` folder. You can get the image itself from the finished [project files](https://github.com/HubSpot/cms-modules-getting-started/tree/3b9159334f8c6204c7d2d34d8945d71b6ba402ea). Then give Ollie's image the `alt` attribute of `Ollie Profile Picture`. - To maintain spacing, set `offset` to `4` and `width` to `4`. - Underneath the second testimonial module, add a third with the following details: - The customer's name is `Erin`. - Erin's testimony reads: `My business has nearly tripled since I began to use this product! Don't wait, start now!` - For Erin's picture, add the relative file path for the file within the `images` folder. Then give Erin's image the `alt` attribute of `Erin Profile Picture`. - To maintain spacing, set `offset` to `8` and `width` to `4`. - Save your changes. If you’ve kept the `watch` command running in your terminal, you can navigate back to the Design Manager to preview your changes to the template. The template preview should now contain all three testimonial modules. As a final step, you'll prepare the theme for use in a live page by modifying the `theme.json` file. ## Modify the theme name If you'd like to modify the default name of the theme, you can do so by configuring the `name` and `label` fields in the `theme.json` file. ```json // example theme.json { "name": "my_first_theme", "label": "My first theme" } ``` After adding that code, save your changes so that the `watch` command sends your changes to HubSpot. ## Next steps Now that you've created a custom module, added it to the `home.html` template, and optionally customized the theme name, you're ready to create a live page from the theme. If you'd like to follow a similar tutorial that focuses on themes, check out the [Getting started with themes tutorial](/guides/cms/content/themes/getting-started) next. Otherwise, you can learn more about [creating and customizing pages](https://knowledge.hubspot.com/website-and-landing-pages/create-and-customize-pages) in HubSpot's Knowledge Base. Or, if you'd like to learn more about modules, check out the following module guides: - [Configuring a module](/reference/cms/modules/configuration) - [Using modules in templates](/reference/cms/modules/using-modules-in-templates) - [Default modules](/reference/cms/modules/default-modules) ## Other tutorials - [Getting started with themes](/guides/cms/content/themes/getting-started) - [Getting started with serverless functions](/guides/cms/content/data-driven-content/serverless-functions/getting-started-with-serverless-functions) - [Creating an efficient developer workflow](/guides/cms/setup/creating-an-efficient-development-workflow) - [Getting started with accessibility](/guides/cms/content/accessibility) - [How to use JavaScript frameworks on CMS Hub](/guides/cms/setup/js-frameworks) - [How to use web components in custom modules](https://developers.hubspot.com/blog/use-web-components-in-hubspot-cms-development) - [How to build a code block web component based module](https://developers.hubspot.com/blog/how-to-build-a-code-block-web-component) # Multi-language Content @ Developers building websites on HubSpot can easily support a multi-language website. Some development effort may be required based on your unique business. $ guides & cms/content --- # Multi-language Content Any company which does business across regions or with a customer-base that speaks multiple languages needs to be able to connect with their audience in the audience’s language. With HubSpot’s CMS, users are able to [create multi-language variations](https://knowledge.hubspot.com/cos-general/how-to-manage-multi-language-content-with-hubspots-cos) of their content that enable the end-user to view the content in the language with which they are most comfortable. HubSpot sets a number of facets of a multi-language website up for you automatically, but there is also a number steps developers should take to ensure their website is multi-language ready. ## HubSpot's default multi-language functionalities Whenever a multi-language variant is created for a page in HubSpot, we will automatically: - Create a new entry in the XML sitemap indicating the translated page’s name and URL. - Specify the language of the content within the page `` for templates built using drag and drop functionality. - Identify other pages within the multi-language content group following the appropriate standardized format, which marks the other pages as alternates to avoid duplicate content errors and also identifies the [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) code associated with the language translation(s): `` - Re-write links on language pages to navigate to intra-language versions of the linked page to help visitors stay in-language, and prevent the need for you to have to update every single link on every page translation. For a given element, you can disable this rewrite by adding the class "hs-skip-lang-url-rewrite" to the element. ### What HubSpot does not do With the HubSpot CMS, HubSpot does not automatically: - translate the content of the page for you. - direct users to a multi-language variation based upon their GeoIP. - include a language-switcher module within your header or website. - specify the language of a page for coded files. - set the content directional attribute for translations using a language that reads right-to-left as opposed to left-to-right for coded files. ## Set language variables Because coded files do not automatically include language declarations or content language directional attributes, this will need to be manually set up for coded templates. Language variables can be set in HTML or populated via HubL, such as in the [CMS Boilerplate template](https://github.com/HubSpot/cms-theme-boilerplate/blob/master/src/templates/layouts/base.html#L2). Setting these properties using HubL will allow this data to populate dynamically within a page’s HTML based upon the language set for the page within the HubSpot CMS. ```hubl ``` ## Use page-editable modules In order to ensure that content can be localized across each instance of a template’s use, leverage [custom modules](/guides/cms/content/modules/overview) in place of hard-coded HTML content whenever possible. Creating modules that can be edited at the page level will enable content creators to set the specific content that should appear on each page without having the adjust template code. It also allows for unique content to be used across pages which share a template. ## Include field translations in custom modules and themes To support your global team, you can publish translations of modules you've created in HubSpot. After you've translated the content in the module and published in the languages of your team members, users will see the field labels for that module in their account default language. The content in a translated module will not be translated automatically; you will need to do this. You can create translations of your module in any supported language. You can set translations using the [local development tools](/guides/cms/content/modules/overview#local-module-file-structure) or through the Design Manager. ### Local development To set translations using local development tooling, every module folder and every theme folder can contain a `_locales` folder, with language locale subfolders, each with a `messages.json` file, containing module field translations. ### Design Manager To set field translations through the [Design Manager](/guides/cms/tools/design-manager), when viewing the module, navigate to the “Add Translations” option on the right hand side of the screen. Select the languages in which your team works from the dropdown menu. From there, you are able to select each language and specify the labeling conventions for each field in each language. Theme field translations do not have an interface in the design manager and need to be edited through the `.json` files. ## Translate system pages To set up translations for system pages, including password reset and email subscription pages, you can customize module and HubL tag fields with your translated content. Learn more about the available fields for [modules](/reference/cms/modules/default-modules) and [system page HubL tags](/reference/cms/hubl/tags/standard-tags#system-page-tags). ## Include a language switcher To enable end-users to toggle between available translations, it is advised that a language-switcher module be added to your website. An [example of of how to implement a language switcher](https://github.com/HubSpot/cms-theme-boilerplate/blob/09ad0b2c3bbc8400e64c5617268cb0504392e8e5/src/templates/partials/header.html#L26-L47) can be found in the [CMS Theme Boilerplate](/guides/cms/content/themes/hubspot-cms-boilerplate). ```hubl {# Header navigation row one #} {% endif %} {# End header navigation row one #} ``` ## Implementing search on multi-language websites [Content Search](/guides/cms/content/content-search) supports querying for content across the various languages on your website. The language filter parameter can be used when hitting the [/contentsearch/v2/search](/reference/api/cms/site-search/v2#search-your-site) to return only specified languages, which allows you to create search experiences for each language on your website, or let visitors search across multiple languages on your website. ## Use global partials and modules Use module fields to make text in headers, footers, and sidebars editable. Place these modules into global partials. Not only will content creators benefit from ease of editing, global partials support [configuring their settings for each language](https://knowledge.hubspot.com/cos-general/how-to-manage-multi-language-content-with-hubspots-cos#edit-global-content-in-a-multi-language-page). # CMS building blocks overview @ Developers can use a series of building blocks to create websites on the HubSpot CMS. $ guides & cms/content --- # CMS building blocks overview Developers utilize a series of building blocks to create websites on HubSpot's CMS software. The building blocks can be used to create a design system for content creators to work within, with varying degrees of flexibility and guardrails depending on your business needs. ## Themes A [Theme](/guides/cms/content/themes/overview) is a portable and contained package of developer created assets designed to work together to enable a marketer-friendly content editing experience. A theme is a top-level building block that define the look and feel of a website and create a marketer-friendly content-editing experience. A theme will include templates, modules, CSS files, JavaScript files, images, and more. Using themes, you can create a set of fields that content creators use to gain global stylistic control over a website without having to edit CSS. You can specify in CSS where these controls are applied, arrange controls to inherit from others, and manage how they are presented and organized to marketers in the Theme Editor. Content creators use the [theme editor](/guides/cms/content/themes/overview#theme-fields) to modify theme fields, preview those changes against existing templates within a theme, and publish their changes. These theme fields can be set either globally across a site or overridden at a page level. ## Templates [Templates](/guides/cms/content/templates/overview) define the base markup and style of a set of pages that use a template. They can contain HubL, HTML, and links to JavaScript and CSS files, and can be restricted to use with specific content types. You have full flexibility in the markup and style you can include in a template, but you're encouraged to adhere to a few best practices and use some key features to ensure marketers can edit pages easily. Some best practices include: - Building [templates](/guides/cms/content/templates/overview) as part of a theme and using theme-level CSS, including theme fields, to do the majority of styling within a template. This'll make it easy for content creators to make global and local style changes in a consistent way without needing to get into editing CSS. - Using [modules](/guides/cms/content/modules/overview) for the majority of components on your page, which allows them to be rearranged and reused across a website. Learn more about modules below. - Using [drag-and-drop areas](/guides/cms/content/templates/types/drag-and-drop-templates) where possible for core page content, especially on internal pages. Drag-and-drop areas let you set a default layout for the modules that comprise a page but give marketers flexibility to edit layout and style independently. - Using [global partials](/guides/cms/content/templates/types/html-hubl-templates#global-partials) to contain shared content like headers and footers that you want to look consistent across a website. ## Modules [Modules](/guides/cms/content/modules/overview) are reusable components that you can place on templates and pages in the HubSpot CMS. They include controls for marketers and can be built using either HubL/HTML or React. The controls for a module are defined in fields, so building a great module means considering both the resulting appearance on a page, as well as the editing experience for content editors. HubSpot provides a set of [default modules](/reference/cms/modules/default-modules) like headers, rich text, images, buttons, and CTAs that you'll use as fundamental components. You'll also likely want to build out elements that can have more interesting layouts that fit into your theme and templates. Some common examples of modules you might want to build are accordions, sliders, and tabbers. You can think of a module as an object and modules on pages as instances of that object, meaning updates to the [HubL](/reference/cms/hubl/overview), CSS, JavaScript, or [React](/guides/cms/react/modules) code of a module are applied across all instances of that module inside pages or templates in a given portal. This means you can implement a design solution once and use it across pages and portals, and when you need to update it, those changes can be applied across pages without having to edit multiple pages or multiple templates. ### HubL Modules Traditional modules contain HubL/HTML markup, CSS, and JavaScript that together build reusable and editable components of a website. ### React Modules Alternatively, you can also build [React modules](/guides/cms/react/modules) to gain the benefit of a component based approach to building modules. React modules function similarly to traditional modules, as in they have fields, can be edited in the content editor, and can be used in drag and drop areas. The key differences are: - Modern JavaScript features and React patterns can be used - Fields can be defined using either JSX or JSON - React modules support powerful features like [islands](/reference/cms/react/islands) for client-side interactivity CMS React modules must follow a specific directory structure to adhere to the [Project system](/guides/cms/react/project-structure). Whether you choose to build modules with HubL or React depends on your team's expertise and project requirements. Both approaches are fully supported and can coexist within the same website. Modules may also be included in themes, which allows you to use theme fields to manipulate the look of modules and ensure they're prominently displayed in the page editor so content creators can have easy access to modules that'll look great with the designs you've built. ## Module and Theme Fields [Fields](/reference/cms/fields/module-theme-fields) are the controls that content creators use to adjust the parameters passed into your themes and modules. Fields are typed, including simple types like boolean, text, URL, choice, and file, but also have more complex fields like font with styling as well as HubSpot-specific fields like links to other pieces of content or forms in the HubSpot system. Fields can also be placed inside repeaters that'll pass an array to the module—an example of this could be an image carousel where you want a set of images with associated \`alt\` text passed in. Rather than creating a number of image and text fields, you can create one of each and put them in a repeating group. Fields of a module are specified either inside the design manager or with [this syntax in a fields.json file.](/guides/cms/content/modules/overview#the-user-interface-for-editing) Fields for a theme must be specified in the `fields.json` file at the root of the theme. ## Global Content [Global content](/guides/cms/content/global-content) allows markup and content to be globally shared across a website. Global content makes managing elements like headers, footers, sidebars or other global elements of a website simple. ## Menus and Navigation [Menus](/guides/cms/content/menus-and-navigation) allow you to easily build a navigation hierarchy and information architecture for navigating your website. ## Forms [Forms](/guides/cms/content/forms) allow you to capture multiple types of information from users (such as address information, email addresses, and feedback) which you can then use throughout the HubSpot Ecosystem in areas such as smart content, workflows, content personalization, and more in your day-to-day operations. ## Website Settings [Website settings](/guides/cms/content/website-settings) is a single place where various global and system-level settings can be configured for your website, such as your logo, favicon, system templates and more. # Lazy loading assets for performance @ Lazy loading assets is the act of deferring the loading of the assets until the time that they are actually needed. $ guides & cms/content/performance --- # Lazy loading assets for performance Lazy loading assets allows you to defer the loading of the assets until the time when they are actually needed. On the web, this often means downloading the designated content only once the user has gotten sufficiently close to where in the HTML document the asset displays. This technique is one of many suggested for [optimizing page performance](/guides/cms/content/performance/speed). ## Lazy loading images Lazy loading options are available for the [image and logo fields](/reference/cms/fields/module-theme-fields#image) in custom modules for use in HubL tags and also available in the [default image module.](https://knowledge.hubspot.com/cos-general/use-default-modules-in-your-template#image) When building custom modules, you have the option to enable [browser built-in lazy loading](https://web.dev/browser-level-image-lazy-loading/) on image fields. When enabled, you can choose whether to show or hide controls to the content editor enabling them to change the loading behavior in the page editor. ### Browser Compatibility Lazy loading of images via the `loading` attribute is supported by most of the popular Chromium-powered browsers (Chrome, Edge, Opera) and Firefox. To learn more about what browsers are supported you can visit [caniuse.com](https://caniuse.com/#feat=loading-lazy-attr). Browsers that do not support the `loading` attribute will simply ignore it without side-effects. ### Add lazy loading to Image fields using the CLI To enable lazy loading of images while building with the CMS CLI, add the `show_loading` and `loading` keys to the [image](/reference/cms/fields/module-theme-fields#image) or [logo](/reference/cms/fields/module-theme-fields#logo) field in the module's `fields.json` file. ```json // fields.json file { "id": "357bacfa-2bb8-e996-4589-f55e10d4f1d4", "name": "image_field", "label": "Image", "required": false, "locked": false, "responsive": true, "resizable": true, "show_loading": false, "type": "image", "default": { "size_type": "auto", "src": "", "alt": null, "loading": "disabled" } } ``` | Parameter | Type | Description | Default | | --- | --- | --- | --- | | `show_loading` | Boolean | Controls showing/hiding lazy load controls to the content editor. | `False` | | `loading` | String | Determines whether to use lazy loading. Options include: `"disabled"` or `"lazy"` | `"disabled"` | You can then reference these variables in your `module.html` file using the following syntax: ```hubl {% set loadingAttr = module.image_field.loading != 'disabled' ? 'loading="{{ module.image_field.loading }}"' : '' %} {{ module.image_field.alt }} ``` ### Add lazy loading to image and logo fields in HubSpot To enable lazy loading, add an image or logo field to your custom module, then navigate to the **Content options** section in the [Inspector pane](https://knowledge.hubspot.com/cos-general/a-quick-tour-of-the-design-manager#inspector). Then use the **Image loading** and **Available loading options** dropdown menus to configure the image loading behavior. #### Image loading The **Image loading** option will set the value of the `loading` attribute in the browser. Options for this include "Default" (default option) which is the default browser loading behavior for the asset. When enabling lazy loading, the image will load once the image reaches a certain distance from the viewport as defined in the [distance-from-viewport threshold](https://web.dev/browser-level-image-lazy-loading/#distance-from-viewport-thresholds). #### Available loading options The **Available loading options** will determine if content editors will be able to see and set the **Image loading** option while inside of the page, global, and theme content editor panes. Options for this include _Do Not Show Controls_ (default) or _Show all controls_. Below is a sample of what the page editor would look like with _Show all controls_ selected: You can then reference these variables in the `module.html` file using the following syntax: ```hubl {% set loadingAttr = module.image_field.loading != 'disabled' ? 'loading="{{ module.image_field.loading }}"' : '' %} {{ module.image_field.alt }} ``` # CDN, security, and performance overview @ The HubSpot CMS offers security, reliability, performance, and maintenance tools plus a global and fast CDN $ guides & cms/content/performance --- # CDN, security, and performance overview By default, HubSpot's CMS includes [security](https://legal.hubspot.com/security), [reliability](https://www.hubspot.com/reliability), and performance solutions so that you can focus on writing code and creating delightful user experiences. Below, learn more about HubSpot's content delivery network, security features, and performance enhacements. In addition to the features listed in this article, you can also [further optimize performance on HubSpot CMS.](/guides/cms/content/performance/speed) ## Content Delivery Network (CDN) HubSpot's CMS content is powered by a globally distributed Content Delivery Network that ensures faster page load times regardless of where your visitors are. No configuration, setup, or additional accounts are required to take advantage of the CDN for hosting media or pages, as HubSpot automatically handles distribution and cache validation. The CDN also features a web application firewall and built-in security to provide peace of mind against online attacks. HubSpot's [content prerendering service](/guides/cms/content/performance/prerendering) also takes advantage of the CDN by storing local copies of prerendered pages across the globe. This enables users worldwide to load your pages faster based on their location. When a page or any dependency of a page, such as a template or module, changes, HubSpot automatically expire the server caches for that page. ## Secure Socket Layer (SSL) SSL is included and automatically provisioned for free on all [connected domains.](https://knowledge.hubspot.com/domains-and-urls/connect-a-domain-to-hubspot) Each domain is provisioned its own SAN certificate, with configurable options such as disabling support for select versions of TLS, redirecting requests made over HTTP to HTTPS, and serving the HSTS header so future requests are made over HTTPS. Per request, custom SSL certificates can be hosted with the [custom SSL add-on](https://legal.hubspot.com/hubspot-product-and-services-catalog). ## HTTP/2 All SSL traffic on HubSpot hosted websites is served using [HTTP/2](https://http2.github.io/). HTTP/2 is a replacement for how HTTP is expressed “on the wire.” It is not a ground-up rewrite of the protocol; HTTP methods, status codes, and semantics are the same, and it's possible to use the same APIs as HTTP/1.x (possibly with some small additions) to represent the protocol. The focus of the protocol is on performance; specifically, end-user perceived latency, network and server resource usage. One major goal is to allow the use of a single connection from browsers to a website. ## IPv6 All HubSpot hosted websites include IPv6 addresses so they can be natively accessed over [IPv6](https://www.google.com/intl/en/ipv6/index.html). IPv6 is the most recent version of the Internet Protocol and expands the number of available addresses to a virtually limitless amount–340 trillion trillion trillion addresses. Because the internet is running out of IPv4 addresses, transition to IPv6 enables the internet to continue to grow and enables new, innovative services to be developed because more devices can connect to it. ## JavaScript and CSS minification When writing JavaScript and CSS for HubSpot pages, you can expect the following minification behavior: - 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](/guides/cms/setup/getting-started-with-local-development). Because HubSpot automatically minifies JavaScript and CSS in the design manager, you should not add minified code directly to the design manager. To include minified code, you should instead upload the file to the [file manager](/guides/cms/storage/file-manager), then attach the file through the design manager. To link a minified file to a module locally, you can [update the module's meta.json to include css_assets or js_assets](/reference/cms/modules/configuration#adding-css-and-javascript-dependencies). - HubSpot does not minify JavaScript or CSS files that are uploaded to the file manager or referenced via external links. You should ensure these files are minified before upload. - Every time you update a JavaScript or CSS file in the design manager or through local upload, HubSpot will automatically re-minify it. This can result in a short delay before you see the `.min.js` version of your file being served on live pages. During that period, HubSpot will serve the unminified version to ensure site visitors still get the latest version of your files. - Syntax errors can prevent HubSpot from being able to minify a file. In addition to minification, you can [use HubL includes to combine multiple CSS files into one file](/reference/cms/hubl/overview#including-files-in-files) to further increase performance. ## Domain rewriting Each additional domain used on your website incurs an additional DNS lookup and connection. The fewer domains you use, the faster your website will load. HTTP/2 supports loading multiple files simultaneously over the same connection, so the old guidelines for “sharding” your assets amongst multiple domains no longer apply. The URLs of assets referenced in CMS pages such as developer file system files, CSS, JS, and images are automatically rewritten to match the domain of the current page when possible. So if you reference an image at [http://cdn2.hubspot.net/hubfs/53/HubSpot_Logos/HSLogo_gray.svg](https://cdn2.hubspot.net/hubfs/53/HubSpot_Logos/HSLogo_gray.svg) on a page served on [www.hubspot.com](https://www.hubspot.com/), the URL will automatically update to [https://www.hubspot.com/hubfs/HubSpot_Logos/HSLogo_gray.svg](https://www.hubspot.com/hubfs/HubSpot_Logos/HSLogo_gray.svg). ## Text compression Text-based files such as HTML, CSS, and JS are all compressed using [brotli](https://github.com/google/brotli) before they are served to browsers. Compression with brotli is even more significant than GZIP. If the client does not indicate that Brotli compression is supported, then gzip compression will be applied. While minification speeds up the parse time of CSS and JS files in the browser, compression gets those files to the browser faster. ## Image compression, optimization, and automatic resizing When you upload an image to the file manager, images are automatically optimized. Specifically, JPEGs and PNGs are stripped of their metadata (such as EXIF data). All images except for GIF files are recompressed to be visually lossless. Additionally, HubSpot may serve images in a different encoding if it can be represented as a smaller PNG than a JPEG. Images are re-saved at the web-standard 72dpi regardless of their original resolution. For example, if you upload a file at 300dpi, originally created for print, it will be converted to 72dpi. HubSpot automatically serves images in a WebP format for browsers that support it when the WebP format is a smaller file size than the original image. This conversion happens server-side and does not change the file extension in the URL. To ensure that links to the image work for all visitors regardless of WebP support, image URLs maintain their original format, meaning an image uploaded as a `.jpg`, will still show as a `.jpg` in the URL but will be served as a WebP. HubSpot-hosted images in CMS content are also automatically resized by appending height and/or width query parameters to the `src` URL for any images that have a height or width attribute. If a page is requested before an image is resized, the non-resized image will be served for that request. Browsers are given multiple options for the image resolution to load, ensuring that images look crisp on standard and high-resolution displays. For individual jpg files if the image url has the `quality=high` query parameter, the image will not be compressed. In addition, you can provide HubSpot with additional context to images to further control image resizing. This is done through the [resize_image_url()](/reference/cms/hubl/functions#resize-image-url) function, which prevents content creators from displaying oversized images on pages and emails. The function can also be useful when an image's size is not dictated by height and width attributes within the HTML, such as a background image. ## Accelerated Mobile Pages (AMP) AMP, or Accelerated Mobile Pages, is a mobile-specific page format that loads content nearly instantaneously, and can be enabled for HubSpot blog posts in your account settings. Learn more about [using Accelerated Mobile Pages (AMP) in HubSpot](https://knowledge.hubspot.com/blog/use-accelerated-mobile-pages-in-your-blog). # Prerendering @ Prerendering is where the HubSpot CMS makes a static version of your website page, landing page, blog post or knowledge base article. $ guides & cms/content/performance --- # Prerendering To improve page load speed and security, HubSpot automatically creates static versions of pages, blog posts, and knowledge base articles when possible. This means that, rather than assembling the page's data and layout for each request, HubSpot will render the page ahead of time and push it to the [CDN](/guides/cms/content/performance/overview). Local copies of prerendered pages are stored around the globe so that users across the world can access pages faster based on their location. This is separate from the concept of caching. All prerendered pages are cacheable on HubSpot's CDN. Prerendering is the process by which the HubSpot CMS generates the static version of a page. Even if the CDN doesn't have a page cached, requests will be faster because the page is already in its final rendered form. Not all pages are eligible for prerendering, depending on the type of content it serves. When a page is not eligible, HubSpot will continue to serve that page dynamically. Below, learn more about prerendering. ## Overview Updating data that affects your page will automatically trigger a render. For example, publishing an edit to a page's template will re-render it. Changes to other shared data, such as CMS settings, may cause pages pages to be re-rendered. Generally, the prerendering process takes seconds to complete. If a large change is made, such as updating a module that’s included in many templates and pages, it may be minutes before all pages are updated. CSS, JavaScript and images files have always been statically generated on HubSpot, so they have effectively always been prerendered. HubL in JS or CSS files is evaluated only once when the asset is published. ## Check if a page is prerendered The easiest way to tell if a page is prerendered is by looking at the HTTP response headers of the request. If it includes a `X-HS-Prerendered` header, the page is prerendered. The value of the header is the last time the page was prerendered. ## Incompatible features The use of the features below prevents the same response from being served to every user, so they prevent the serving of a static prerendered page. Pages using some of these features may be served using partial prerendering. ### Incompatible HubL variables - `account` - `company` - `contact` - `local_dt` - `owner` - `query` - `request_contact` - `request.cookies` - `request.full_url` - `request.geoip*` (deprecated) - `request.headers` - `request.path_and_query` - `request.query` - `request.query_dict` - `request.referrer` - `request.remote_ip` - `year` The HubL request variables generally have JavaScript based alternatives you can use. Enabling you to avoid using those variables. ### Incompatible HubL functions - `personalization_token` - `today` - `unixtimestamp` ### Incompatible functionalities - Password-protected pages - Pages that use [adaptive tests](https://knowledge.hubspot.com/website-pages/create-an-adaptive-test-for-a-page) Pages that include [smart content](https://knowledge.hubspot.com/website-pages/create-and-manage-smart-content-rules) are supported via [partial pre-rendering](#what-is-partial-prerendering-). ## How can I make my pages compatible with prerendering? Load the page with a `?hsDebugOnly=true` query param, and there will be additional output that can help point to specific features that prevent pre-rendering. This additional output includes: - A list of specific features that may be causing issues - Specific files and line numbers of templates that use uncacheable variables When possible, move all dynamic rendering of content to JavaScript executed in the user’s browser. You may also use Serverless Functions to pull in dynamic data. ## What is partial prerendering? Partial prerendering allows HubSpot to serve “mostly” prerendered pages. For example, a page may be entirely static except for a contact’s name displayed on the page. The page can be prerendered except for that contact name. Just before returning the page to the user, HubSpot will perform a render of just those dynamic values. Pages that use partial prerendering can't be cached at the CDN or the browser. However, partially prerendered pages are faster to deliver than pages that can't be partially prerendered. Partially prerendered pages also have the ability to fall back to an unpersonalized state in case of an outage or an attack. While partial prerendering may help the speed and reliability of your site, removing the HubL features that make pages non-prerenderable will have a much greater positive effect on your overall page performance. ## How can I tell if my page uses partial prerendering? Load the page with a `?hsPrcDebug=true` query param, and there will be additional output about the pre-rendered content for that page. If the page is prerendered, the `X-HS-Prerendered` header will be present and contain `partial` before the time the page was partially prerendered. **The features below _are currently supported_ with partial prerendering**. The page will be partially pre-rendered and expressions using these will be instead be evaluated at serve-time. ### HubL variables - `account` - `company` - `contact` - `local_dt` - `owner` - `query` - `request` - `request_contact` - `year` ### HubL filters - `|random` - `|shuffle` ## How does prerendering affect CSS combining? With page prerendering, HubSpot no longer automatically combines CSS at the page-level. There’s several reasons for this: 1. CSS combining may optimize a single page view, but it hurts subsequent page views because all the CSS has to be re-downloaded for each page. 2. HTTP/2 (all HubSpot sites served over SSL use HTTP/2) [solves a lot of the problems](https://developers.google.com/web/fundamentals/performance/http2#request_and_response_multiplexing) with many CSS files, so combining CSS doesn’t provide as much benefit as it used to. 3. To verify that the combined CSS doesn’t break anything on a page, HubSpot runs an automated visual check on the page with the combined CSS. The visual checker isn’t 100% effective (consider elements that only appear or scroll, JavaScript animations, etc.) so sometimes it will miss problems. 4. Prerendering pages is much more important than CSS combining for site availability and speed. Since it’s required to verify combined CSS after every renderer, it’s not feasible to also combine and visually verify CSS on every change. This is referring to functionality that historically combined module and page styles into one monolithic file per page. This has no effect on CSS files that are combined using the `include` tag. ## Without page-level automatic CSS combining, how can I optimize delivery of CSS? - Take advantage of modules. If your CSS is unique to the module, put it in the module's CSS or a CSS file you are tying to the module with require_css. This means the small css file specific to just this module will only load on pages where that module is used. For subsequent page loads, the CSS for modules already seen by the user will be cached and they only need to download the CSS for assets they haven't yet seen. - If you share styles like layout or utility classes etc in multiple modules or modules are visually similar so they share a lot of CSS, consider creating a CSS file that you require_css in each of the modules that need it. This enables you to avoid repeating CSS in each module, while also getting the caching benefits. - Another way to consider this is also modules that are used everywhere on your site - for example if your header and footer modules don't change per page, consider using this approach for their CSS styles. ## Related resources - [Optimizing CMS Hub site performance](/guides/cms/content/performance/speed) - [Using JavaScript frameworks and libraries on HubSpot](/guides/cms/setup/js-frameworks) # Use a reverse proxy with HubSpot @ While websites built on HubSpot automatically use a global CDN, HubSpot also supports reverse proxies. If you have an existing CDN or complex routing rules that aren't possible to maintain using HubSpot's CDN, learn how to set up a reverse proxy for your HubSpot-hosted content. $ guides & cms/content/performance --- # Use a reverse proxy with HubSpot Websites built on HubSpot's CMS automatically use HubSpot's global CDN with hundreds of local points of presence. However, you may have an existing CDN or complex routing rules that are not possible to maintain using HubSpot's built-in CDN. In that case, you may want to set up a reverse proxy with HubSpot instead. A reverse proxy is a type of proxy server that takes resources from one or more servers and then returns them to the client with the appearance of it coming from the proxy server itself. For example, you have an existing website at `www.website.com` which is not hosted on the HubSpot CMS, while also hosting a blog on HubSpot at `www.website.com/blog`. Using a reverse proxy, the blog would appear to be hosted from the same server as the website when it's actually coming from HubSpot's servers. Below, learn more about how to set up a reverse proxy with HubSpot. By the end of the guide, you will have: - Reviewed the [considerations](#considerations) for whether to set up a reverse proxy. - [Configured the proxy](#1.-configure-the-proxy) in your external environment. The guide includes general instructions as well as provider-specific instructions for [Amazon CloudFront](set-up-a-reverse-proxy-in-amazon-cloudfront) and [nginx](#set-up-a-reverse-proxy-using-nginx). - [Confirmed that the configuration is correct](#2.-confirm-your-configuration-is-correct). This guide also includes troubleshooting steps if you're seeing a `404` error during that process. - [Added the domain to HubSpot](#3.-add-the-domain-to-hubspot) to enable content creation in HubSpot. Note that this guide assumes that you have an existing website or app that uses a CDN or web service which supports reverse proxies. - Reverse proxy setup is not provided by HubSpot's support team. [You may purchase time with a HubSpot Technical Consultant](#interested-in-purchasing-consulting-to-help-configure-reverse-proxy-fill-out-the-form-below-) for support with implementing a reverse proxy on HubSpot, or use our [community forums for peer-to-peer support](https://community.hubspot.com/t5/HubSpot-Developers/ct-p/developers). - HubSpot’s built-in CDN and all other services have multiple instances with automatic failover and recovery. If you implement a reverse proxy, it's highly recommend that you use multiple instances with load balancing. If all requests are instead routed through a single proxy node, it’s possible that requests will trip rate limiting protocols. This would result in requests being served `429` or `403` responses until an in-browser JavaScript challenge is completed. ## Considerations Using your own CDN or reverse proxy may open up more configuration options, but it also requires significant operational knowledge and maintenance. Additionally, if you proxy a subpath of your site to HubSpot, your main `sitemap.xml` file won't include HubSpot pages unless you manually add them. Before proceeding with a reverse proxy setup, review the list of feature considerations below. | Feature | HubSpot's CDN | Custom Solution | | --- | --- | --- | | Bandwith | Included | Additional cost | | SSL | Included; automatic setup | Additional cost; manual setup | | Global CDN | Included | ? | | Automatic cache management | Included | No | | Anti-abuse protection | Included | Customer-owned | | 24x7 monitoring | Included | Customer-owned | | Support | Included | Customer-owned | | IPv6 | Included | ? | | HTTP/2 | Included | ? | | Latency | Optimal | Additional network hop required | ## 1. Configure the proxy Adding a custom reverse proxy means that users of your website will make a request to your service and then be proxied through to HubSpot’s CDN, introducing another network hop. To start the proxy setup process, first configure the proxy in your external environment, such as a CDN like [Amazon CloudFront](#set-up-a-reverse-proxy-in-amazon-cloudfront) or an [nginx](#set-up-a-reverse-proxy-using-nginx) server. You should always implement the proxy in a load balanced environment so that traffic from your proxy rotates requests to the HubSpot CNAME origin from multiple IP addresses. The CNAME needed for the proxy will be in the following format: `.`. The suffix value is determined by your account's [assigned data center](https://knowledge.hubspot.com/account-security/hubspot-cloud-infrastructure-and-data-hosting-frequently-asked-questions#how-can-i-check-my-data-center-and-where-my-account-data-is-hosted) and the last two digits of your HubID. Use the tables below to find the correct suffix along with the correct numbers to include in the suffix. **Suffixes by data center** | Data center | Suffix | | -------------- | ---------------------------------- | | US East | `sites-proxy.hscoscdn[##].net` | | US West | `sites-proxy.hscoscdn[##]-na2.net` | | Canada | `sites-proxy.hscoscdn[##]-na3.net` | | European Union | `sites-proxy.hscoscdn[##]-eu1.net` | | Australia | `sites-proxy.hscoscdn[##]-ap1.net` | **Suffix numbers by HubID** | HubIDs ending with | Suffix number | | ------------------ | ------------- | | 00-10 | `00` | | 11-19 | `10` | | 20-29 | `20` | | 30-39 | `30` | | 40-49 | `40` | | 50-59 | `00` | | 60-69 | `10` | | 70-79 | `20` | | 80-89 | `30` | | 90-99 | `40` | For example, if your HubID is `123456` and is hosted in the US East data center, the correct origin CNAME would be `123456.sites-proxy.hscoscdn00.net`. Once you've noted the correct CNAME to use, continue reading for general instructions for configuring a reverse proxy, as well as specific guidance for [Amazon CloudFront](#set-up-a-reverse-proxy-in-amazon-cloudfront) and [nginx](#set-up-a-reverse-proxy-using-nginx). If you have _Cloudflare Enterprise_, you can set up an [Orange-to-Orange (O2O)](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/saas-customers/provider-guides/hubspot/) reverse proxy configuration for HubSpot in Cloudflare. ### General instructions In general, you can configure your proxy to forward requests using your origin CNAME and add the following configurations: 1. Set your proxy to perform no caching for paths originating from HubSpot. HubSpot automatically manages the content of our CDN’s cache so pages are updated automatically when content is published. Note that if the service caches responses, ​pages may not update for hours or days​. 2. Add or prepend to a ​`X-Forwarded-For`​ header with the client IP address of the original requestor. This is required to differentiate clients from each other. Many services such as CloudFront maintain these headers automatically. 3. To ensure personalized content based on location works, pass a static header of `X-HubSpot-Trust-Forwarded-For: true`​. This will trust the `​X-Forwarded-For​` header, which may not have been updated by all upstream proxies. 4. Pass a ​`X-HS-Public-Host`​ header with a value of your destination domain. 5. Allow all HTTP methods. 6. Ensure an SSL certificate is provisioned and installed for your proxy domain. 7. Forward all query strings. 8. Forward ​all​ other request and response headers as-is, including cookies. 9. Ideally, all paths under your domain should proxy to HubSpot. If that’s not the case, then the following paths must proxy so assets load properly from your domain: `/_hcms/*`, `/hs/*`, `/hubfs/*`, `hs-fs/hubfs/*`, `/hs-fs/*​`, `/cs/c/*`, and `/e3t/*`. The `hs-fs/hubfs/*` path was added as of December 20, 2024, and replaces the `hs-fs/hub/*` path. Your reverse proxy configuration should allow for the new path, and you may want to update any hard-coded references to the old path to avoid redirects. Learn more in the [developer changelog](https://developers.hubspot.com/changelog/cdn-path-for-static-assets-css-javascript-changing). ## 2. Confirm your configuration is correct - You can identify issues with your configuration in your HubSpot domain settings: - In your HubSpot account, click the **settings icon** in the main navigation bar. - In the left sidebar menu, navigate to **Website** \> **Domains & URLs**. - Next to the domain you are using for your reverse proxy, click the **Edit** dropdown menu and select **Verify Reverse Proxy Connection**. - Click any **category** that is labeled as _Failed_ to view recommendations for fixing the issue. - Once you've implemented your fixes, click **Refresh test** to check your configuration again. If you see an error in your domain settings stating that “Reverse proxy domains need your action to avoid website disruption,” learn how to [set up a Domain Control Validation (DCV) record](https://knowledge.hubspot.com/domains-and-urls/ssl-and-domain-security-in-hubspot#set-up-a-domain-control-validation-dcv-record). - For all other accounts, to can confirm your configuration, visit: `https://[yourFullDomain]/_hcms/diagnostics` - Verify the following information: - The current time value changes on every load. This confirms that the page is not cached. - The `User-Agent` is consistent with your browser. - The `Accept-Language` value is consistent with your browser. - The `Accept-Encoding` header is `*`. This ensures that responses are compressed. - The `Cookie` value is not blank. - The `Protocol` is `https`. - The leftmost IP address in `X-Forwarded-For` matches your IP address as reported by a service like [https://www.whatismyip.com](https://www.whatismyip.com/). - The `IP-Determined Location` values are accurate to your location. These are based on the IP-related headers in `X-Forwarded-For`. ### Troubleshooting If you're seeing a `404` when going to the diagnostics URL, that likely means you have an issue with your configuration. Visit `https://[yourFullDomain]/_hcms/_worker/headers` to view all the headers that HubSpot is receiving from a request through your reverse proxy. The most important headers for proxies are: - `X-Forwarded-For` - `X-HubSpot-Trust-Forwarded-For` - `X-HS-Public-Host` Verify you are not sending additional/unnecessary headers, or duplicate values. #### Clicks registered as bot events If you're using Amazon CloudFront and are seeing clicks registered as bot events, the `User-Agent` is likely being set as Amazon CloudFront instead of the visitor's. To fix this, update your [managed origin request policies](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html) to forward the visitor's `User-Agent` instead. ## 3. Add the domain to HubSpot With your proxy configured, you’ll then add your domain to HubSpot. You will not be fully connecting the domain to HubSpot in the way that you would in the standard domain connection process. Rather, you’ll start the connection process to make the domain available for publishing HubSpot content, but you will not create CNAME records in your DNS provider. By the end of this process, your proxy will receive all requests to the domain and can choose to proxy certain paths to HubSpot and other paths to other content hosts. To add your domain to HubSpot: - In your HubSpot account, navigate to your [domain settings](https://app.hubspot.com/login?loginRedirectUrl=https%3A%2F%2Fapp.hubspot.com%2Fshortlink%2Fdomains). - Click **Connect a domain**. - Select **Primary** or **Secondary**. Redirect and email sending domains are not supported for this feature. - Click to toggle the **Connect with HubSpot's built-in content delivery network (CDN)** switch off, then click **Connect**. - Select the **content type** you’ll be hosting on the domain, then click **Next**. - Enter the **brand domain**. For example, for _www.website.com_, you would enter **website.com**. This domain must match the domain being requested through your reverse proxy. - Enter the **subdomain** that you’ll be hosting content on. The subdomain needs to match the subdomain for the externally hosted domain. Then click **Next**. - Review the domain you’ve entered, then click **Next**. - Next, verify your domain so that HubSpot can confirm your domain ownership and allow content publishing: - In your DNS provider, create the records using the provided values. - In HubSpot, click **Verify**. It can take up to 4 hours for HubSpot to recognize changes made to your DNS provider and verify your hostname. ## Provider-specific instructions While you can use the [general instructions above](#general-instructions) to set up your proxy, below are steps for setting up a reverse proxy with [Amazon CloudFront](#setup-up-a-reverse-proxy-in-amazon-cloudfront) and [nginx](#set-up-a-reverse-proxy-using-nginx) specifically. If you have _Cloudflare Enterprise_, you can set up an [Orange-to-Orange (O2O)](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/saas-customers/provider-guides/hubspot/) reverse proxy configuration for HubSpot in Cloudflare. ### Set up a reverse proxy in Amazon CloudFront To set up a reverse proxy in Amazon CloudFront, you’ll need to create a new distribution with a new alternate domain name, create a new origin, then create cache behaviors for the page paths where your HubSpot content is hosted. You can learn more about working with distributions in the [AWS documentation](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-working-with.html). - Log in to your Amazon CloudFront account. - In the left sidebar, navigate to **Distributions**. - If you’re starting from scratch, you’ll first need to create a new distribution by clicking **Create Distribution**. Alternatively, you can edit an existing distribution or skip to the [origin and behaviors setup steps](#origin). - On the _General_ tab, click **Edit**. - In the **Alternate Domain Names (CNAMEs)** field, add the domain, including the subdomain. This must match the domain you added to HubSpot. - Confirm your changes by clicking **Yes, Edit**. You’ll then be directed back to the _General_ tab where your domain should now be listed next to _Alternate Domain Names (CNAMEs)_. - You’ll also need to create a new CNAME record in your DNS provider using the value from the _Domain Name_ field. This value should look something like `.cloudfront.net`. - Next, set up a new origin: - Click the **Origins and Origin Groups** tab. - Click **Create Origin**, then set up your origin: - In the **Origin domain** field, enter the `.` CNAME value from the [table above](#configure-the-proxy). This value should look something like `123.sites-proxy.hscoscdn20.net`. - Under _Add custom header_, click **Add header**. Then, add the following header details: - To ensure personalized content based on location works, either pass a `X-Client-IP` header with a value of the end user’s IP (preferred) or pass a static header of `X-HubSpot-Trust-Forwarded-For: true`. The latter will trust the `X-Forwarded-For` header, which may not have been updated by all upstream proxies. - Pass a `X-HS-Public-Host` header with a value of your destination domain. - Click **Create** to save your changes. - Then, set up [cache behaviors](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#DownloadDistValuesCacheBehavior) for the page paths you’ll be hosting HubSpot content on: - Click the **Behaviors** tab. - Click **Create Behavior**. - In the **Path pattern** field, enter the URL path of the page that your HubSpot content is hosted on. This can be a path to a specific page, or a flexible URL such as a wildcard. Learn more about [path patterns](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#DownloadDistValuesPathPattern). - Click the **Origin and Origin Groups** field, then select the origin you created earlier. - Click **Save changes**. With your distribution, origin, and behaviors configured, the reverse proxy will now be available for HubSpot pages that you create at the specified paths. Proceed to the steps for [configuring your domain in HubSpot](#add-the-domain-to-hubspot). By default, if no Origin Request Policy configuration is specified, Amazon CloudFront [strips cookies, query parameters and most of headers](https://aws.amazon.com/developer/application-security-performance/articles/api-dynamic-acceleration/#:~:text=By%20default%2C%20If%20you%20do%20not%20explicitly%20configure) before forwarding the request to your origin. This includes setting the User-Agent to `Amazon CloudFront`, which will result in clicks to be marked as bot events. To resolve this, you should [update origin request policies](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html#managed-origin-request-policy-user-agent-referer) to forward the visitor's User-Agent instead. ### Set up a reverse proxy using nginx To configure a reverse proxy with nginx, you’ll need to create a location configuration file that includes SSL information and the location path information. Full documentation for setting up reverse proxies with nginx can be found in the [nginx documentation](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/). In particular, you may want to review documentation for settings such as [securing upstream traffic](https://docs.nginx.com/nginx/admin-guide/security-controls/securing-http-traffic-upstream/), [proxy_ssl](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ssl_server_name), and [$proxy_protocol_addr](https://docs.nginx.com/nginx/admin-guide/load-balancer/using-proxy-protocol/). When working with nginx, there are several headers and settings required to route traffic. Below are snippets of a sample nginx location configuration file to use as a starting point. ```shell location ~ ^(/|/some-other-path) { proxy_set_header Host $http_host; proxy_set_header X-HS-Public-Host www.example.com; proxy_pass_request_headers on; proxy_set_header X-HubSpot-Trust-Forwarded-For true; proxy_pass https://2XX93.sites-proxy.hscoscdnXX.net; proxy_ssl_name www.example.com; proxy_ssl_server_name on; proxy_set_header X-Real-IP $proxy_protocol_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-HubSpot-Client-IP $proxy_protocol_addr; } location ~ ^(/hs|/_hcms|/hubfs|/hs-fs|/cs/c) { proxy_set_header Host $http_host; proxy_set_header X-HS-Public-Host www.example.com; proxy_pass_request_headers on; proxy_set_header X-HubSpot-Trust-Forwarded-For true; proxy_pass https://2XX93.sites-proxy.hscoscdnXX.net; proxy_ssl_name www.example.com; proxy_ssl_server_name on; proxy_set_header X-Real-IP $proxy_protocol_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-HubSpot-Client-IP $proxy_protocol_addr; } ``` In the above code, note that the proxy connection and content host domain are different: one is a HubSpot provided CNAME ([see configuration table](#configure-the-proxy)), and the other is the domain that the content should be served from, matching the domain you’ll add to HubSpot. This is due to the SNI (Server Name Indication) connection process that establishes a secure connection between your proxy and HubSpot’s servers. To enable this in your nginx proxy, ensure that you include the settings below, which are also in the above code: ```shell proxy_ssl_name www.example.com; proxy_ssl_server_name on; ``` These settings instruct nginx to send the server domain name with the SNI SSL connection handshake since the origin domain is different from the Host. ## Request a consultation Fill out the form below to request a consultation from a HubSpot Technical Consultant. # Optimizing your HubSpot CMS site for speed @ Tips and strategies to improve your website's speed. Make your CMS Hub theme or site fast, and learn how to test your site's speed. $ guides & cms/content/performance --- # Optimizing your HubSpot CMS site for performance Great user experience is a factor of content quality, speed, security and [accessibility.](/guides/cms/content/accessibility) Optimizing for these generally also improves Search Engine Optimization (SEO). Better performance is all about providing a better experience for end users. Achieving better performance is all about solving for your individual site's bottlenecks. ## Common website performance bottlenecks Most web performance optimization techniques and best practices are not HubSpot-specific. Instead, they fall into a few categories: - **Loading performance:** the efficiency of transferring all of the files needed for your web page to the user's browser. The quantity of files, size of files, and the delivery speed of those files determines loading performance. - **Rendering performance:** the efficiency for the browser to take everything it downloaded, process it, and display the computed end result to the user. Rendering performance in particular is complex and is impacted by several factors, including: - The loading of Cascading Style Sheets (CSS) - The loading of JavaScript (JS) - The loading of media, such as images and videos - The device or web browser the visitor is using - The speed of response to user interactions CSS is render-blocking, which means that poorly written CSS can cause Cumulative Layout Shift (CLS) during page rendering. Images can cause CLS, and take up RAM. Video players can cause CLS, some file formats require more processing work. JS can manipulate the [Document Object Model (DOM)](https://developer.mozilla.org/en-US/docs/Glossary/DOM) and [Cascading Style Sheet Object Model (CSSOM)](https://developer.mozilla.org/en-US/docs/Glossary/CSSOM)of a page, causing any of those issues. JS can also be resource intensive. All of these factors need to be balanced and best practices followed to ensure a fast experience for all visitors. ## What HubSpot handles for you HubSpot's CMS automatically handles many common performance issues, including: - CDN with Image optimization and automatic WebP conversion - HTTP2 - Javascript and CSS minification - Browser and server caching - [Prerendering](/guides/cms/content/performance/prerendering) - [Domain Rewriting](/guides/cms/content/performance/overview) - [Brotli compression (with fallback to GZIP Compression)](/guides/cms/content/performance/overview#text-compression) - [HubSpot Blog posts support AMP](/guides/cms/content/performance/overview#accelerated-mobile-pages-amp-) When including CSS in a custom module, HubSpot intelligently loads `module.css` only when a module is used on a page, and only loads it once regardless of how many instances of the module are on the page. By default, `module.css` does not load asynchronously, but you can change this by including [css_render_options](/reference/cms/modules/configuration) in the module’s `meta.json` file. ## Improve your Website speed further Along with everything that HubSpot handles, there are some things you can do as a developer that have a big impact on your site's performance. ### Start with a good foundation It's easier to build from a great foundation that was built with performance in mind, than trying to fix performance issues later. Building a fast car from the ground up is easier than buying a slow car and trying to make it fast. The [HubSpot CMS Boilerplate](/guides/cms/content/themes/hubspot-cms-boilerplate) was built to be fast, and encourage best practices. See the [GitHub README](https://github.com/HubSpot/cms-theme-boilerplate) to review the current scores in Lighthouse and Website Grader. By building from the boilerplate, you're already starting from a set of high scores. This means that you can focus your attention on the code you want to add on top of the boilerplate. ### Images Images are prevalent on almost every page on the web. Images are usually the largest files on a page. The more images, and the larger the images, the longer your page will take to load. Animated images such as gifs and animated webp files also take up more space than non-animated images of the same size. Some image formats also are more performant than others, and better for certain scenarios. #### What you can do 1. The most important thing you can do is [optimize your images](https://blog.hubspot.com/marketing/compress-image) for the web. Image optimization is very much a shared responsibility among both content creators and developers. While HubSpot converts your images to webp and you can resize images using [`resize_image_url()`](/reference/cms/hubl/functions#resize-image-url), uploading a non webp file that is already sized appropriately can help. 2. Use fewer images per page. 3. [Use the right image format for the use-case](https://blog.hubspot.com/insiders/different-types-of-image-files). 4. Use Scalable Vector Graphics (SVGs) where possible. SVGs can scale in size infinitely without losing quality. Inlining your SVGs makes sense when you are making animations. In static graphics creating an SVG sprite sheet or simply treating it as a normal img element, or background image is typically better for performance. 5. Intelligently [lazy load images](/guides/cms/content/performance/lazy-loading). 6. Make sure `img` elements contain height and width HTML attributes. This makes it so web browsers can intelligently optimize for [cumulative layout shift](https://web.dev/cls/) during render time and also makes it so HubSpot can generate a `srcset` for you. 7. Use the [CSS aspect-ratio](https://developer.mozilla.org/en-US/docs/Web/CSS/aspect-ratio) property to reserve space when img dimensions may change. 8. Use [`resize_image_url`](/reference/cms/hubl/functions#resize-image-url) to force images to be resized to a certain resolution. 9. For background images, [use media queries](https://web.dev/optimize-css-background-images-with-media-queries/) in combination with `resize_image_url` to deliver images at sizes that make sense for the device. 10. For large hero images - you can preload them by using `` within a [`require_head` tag](/reference/cms/hubl/tags/standard-tags#require-head). Use this technique sparingly, overusing it can actually hurt performance. 11. If you fully control the HTML for an `img` and can predict it's sizes at different viewport sizes, providing a custom `srcset` and `sizes` attribute can help. You can use the[`resize_image_url`](/reference/cms/hubl/functions#resize-image-url) function to generate the alternate sizes. A custom tailored `srcset` and `sizes` based on the actual usage of the `img` element, will likely be more effective than the HubSpot generated one, but the automatically generated one is better than nothing. 12. Add to your `img` element [`decoding="async"`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/decoding). This tells the browser that it can start loading other content on the page at the same time as it's loading and processing the image. ### Autoplaying video Video backgrounds and auto-playing videos can certainly set a website apart. Unfortunately they come at a cost. Video backgrounds are often used for website headers. When a video auto-plays, it means the browser needs to start loading the video right away. This can be especially problematic for users on slower connections or using cellphone data. #### What you can do 1. Avoid using autoplaying video. If what you're showing is a background animation, consider using CSS animations or javascript animations. If you must display an autoplaying video: 2. Choose a reasonable resolution for the video based on your use-case, and apply an effect over the video to make a lower resolution less noticeable. 3. Make sure the video scales in quality based on the device and connection, the best way to do this is using a video sharing/hosting service like YouTube, Vidyard, or Vimeo. 4. Disable autoplaying on mobile, show a fallback image instead. 5. If the video is loaded by iframe, add `loading="lazy"`. This tells the browser it can wait to render and process the iframe until the user is close to displaying it on screen. ### JavaScript JavaScript (JS) is useful for adding interactivity to your website. Loading a lot of JS code in general increases the file size of the JS files and the amount of time it takes for the browser to render interactive elements. Loading JS in the `` can also be a problem as javaScript is [render blocking resource](https://web.dev/render-blocking-resources/) by default. Additionally JS is running on the visitors device, meaning it is limited to the resources that device has. #### What you can do 1. When HubSpot's CMS first came out jQuery was loaded in the `` by default. You can remove it entirely in **Settings > Website > Pages,** or [upgrade to the latest version of jQuery](/guides/cms/improve-older-sites/upgrade-jquery). Take care when changing these settings on older websites if you did not build them, they may have been built reliant on jQuery or based on jQuery loading in the header. 2. Ensure javascript is loaded just before the `` to prevent render blocking. You can use `require_js` to load js for modules or templates only when needed and without accidentally loading the javascript multiple times for multiple instances of a module. 3. Consider refactoring your JS to be more efficient. Use fewer JS plugins, use semantic HTML where it can help. For example for dropdowns, use `
        ` and ``. For modals use ``. 4. If you're using a giant JS library just for a few small features, consider using vanilla JS or loading a subset of the library if possible. 5. Use [require_js](/reference/cms/hubl/functions#require-js) to load JS only when necessary and only once per page. When using `require_js`, use `async` or `defer` attributes to improve page performance. 6. To control where and when a module's JavaScript loads, use [js_render_options](/reference/cms/modules/configuration) in the module's meta.json file. 7. If loading external resources use [preconnect and DNS prefetch](https://web.dev/preconnect-and-dns-prefetch/) appropriately to deliver a faster experience. 8. Limit the number of tracking scripts you use. Tracking scripts often try to understand all of the actions a user is taking on a page to provide you insights. That is a lot of code analyzing what the user is doing. Each tracking script amplifies this. 9. When [handling interactions from a user, prioritize how you respond](https://web.dev/articles/optimize-inp#optimize_interactions) to focus on what's most important to be done right away, and defer through `setTimeOut` and/or `RequestAnimationFrame` any code that needs to happen in response to the user interaction, but can happen later or is not going to be visible to the user right away. ## SEO recommendations tool The HubSpot Recommendations tool is a great way to get performance and SEO feedback specific to your website. [Learn more about the recommendations tool](https://knowledge.hubspot.com/seo/view-seo-recommendations-in-hubspot) ## Code Alerts Code Alerts is a CMS Hub Enterprise feature which acts as a centralized overview of issues that are identified inside of your HubSpot CMS website. Fixing issues that are identified in Code Alerts can help to optimize your website performance. Issues identified comprise several different areas from HubL limits to CSS issues. [Learn more about Code Alerts.](/guides/cms/debugging/code-alerts) ## Additional resources for improving your site's speed There is a lot that can be done to optimize a site for speed and many of the topics warrant a further breakdown. We've compiled some great resources we encourage you to check out when working on optimizing your site. - [Site Speed and Performance: what you can do, and how HubSpot Helps](https://designers.hubspot.com/blog/site-speed-and-performance-what-we-do-and-what-you-can-do) - [How we improved page speed on HubSpot.com](https://developers.hubspot.com/blog/how-we-improved-page-speed-on-hubspot.com) - [15 tips to speed up your website](https://designers.hubspot.com/blog/15-tips-to-speed-up-your-website) - [5 easy ways to help reduce your website page's loading time](https://blog.hubspot.com/marketing/how-to-reduce-your-websites-page-speed) - [8 step guide to achieving 100% Google Page Speed](https://blog.hubspot.com/marketing/google-page-speed) - [Website Optimization - HubSpot Academy](https://academy.hubspot.com/courses/website-optimization) - [Web.dev](https://web.dev/learn/) - [How we optimize the HubSpot CMS - Jeff Boulter](https://www.youtube.com/watch) - [The humble img element and Core Web Vitals - Smashing Magazine](https://www.smashingmagazine.com/2021/04/humble-img-element-core-web-vitals/) ### Image Optimization Optimizing your images for the web prior to uploading and serving them helps ensure you won't serve an oversized image for the screen and use-case. **Popular image optimization tools:** - [ImageOptim](https://imageoptim.com/mac) - [Adobe Photoshop](https://www.adobe.com/products/photoshop.html) - [Adobe Illustrator](https://www.adobe.com/products/illustrator.html) - [Optimizilla](https://imagecompressor.com/) ### Performance testing Testing performance and optimizing for it should be apart of any website build out. There are many tools available for testing a website's speed, some of which grade and some of which only measure. It's important to understand these tools and how they work, so you can make educated decisions around performance improvements. Popular performance tools include: - [Website Grader](https://website.grader.com/) - [GTMetrix](https://gtmetrix.com/) - [Google Page Speed Insights](https://developers.google.com/speed/pagespeed/insights/)and other [Google performance tools](https://developers.google.com/web/fundamentals/performance/speed-tools). - [Pingdom](https://www.pingdom.com/) - [WebPageTest](https://www.webpagetest.org/) #### Measurement tools Tools that measure will usually test the follow aspects of a page: - Loading time - Script execution time - Time until first contentful paint - Network times for assets downloading These tools will generally provide results that state specific times for each of these metrics. If you retest, generally the measurements will shift slightly because not every page load is exactly the same. #### Grading tools In addition to measuring, grading tools will assign a grade to your page based on its testing, often in a letter or percent form. While these tools are intended to motivate making improvements, there are many different metrics and aspects to performance that need to be taken into account when reviewing results. - It's recommended to use multiple tools and strive for the best score you can in each. Understand, though, they will weight things differently. Efforts that may improve a score in one tool may not improve it in others. - It's not recommended to base your overall performance off of one metric's grade. Some metrics have different levels of affect on perceived performance, which results in some tools weighing these metrics differently to calculate their final grade. - There is no industry-wide standard for how to weigh metrics for grading. Over time, weights can and will change, as has occurred with [Google Page Speed](https://googlechrome.github.io/lighthouse/scorecalc/). There is also no industry-wide accepted for what is considered the minimum or maximum "good" value for individual metrics. Some tools base this off of a percentile of websites that have been tested., meaning that your scores are being compared to other sites. - Over time, a high grade for speed range has become more difficult to attain. Some tools instead look at user experience, visitor retention, and ROI-based research to determine what the threshold for a good score should be. - Not all tools take into account subsequent page load performance. For example, the HubSpot module system separates CSS and JS for individual modules, and only loads those assets when the module is actually placed on the page. This can result in several smaller CSS files, which could get flagged by Google Page Speed Insights. But by doing this, the next page load won't need to download any of the CSS or JS for any modules that repeat on the next page, as they're cached. This means that subsequent page loads would be kilobytes instead of a monolithic file. **Related:** - [How Lighthouse performance scoring works](https://web.dev/performance-scoring/) - [Website Optimization Roadmap (Core Web Vitals) | Mark Ryba](https://www.youtube.com/watch) # How to provide a good experience in the page editor @ How to make sure your CSS/JS displays well in the HubSpot CMS editors. $ guides & cms/content --- # How to provide a good experience in the page editor 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. I'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 $('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](https://developers.hubspot.com/docs/tools/boilerplate-css) 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 `