# 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**.
  <ul>
    <li>
      Developer test accounts will expire after 90 days if no API calls are made
      to the account. You can either manually renew the account from the _Test
      accounts_ page in HubSpot, or by making an API call to the account.
    </li>
    <li>
      When renewing via API call, you must use an [OAuth
      token](/guides/api/app-management/oauth-tokens) generated from an
      application in the same developer account as the test account you want to
      renew. In addition, renewals must be done no more than 30 days before the
      test account's expiration date.
    </li>
  </ul>
## 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

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 [<u>Node.js</u>](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.


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

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

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](/guides/cms/quickstart)
- Check out the [CMS guides](/guides/cms) and [reference docs](/reference/cms)


# 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 <u>does not</u> 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 <u>not</u> 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 <u>not</u> 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
<button
  type="button"
  id="hs_show_banner_button"
  onClick="(function(){
        var _hsp = window._hsp = window._hsp || [];
        _hsp.push(['showBanner']);
      })()"
>
  Cookie Settings
</button>
```
```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
<button
  id="hs-hud-cookie-settings"
  onClick="(function(){
      var _hsp = window._hsp = window._hsp || [];
      _hsp.push(['showBanner']);
    })()"
>
  Cookie Settings
</button>
```
```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
<a
  id="hs-cookie-settings-link"
  onClick="(function(){
      var _hsp = window._hsp = window._hsp || [];
      _hsp.push(['showBanner']);
    })()"
>
  Cookie Settings
</a>
```
```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:

`<script src=”https://my.advertisement.script.com/ads”></script>`

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
<script>
 var _hsp = window._hsp = window._hsp || [];
_hsp.push(['addPrivacyConsentListener', (consent) => {
if (consent.categories.advertisement) {
const script = document.createElement('script');
        	script.src = "https://my.advertisement.script.com/ads";
        	document.head.appendChild(script)
}
}])
</script>
```

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
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_TRACKING_ID"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){window.dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'GA_TRACKING_ID');
</script>
```

To load Google Analytics when analytics consent has been given, the gtag script needs to be added when consent is given:

```js
<!-- Google tag (gtag.js) -->
<script>
     var _hsp = window._hsp = window._hsp || [];
    _hsp.push(['addPrivacyConsentListener', (consent) => {
    if (consent.categories.analytics) {
        const script = document.createElement('script');
          script.src = "https://www.googletagmanager.com/gtag/js?id=GA_TRACKING_ID";
          script.async = 'true'
          document.head.appendChild(script)
      }
    }])
</script>
<script>
    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag('js', new Date());
    gtag('config', 'GA_TRACKING_ID');
</script>
```

### 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
<!-- Hotjar Tracking Code -->
<script>
    (function(h,o,t,j,a,r){
        h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
        h._hjSettings={hjid:HOT_JAR_ID,hjsv:6};
        a=o.getElementsByTagName('head')[0];
        r=o.createElement('script');r.async=1;
        r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
        a.appendChild(r);
    })(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
</script>
```

To ensure Hotjar runs when analytics consent is given, the consent listener can be added.

```js
<!-- Hotjar Tracking Code -->
<script>
var _hsp = window._hsp = window._hsp || [];
_hsp.push(['addPrivacyConsentListener', (consent) => {
	if (consent.categories.analytics){
(function(h,o,t,j,a,r){
        		h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
        		h._hjSettings={hjid:HOT_JAR_ID,hjsv:6};
        		a=o.getElementsByTagName('head')[0];
        		r=o.createElement('script');r.async=1;
        		r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
        		a.appendChild(r);
    		})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
}

}])
</script>
```

## 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
<script>
  // disable the hubspot cookie banner window.disableHubSpotCookieBanner = true
  // listen for the third party consent event and send consent to HubSpot
  window._hsp = window._hsp || [];
  document.addEventListener("thirdPartyConsentEvent", (e) =>{' '}
  {window._hsp.push([
    'setHubSpotConsent',
    {
      analytics: e.detail.analytics,
      advertisement: e.detail.advertisement,
      functionality: e.detail.functionality,
    },
  ])}
  )
</script>
```


# 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 <u>either</u> 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 `<head>` 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.

<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
    dataLayer.push(arguments);
}
// Determine actual values based on your own requirements,
gtag('consent', 'default', {
    'analytics_storage': 'denied',
    'ad_storage': 'denied',
    'ad_user_data': 'denied',
    'ad_personalization': 'denied',
  // Use region, to specifiy where this default should be applied.
    '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'
    });
}]);
</script>
// Step 4: This snippet installs Google Analytics 4

<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'G-XXXXXXXXXX');
</script>
```

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 `<head>` 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

<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXXX');</script>
<!-- End Google Tag Manager -->
```

- Add this code immediately after the opening `<body>` tag

```js
// Step 4 continued: This snippet installs Google Tag Manager

<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
```

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
<script>
// 1. Set up default denied consent
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"]
});

// 2. Send consent updates from the cookie banner to google tags
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'
    });
}]);
</script>
```

### 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 `<head>` 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 `<head>` html.

```js
<script>
// 1. Set up default denied consent
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"]
});

// 2. Send consent updates from the cookie banner to google tags
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'
    });
}]);
</script>
// GA4 installation code
<script async src="https://www.googletagmanager.com/gtag/js?id=MEASUREMENT_ID>"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'MEASUREMENT_ID');
</script>
```

### 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 `<head>` tag.

```js
<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','CONTAINER_ID');</script>
<!-- End Google Tag Manager -->
```

**2\. Body code**: Google instructs that this code be pasted into the `<body>` tag.

```js
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=CONTAINER_ID"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
```

#### 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 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 up the path for the initial page view -->
<script>
  var _hsq = window._hsq = window._hsq || [];
  _hsq.push(['setPath', '/home']);
</script>

<!-- Load the HubSpot tracking code -->
<!-- Start of HubSpot Embed Code -->
  <script type="text/javascript" id="hs-script-loader" async defer src="//js.hs-scripts.com/{hubId}.js">
</script>
<!-- End of HubSpot Embed Code -->

<!-- Tracking subsequent page views -->
<script>
  var _hsq = window._hsq = window._hsq || [];
  _hsq.push(['setPath', '/about-us']);
  _hsq.push(['trackPageView']);
</script>
```

### 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 `<link rel="canonical">` tag in your page. If your page uses `<link rel="canonical">` 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.


# 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 <u>only</u> 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 <u>not</u> 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 <u>do not</u> 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 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
<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>
```

## 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:<ul><li>`false` (default): uses the [chat widget's settings](https://knowledge.hubspot.com/chatflows/create-a-live-chat#4-options).</li><li>`true`: presents cookie banners when the widget is loaded.</li><li>`ON_WIDGET_LOAD`: same as `true`.</li><li>`ON_EXIT_INTENT`: enable cookie banners when the user exhibits an exit intent.</li></ul> |
| `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

</div>
```
```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 <u>not</u> 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

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 <u>cannot</u> 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 <u>cannot</u> 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.


# Sunsetted and deprecated APIs

The APIs listed in the sections below will <u>not</u> 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 <u>not</u> 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:<ul><li>[Get publishing channels](/reference/api/deprecated/overview)</li><li>[Get broadcast messages](/reference/api/deprecated/social-media/v1#get-broadcast-messages)</li><li>[Get a broadcast message](/reference/api/deprecated/social-media/v1#get-a-broadcast-message)</li><li>[Create a broadcast message](/reference/api/deprecated/overview)</li><li>[Cancel a broadcast message](/reference/api/deprecated/overview)</li></ul> |


# 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');
  }
});
```


# 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 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)
<thead>
<tr>
<th>Language</th>
<th>Package Link</th>
<th>[Source Code](https://github.com/HubSpot/hubspot-api-nodejs)</th>
</tr>
</thead>
<tbody>
<tr>
<td style={{display: 'flex', alignItems: 'center',justifyContent: 'space-around'}}><img style={{maxWidth: '60px'}} src="https://f.hubspotusercontent00.net/hubfs/53/iconfinder_nodejs-512_339733.png" alt="Node.Js" />Node.Js</td>
<td><a href="https://www.npmjs.com/package/@hubspot/api-client">npm install @hubspot/api-client</a></td>
<td>[hubspot-api-nodejs](https://github.com/HubSpot/hubspot-api-nodejs)</td>
</tr>

<tr>
<td style={{display: 'flex', alignItems: 'center', justifyContent: 'space-around'}}><img style={{maxWidth: '60px'}} src="https://f.hubspotusercontent00.net/hubfs/53/new-php-logo.png" alt="PHP" />PHP</td>
<td><a href="https://packagist.org/packages/hubspot/api-client">composer require hubspot/api-client</a></td>
<td>[hubspot-api-php](https://github.com/HubSpot/hubspot-api-php)</td>
</tr>
<tr>
<td style={{display: 'flex', alignItems: 'center', justifyContent: 'space-around'}}><img style={{maxWidth: '60px'}} src="https://f.hubspotusercontent00.net/hubfs/53/ruby.png" alt="Ruby" />Ruby</td>
<td><a href="https://rubygems.org/gems/hubspot-api-client">gem install hubspot-api-client</a></td>
<td>[hubspot-api-ruby]([hubspot-api-nodejs](https://github.com/HubSpot/hubspot-api-nodejs))</td>
</tr>
<tr>
<td style={{display: 'flex', alignItems: 'center', justifyContent: 'space-around'}}><img style={{maxWidth: '60px'}} src="https://f.hubspotusercontent00.net/hubfs/53/iconfinder_267_Python_logo_4375050.png" alt="Python" />Python</td>
<td><a href="https://pypi.org/project/hubspot-api-client/">pip install hubspot-api-client</a></td>
<td>[hubspot-api-python]([hubspot-api-nodejs](https://github.com/HubSpot/hubspot-api-nodejs))</td>
</tr>
</tbody>

</Table>

## 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 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

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:<ul><li>`HORIZONTAL`</li><li>`VERTICAL`</li><li>`BOTH`</li></ul> | `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:<ul><li>`select`: renders a dropdown menu. Allows for selecting multiple options when `multiple` is set to `true`. </li><li>`checkbox`: renders a list of selectable checkboxes. Allows for selecting multiple options when `multiple` is set to `true` and `reordering_enabled` is set to `false`.</li><li>`radio`: renders a list of radio button options. Does not allow for selecting multiple options.</li><li>`buttons`: renders a set of buttons based on the specified `preset`. Does not allow for selecting multiple options.</li></ul> | `"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 |
| --- | --- | --- |
| `expand_icon` | `caret` \| `plus` |  |
| `layout` | `cards` \| `tiles` \| `minimal` |  |
| `icon_size` | `small` \| `medium` \| `large` |  |
| `social_icon_size` | `small` \| `medium` \| `large` |  |
| `icon_background_shape` | `none` \| `square` \| `rounded` \| `circle` |  |
| `social_icon_background_shape` | `none` \| `square` \| `rounded` \| `circle` |  |

## 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.<ul><li>`true`: the opacity input is shown.</li><li>`false`: the opacity input is hidden.</li><li>If left undefined, opacity input will not display in email modules, but will display in other module types.</li></ul> | `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".<br />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:<ul><li>`response_type`, which can be one of:<ul><li>`inline:` an inline text message. </li><li>`redirect`: redirect the visitor after submission.</li></ul></li><li>`message`: the text to display after form submit.</li></ul><ul><li>`redirect_id`: for redirected forms, set to a HubSpot content ID to redirect submitters to a HubSpot page.</li><li>`redirect_url`: for redirected forms, set to a specific URL to redirect submitters to a page.</li></ul> |
| `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.<br />**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:<ul><li>`fontawesome-6.4.2`</li><li>`fontawesome-5.14.0`</li><li>`fontawesome-5.0.10`</li></ul> | `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: <ul><li>`size_type`: whether the image is automatically or manually sized:<ul><li>`"auto"`: HubSpot will automatically adjust the size of the image based on its original dimensions.</li><li>`"auto_custom_max"`: HubSpot will automatically adjust the size of the image with maximum dimensions set using the `"max_height"` and `"max_width"` properties.</li><li>`"exact":` HubSpot will size the image based on the dimensions set using the `"height"` and `"width"` properties.</li></ul></li><li>`src`: the URL of the default image. Must be an absolute path to an image.</li><li>`alt`: the image's default alt-text.</li><li>`loading`: the image's [lazy loading options](/guides/cms/content/performance/lazy-loading). Can be set as `"disabled"` (default), `"eager"`, or `"lazy"`.</li></ul> | `{ "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:<ul><li>A `url` object which contains:<ul><li>`content_id`: if linking to HubSpot content, the ID of that content. Set to `null` if linking to external content.</li><li>`type`: the type of URL. Learn more about supported types below.</li><li>`href`: the URL of the content. When linking to HubSpot content, set this to `null` and use `content_id` instead.</li></ul></li></ul> | `{ "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:<ul><li>`EXTERNAL`</li><li>`CONTENT`</li><li>`FILE`</li><li>`EMAIL_ADDRESS`</li><li>`BLOG`</li><li>`CALL_TO_ACTION`</li><li>`PHONE_NUMBER`</li><li>`WHATSAPP_NUMBER`</li><li>`PAYMENT`</li></ul> | `[ "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:<ul><li>`sponsored`: a sponsored link, such as a paid ad link. </li><li>`user_generated_content`: content generated by users, such as forums.</li></ul>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:<ul><li>`"disabled"` (default)</li><li>`"eager"`</li><li>`"lazy"`</li></ul> | `{ 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:<ul><li>`padding`: can contain `top`, `right`, `bottom`, `left` objects</li><li>`margin`: can contain `top` and `bottom` objects</li></ul>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:<ul><li>`padding`: can contain `top`, `right`, `bottom`, `left` objects</li><li>`margin`: can contain `top` and `bottom` objects</li></ul>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:<ul><li>`HORIZONTAL`</li><li>`VERTICAL`</li><li>`BOTH`</li></ul> | `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:<ul><li>`EXTERNAL`: renders a text input field for an external URL.</li><li>`CONTENT`: renders a dropdown menu containing the account's website and landing pages.</li><li>`FILE`: renders a file selector.</li><li>`EMAIL_ADDRESS`: renders a text input field for an email address.</li><li>`BLOG`: renders a dropdown menu containing the account's blog listing pages.</li><li>`PHONE_NUMBER`: renders a text input field for a phone number. The number must start with `+` and contain 7-15 digits (excluding the country code).</li><li>`WHATSAPP_NUMBER`: renders a dropdown menu containing the account's [connected WhatsApp numbers](https://knowledge.hubspot.com/inbox/connect-whatsapp-to-the-conversations-inbox).</li></ul> | `[ "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

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


# HubSpot forms reference

Use [HubSpot forms](/guides/cms/content/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. Below, find reference documentation for forms, including form embed configuration options, internationalization and validation error messages, and form events.
This article deails how to customize your form to add functionality not otherwise provided by the forms tool in HubSpot. If you require a more complex use-case for your form, like sending custom form submission data to HubSpot, you should review the [forms API documentation](/reference/api/marketing/forms/v3-legacy).
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 <u>not</u> 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
<script
  charset="utf-8"
  type="text/javascript"
  src="//js.hsforms.net/forms/embed/v2.js"
></script>
<script>
  hbspt.forms.create({
    region: 'na1',
    portalId: '123456',
    formId: 'df93f0c1-2919-44ef-9446-6a29f9a7f',
  });
</script>
```

| 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
<script
  charset="utf-8"
  type="text/javascript"
  src="//js.hsforms.net/forms/embed/v2.js"
></script>
<script>
  hbspt.forms.create({
    portalId: '',
    formId: '',
    onBeforeFormSubmit: function ($form) {
      // YOUR SCRIPT HERE
    },
  });
</script>
```

| 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)`.<br /><br />It is <u>not</u> recommended to perform a browser redirect in this callback, as it could prevent the form submission. For any custom redirects, use the `onFormSubmitted` callback.<br /><br /> |
| `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.<br /><br />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 <u>not</u> 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:<ul><li>`redirectUrl`: a string containing a redirect URL, if set.</li><li>`submissionValues`: an object containing the submitted values from the form.</li></ul> |

## 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 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<br />
100 $<br />
¥ 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) %}
<ul>
    <li>
        <a href="{{ my_post.absolute_url }}">{{my_post.title}}</a>
    </li>
</ul>
```
```html
<ul>
  <li>
    <a
      href="//www.hubspot.com/blog/articles/kcs_article/email/how-do-i-create-default-values-for-my-email-personalization-tokens"
      >How do I create default values for my email or smart content
      personalization tokens?</a
    >
  </li>
</ul>
```
### 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) %}
<ul>
{% for item in my_tags %}
  <li><a href="{{ blog_tag_url(group.id, item.slug) }}">{{ item }}</a></li>
{% endfor %}
</ul>
```
```html
[Insider]

<ul>
  <li><a href="https://www.ajlaporte.dev/blog/tag/insider">Insider</a></li>
</ul>
```
### 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
<link
  rel="stylesheet"
  href="//cdn2.hubspot.net/hub/327485/hub_generated/style_manager/1431477077901/custom/page/Designers_2015/designers-doc-2015.min.css"
/>
```
### 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
<script
  type="text/javascript"
  src="//cdn2.hubspot.net/hub/327485/hub_generated/style_manager/1431479563436/custom/page/Designers_2015/designer-doc-2105.min.js"
></script>
```
### 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) %}
<ul>
    <li>
        <a href="{{ my_page.absolute_url }}">{{ my_page.title }}</a>
    </li>
</ul>
```
```html
<ul>
  <li>
    <a
      href="//www.hubspot.com/email/how-do-i-create-default-values-for-my-email-personalization-tokens"
      >How do I create default values for my email or smart content
      personalization tokens?</a
    >
  </li>
</ul>
```


# 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 <u>not</u> 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 `&nbsp;` (the second parameter).
```hubl
{% set rows = ["apples", "oranges", "pears", "grapes", "blueberries"] %}

<table>
{% for row in rows|batch(3, " ") %}
   <tr>
    {% for column in row %}
        <td>{{ column }}</td>
    {% endfor %}
    </tr>
{% endfor %}
</table>
```
```html
<table>
  <tbody>
    <tr>
      <td>apples</td>
      <td>oranges</td>
      <td>pears</td>
    </tr>
    <tr>
      <td>grapes</td>
      <td>blueberries</td>
      <td>&nbsp;</td>
    </tr>
  </tbody>
</table>
```
| 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 <u>only</u> 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
<pre>
{% set var = "string to center" %}
before{{ var|center(80) }}after
</pre>
```
```html
<pre>
before                                string to center                                after
</pre>
```
| 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"` &#124; `"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 <br> markup is printed as text" %}
<img src="test.com/imageurl" alt="{{escape_string|escape_attr}}">
```
```html
<img src="test.com/imageurl" alt="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>" %}
 <a href="{{ escape_string|escape_url }}"></a>
```
```html
<a href="http://example.com/with%20space/%3Chtml%3E"></a>
```
## escapejson

Escapes strings so that they can be used as JSON values.
```hubl
{% set escape_string = "<script>alert('oh no!')</script>" %}
{% require_js position="head" %}
<script data-search_input-config="config_{{ name }}" type="application/json">
{
  "autosuggest_results_message": "{{ escape_string|escapejson }}"
}
</script>
{% end_require_js %}
```
```html
<script
  data-search_input-config="config_widget_1234567"
  type="application/json"
>
  {
    "autosuggest_results_message": "<script>alert('oh no!')<\/script>"
  }
</script>
```
## 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 <u>only</u> 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'` &#124; `'medium'` &#124; `'long'` &#124; `'full'` &#124; 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 <u>only</u> 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'` &#124; `'medium'` &#124; `'long'` &#124; `'full'` &#124; 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 <u>only</u> 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'` &#124; `'medium'` &#124; `'long'` &#124; `'full'` &#124; 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
<!-- in the example below
the HubDB Location =
42.3667, -71.1060 (Cambridge, MA) |
Chicago, IL = 37.3435, -122.0344 -->
{{ 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
<ul>
{% for group in contents|groupby("blog_post_author") %}
    <li>{{ group.grouper }}
      <ul>
        {% for content in group.list %}
          <li>{{ content.name }}</li>
        {% endfor %}
      </ul>
    </li>
{% endfor %}
</ul>
```
```html
<ul>
  <li>
    Blog author 1
    <ul>
      <li>Post by Blog author 1</li>
      <li></li>
      <li>Post by Blog author 1</li>
      <li></li>
      <li>Post by Blog author 1</li>
      <li></li>
    </ul>
  </li>
  <li>
    Blog author 2
    <ul>
      <li>Post by Blog author 2</li>
      <li></li>
      <li>Post by Blog author 2</li>
      <li></li>
      <li>Post by Blog author 2</li>
      <li></li>
    </ul>
  </li>
  <li>
    Blog author 3
    <ul>
      <li>Post by Blog author 3</li>
      <li></li>
      <li>Post by Blog author 3</li>
      <li></li>
      <li>Post by Blog author 3</li>
      <li></li>
    </ul>
  </li>
</ul>
```
| 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 `<pre>` 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
<pre>
{% set var = "string to indent" %}
{{ var|indent(2, true) }}
</pre>
```
```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 <u>not</u> 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 }}
</div>
{% 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 %}
<h3>Posts about {{ page_meta.html_title|replace("Blog | ", "") }}</h3>
{% endif %}
```
```html
<h3>Posts about topic name</h3>
```
| 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) &#124; `'ceil'` &#124; `'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
<p>HTML post content that is not escaped.</p>
```
## 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 <u>all</u> HTML. All content is run through `escape_jinjava` as well to prevent nested interpretation.
```hubl
" %}
{{ escape_string|sanitize_html("IMAGES") }}
```
```html
This  markup is <img src="test.com/image"> printed as text.</div>
```
## 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 }}
</div>
{% endfor %}
```
```html

  Post with featured image
</div>
```
| 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 <u>not</u> 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 }}<br>
{% endfor %}
```
```html
A post<br />
B post<br />
C post<br />
D post<br />
E post<br />
```
| 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) %}
<ul>
{% for name in names %}
  <li>{{ name }}</li>
{% endfor %}
</ul>
```
```html
<ul>
  <li>Mark</li>
  <li>Irving</li>
  <li>Helly</li>
  <li>Dylan; Milchick; Harmony;</li>
</ul>
```
| 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 = "<p>I want to truncate this text without breaking my HTML<p>" %}
{{ html_text|truncatehtml(28, "..." , false) }}
```
```html
<p>I want to truncate this..</p>
```
| 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 <u>only</u> 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
<a href="//hubspot.com/">http://hubspot.com/</a>
<a href="//hubspot.com/" rel="nofollow">http://...</a>
<a href="//hubspot.com/" rel="nofollow">http://hubspot.com/</a>
<a href="//hubspot.com/" target="_blank">http://hubspot.com/</a>
```
## 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 `<pre>` 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

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
<a href="{{ blog_all_posts_url("default") }}">All Marketing blog posts</a>
```
```html
<a href="http://www.hubspot.com/marketing/all">All Marketing blog posts</a>
```
| 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
<a href="{{ blog_author_url("default", "brian-halligan") }}">Brian Halligan</a>
```
```html
<a href="http://blog.hubspot.com/marketing/author/brian-halligan"
  >Brian Halligan</a
>
```
| 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) %}
<ul>
{% for author in my_authors %}
<li><a href="{{ blog_author_url(group.id, author.slug) }}">{{ author }}</a></li>
{% endfor %}
</ul>
```
```html
[ Brian Halligan, Dharmesh Shah, Katie Burke, Kipp Bodnar]

<ul>
    <li><a href="http://blog.hubspot.com/marketing/author/brian-halligan">Brian Halligan</a></li></li>
    <li><a href="http://blog.hubspot.com/marketing/author/dharmesh-shah">Dharmesh Shah</a></li></li>
    <li><a href="http://blog.hubspot.com/marketing/author/katie-burke">Katie Burke</a></li></li>
    <li><a href="http://blog.hubspot.com/marketing/author/kipp-bodnar">Kipp Bodnar</a></li></li>
</ul>
```
| 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) %}
<ul>
    <li>
        <a href="{{ my_blog.absolute_url }}">{{my_blog.html_title}}</a>
	</li>
</ul>
```
```html
<ul>
  <li>
    <a href="http://blog.bikinginearnest.com/outdoors"
      >Outdoor biking for the earnest</a
    >
  </li>
</ul>
```
| 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
<a href="{{ blog_page_link(current_page_num) }}">Current page</a>
<a href="{{ blog_page_link(7) }}">Page 7</a>
<a href="{{ blog_page_link(next_page_num) }}">Next</a>
<a href="{{ blog_page_link(current_page_num + 4) }}">Page Plus 4</a>
```
```html
<a href="http://designers.hubspot.com/blog/page/1">Page 1</a>
<a href="http://designers.hubspot.com/blog/page/7">Page 7</a>
<a href="http://designers.hubspot.com/blog/page/2">Next</a>
<a href="http://designers.hubspot.com/blog/page/5">Page Plus 4</a>
```
| 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:<ul><li>`"popular_all_time"` </li><li>`"popular_past_year"`</li><li>`"popular_past_six_months"` </li><li>`"popular_past_month"`</li></ul>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
<a href="{{ blog_post_archive_url("default", 2017, 7, 5) }}">Posts from July 5th, 2017</a>
```
```html
<a href="http://blog.hubspot.com/marketing/archive/2017/07/05"
  >Posts from July 5th, 2017</a
>
```
| 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
<a href="{{ blog_tag_url("default", "inbound-marketing") }}">Inbound Marketing</a>
```
```html
<a href="http://blog.hubspot.com/marketing/tag/inbound-marketing"
  >Inbound Marketing</a
>
```
| 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) %}
<ul>
{% for item in my_tags %}
<li><a href="{{ blog_tag_url(group.id, item.slug) }}">{{ item }}</a></li>
{% endfor %}
</ul>
```
```html
[ Blogging, Inbound Marketing, SEO, Social Media]

<ul>
    <li><a href="http://blog.hubspot.com/marketing/tag/blogging"></a></li></li>
    <li><a href="http://blog.hubspot.com/marketing/tag/inbound-marketing">Inbound Marketing</a></li></li>
    <li><a href="http://blog.hubspot.com/marketing/tag/seo">SEO</a></li>
    <li><a href="http://blog.hubspot.com/marketing/tag/social-media">Social Media</a></li></li>
</ul>
```
| 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')}}
<p {% if color_contrast('rgb(12,31,1)', '#F0F', 'AA') %}style="color: #FF0000;"{% else %}style="color: #00FF00"{% endif %}>Hey there</p>
```
```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) %}
<ul>
    <li>
        <a href="{{ my_content.absolute_url }}">{{my_content.title}}</a>
    </li>
</ul>
```
```html
<ul>
  <li>
    <a
      href="http://www.hubspot.com/blog/articles/kcs_article/email/how-do-i-create-default-values-for-my-email-personalization-tokens"
      >How do I create default values for my email or smart content
      personalization tokens?</a
    >
  </li>
</ul>
```
| 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]) %}
<ul>
{% for content in contents %}
    <li>
        <a href="{{ content.absolute_url }}">{{content.title}}</a>
    </li>
    {% endfor %}
</ul>
```
```html
<ul>
  <li>
    <a
      href="http://www.hubspot.com/blog/articles/kcs_article/email/how-do-i-create-default-values-for-my-email-personalization-tokens"
      >How do I create default values for my email or smart content
      personalization tokens?</a
    >
  </li>
  <li>
    <a
      href="https://blog.hubspot.com/marketing/content-marketing-strategy-guide"
      >Content Marketing Strategy: A Comprehensive Guide for Modern Marketers</a
    >
  </li>
</ul>
```
| 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.
<p>After append:</p>
{% 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.
<p>After append:</p>

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:<ul><li>`eq` (default)</li><li>`neq`</li><li>`lt`</li><li>`lte`</li><li>`gt`</li><li>`gte`</li><li>`is_null`</li><li>`not_null`</li><li>`in`</li><li>`nin`</li><li>`contains` (applicable for multi-valued properties or string properties, e.g., _"firstname\_\_contains=Tim"_)</li></ul>Queries can include the following parameters:<ul><li>`limit`: the maximum number of results returned in the response. For example, `limit=50`.</li><li>`offset`: for paging through the results if the number of returned results is higher than the limit parameter. For example, `offset=51`.</li><li>`orderBy`: order results by a specific property. For example, `orderBy=email`.</li></ul> |
| `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
<!-- by query -->
{% set contact = crm_object("contact", "email=contact@company.com", "firstname,lastname", false) %}
<!-- by id -->
{% 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:<ul><li>`eq` (default)</li><li>`neq`</li><li>`lt`</li><li>`lte`</li><li>`gt`</li><li>`gte`</li><li>`is_null`</li><li>`not_null`</li><li>`in`</li><li>`nin`</li><li>`contains` (applicable for multi-valued properties or string properties, e.g., _"firstname\_\_contains=Tim"_).</li></ul>Queries can include the following parameters:<ul><li>`limit`: the maximum number of results returned in the response. For example, `limit=50`.</li><li>`offset`: for paging through the results if the number of returned results is higher than the limit parameter. For example, `offset=51`.</li><li>`orderBy`: order results by a specific property. For example, `orderBy=email`.</li></ul> |
| `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:<ul><li>`eq` (default)</li><li>`neq`</li><li>`lt`</li><li>`lte`</li><li>`gt`</li><li>`gte`</li><li>`is_null`</li><li>`not_null`</li><li>`in`</li><li>`nin`</li><li>`contains` (applicable for multi-valued properties or string properties, e.g., _"firstname\_\_contains=Tim"_).</li></ul>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
<span
  class="hs-cta-wrapper"
  id="hs-cta-wrapper-ccd39b7c-ae18-4c4e-98ee-547069bfbc5b"
>
  <span
    class="hs-cta-node hs-cta-ccd39b7c-ae18-4c4e-98ee-547069bfbc5b"
    id="hs-cta-ccd39b7c-ae18-4c4e-98ee-547069bfbc5b"
    style="visibility: visible;"
  >
    <a
      id="cta_button_158015_19a9097a-8dff-4181-b8f7-955a47f29826"
      class="cta_button "
      href="//cta-service-cms2.hubspot.com/ctas/v2/public/cs/c/?cta_guid=19a9097a-8dff-4181-b8f7-955a47f29826&placement_guid=ccd39b7c-ae18-4c4e-98ee-547069bfbc5b&portal_id=158015&redirect_url=APefjpH34lXcq1H4FhyHhE3DNeHPNigbBluiv6t07-N80GLkyj2Dn9PizhW8bo4ecrS47RmyslLg7QQKWLG7n2oNkvrumL9dWUjMMEjVYvStZ-IMyulMBvdU8AddI6nFXL0G_VPP_VXmnFi66RKPPjPvx6kbyPO6k566noP4CTZMyADHkGsGBf4S95YdTNtTTFabShT62cVAiV_oWlXbpfPWQc4G3IvkoDoAhmpQVW-ggUcKa4Ft_5A&hsutk=683eeb5b499fdfdf469646f0014603b4&utm_referrer=http%3A%2F%2Fwww.davidjosephhunt.com%2F%3Fhs_preview%3DNVkCLfFf-2857453560&canon=http%3A%2F%2Fwww.davidjosephhunt.com%2F-temporary-slug-63bb220d-eda6-44d0-9738-af928e787237"
      style=""
      cta_dest_link="http://www.hubspot.com/free-trial"
      title="Start a HubSpot trial"
    >
      Start a HubSpot trial
    </a>
  </span>
  <script charset="utf-8" src="//js.hscta.net/cta/current.js"></script>
  <script type="text/javascript">
    hbspt.cta.load(158015, 'ccd39b7c-ae18-4c4e-98ee-547069bfbc5b');
  </script>
</span>
<span
  class="hs-cta-wrapper"
  id="hs-cta-wrapper-ccd39b7c-ae18-4c4e-98ee-547069bfbc5b"
>
  <span
    class="hs-cta-node hs-cta-ccd39b7c-ae18-4c4e-98ee-547069bfbc5b"
    id="hs-cta-ccd39b7c-ae18-4c4e-98ee-547069bfbc5b"
    style="display: block; text-align: center; visibility: visible;"
  >
    <a
      id="cta_button_158015_19a9097a-8dff-4181-b8f7-955a47f29826"
      class="cta_button "
      href="//cta-service-cms2.hubspot.com/ctas/v2/public/cs/c/?cta_guid=19a9097a-8dff-4181-b8f7-955a47f29826&placement_guid=ccd39b7c-ae18-4c4e-98ee-547069bfbc5b&portal_id=158015&redirect_url=APefjpH34lXcq1H4FhyHhE3DNeHPNigbBluiv6t07-N80GLkyj2Dn9PizhW8bo4ecrS47RmyslLg7QQKWLG7n2oNkvrumL9dWUjMMEjVYvStZ-IMyulMBvdU8AddI6nFXL0G_VPP_VXmnFi66RKPPjPvx6kbyPO6k566noP4CTZMyADHkGsGBf4S95YdTNtTTFabShT62cVAiV_oWlXbpfPWQc4G3IvkoDoAhmpQVW-ggUcKa4Ft_5A&hsutk=683eeb5b499fdfdf469646f0014603b4&utm_referrer=http%3A%2F%2Fwww.davidjosephhunt.com%2F%3Fhs_preview%3DNVkCLfFf-2857453560&canon=http%3A%2F%2Fwww.davidjosephhunt.com%2F-temporary-slug-63bb220d-eda6-44d0-9738-af928e787237"
      style=""
      cta_dest_link="http://www.hubspot.com/free-trial"
      title="Start a HubSpot trial"
    >
      Start a HubSpot trial
    </a>
  </span>
  <script charset="utf-8" src="//js.hscta.net/cta/current.js"></script>
  <script type="text/javascript">
    hbspt.cta.load(158015, 'ccd39b7c-ae18-4c4e-98ee-547069bfbc5b');
  </script>
</span>
```
| 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}}
<br>
{% 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 %}

<!-- Need to use split filter as CRM property returns a string that is semi-colon separated -->
{% 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=<true/false>`

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 %}
 <article {{ flag_content_for_access_check(content.id) }} >
...
 </article>
{% endfor %}
```
```html
<article hs-member-content-access="true">...</article>
```
| 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}} <br>
{% endfor %}
```
```html
Cambridge, MA<br />
Salem, MA<br />
San Diego, CA<br />
Chicago, IL<br />
```
| 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 `<script>` tag.
```hubl
{{ get_asset_url("/custom/styles/style.css") }}
```
```html
//cdn2.hubspot.net/hub/1234567/hub_generated/template_assets/1565970767575/custom/styles/style.min.css
```
| Parameter | Type   | Description                                           |
| --------- | ------ | ----------------------------------------------------- |
| `path`    | String | The design manager file path to the template or file. |

## get_public_template_url_by_id

This function works just like `get_public_template_url`, returning public URL of a specified template or code file. The only difference is the parameter of this function is the template ID (available in the URL of the template or coded file), instead of the design manager path.
```hubl
{{ get_public_template_url_by_id("2778457004")  }}
```
```html
//cdn2.hubspot.net/hub/327485/hub_generated/style_manager/1431479563436/custom/page/Designers_2015/designer-doc-2105.min.js
```
| Parameter     | Type | Description                            |
| ------------- | ---- | -------------------------------------- |
| `template_id` | ID   | The ID number of the template of file. |

## hubdb_table

[HubDB](/guides/cms/storage/hubdb/overview) is a feature available in **_Content Hub_** _Professional_ and _Enterprise_.

The `hubdb_table` function can be used to get information on a table including its name, columns, last updated, etc.

The following pieces of information can be pulled by calling the appropriate attributes:

- **ID:** the ID of the table.
- **name:** the name of the table.
- **columns:** a list of column information.
- **created_at:** the timestamp of when this table was first created.
- **published_at:** the timestamp of when this table was published.
- **updated_at:** timestamp of when this table was last updated.
- **row_count:** the number of rows in the table.
```hubl
{% set table_info = hubdb_table(1548215) %}
{{ table_info.row_count }}
```
```html
25
```
This function has a limit of 10 calls per page and per email.
| Parameter  | Type   | Description              |
| ---------- | ------ | ------------------------ |
| `table_id` | String | ID or name of the table. |

## hubdb_table_column

[HubDB](/guides/cms/storage/hubdb/overview) is a feature available in **_Content Hub_** _Professional_ and _Enterprise_.

The `hubdb_table_column` function can be used to get information on a column in table such as its label, type and options. This function accepts two parameters.

These pieces of information about the column can be pulled by calling the appropriate attributes:

- **ID:** the ID of the column.
- **name:** the name of the column.
- **label:** the label to be used for the column.
- **type:** the type of the column.
- **options:** for columns of type `select`, a map of `optionId` to option information.
- **foreignIds:** for columns of type `"foreignId"`, a list of `foreignIds` (with `id` and `name` properties).

In addition to the above attributes, there is also a method which can be called: **`getOptionByName("&lt;option name&gt;")`** whereby for columns of type `"select"`, this will get option information by the option's name.
Column names are not case sensitive. For example, `HS_ID` and `hs_id` are both valid.
```hubl
{% set column_info = hubdb_table_column(123456, 6)  %}
{{ column_info.label }}
```
```html
role
```
| Parameter  | Type   | Description               |
| ---------- | ------ | ------------------------- |
| `table_id` | String | ID or name of the table.  |
| `column`   | String | ID or name of the column. |

## hubdb_table_row

[HubDB](/guides/cms/storage/hubdb/overview) is a feature available in **_Content Hub_** _Professional_ and _Enterprise_.

The `hubdb_table_row` function can be used to pull a single row from a HubDB table. From this row, you can pull information from each table cell by calling on the corresponding attribute:

- **hs_id:** the globally unique id for this row.
- **hs_created_at:** a timestamp representing when this row was created.
- **hs_path:** when used with dynamic pages, this string is the last segment of the url's path for the page.
- **hs_name:** when used with dynamic pages, this is the title of the page.
- &lt;column name&gt; or \["&lt;column name&gt;"\]: get the value of the column for this row by the `name` of the column.
Column names are not case sensitive. For example, `HS_ID` and `hs_id` are both valid.
```hubl
{% set row = hubdb_table_row(1548264, 6726439331)  %}
{{ row.role  }}
```
```html
CTO
```
This function has a limit of 10 calls per page and per email.
| Parameter  | Type    | Description                 |
| ---------- | ------- | --------------------------- |
| `table_id` | String  | ID or name of the table.    |
| `row_id`   | Integer | ID of the row of the table. |

## hubdb_table_rows

[HubDB](/guides/cms/storage/hubdb/overview) is a feature available in **_Content Hub_** _Professional_ and _Enterprise_.

The `hubdb_table_rows` function can be used to list rows of a HubDB table, to be iterated through. A single call of `hubdb_table_rows()` is limited to 10 table scans per page and per email.

By default, this function will return a maximum of 1,000 rows. To retrieve more rows, specify a `limit` in the query, as shown in the code below.
If you use a [random filter](/reference/cms/hubl/filters#random) on this function, the page will be [prerendered](/guides/cms/content/performance/prerendering) periodically. This means that the filtered content will not be updated on every page reload.
```hubl
{% for row in hubdb_table_rows(1546258, "years_at_company__gt=3&orderBy=count&limit=1500") %}
  the value for row {{ row.hs_id }} is {{ row.name }}
{% endfor %}
```
```html
the value for row 6726439331 is Brian Halligan the value for row 8438997836 is
Dharmesh Shah
```
| Parameter | Type | Description |
| --- | --- | --- |
| `table_id` | String | ID or name of the table to query. |
| `query` | String | A query in the same format as a URL query string. If not passed, returns all rows. Learn more about the [available filters](/guides/api/cms/hubdb#filter-returned-rows) for querying HubDB table rows.You can reverse the sort by adding a `-` to the column name: `orderBy=-bar`. You can include this parameter multiple times to sort by multiple columns.In addition to ordering by a column, you can also include the following functions:<ul><li>`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 value of the specified location column are from the provided coordinates.</li><li>`length(column_name)`:takes the name of a column, returns the rows ordered by the length of the column value (calculated as a string)</li><li>`random()`:returns the rows in random order.</li></ul>These functions also support reverse ordering. For example, `orderBy=-geo_distance(location_column,42.37,-71.07)`returns the items that are the farthest away first. |
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=`).
## include_default_custom_css

This function generates a link tag that references the [Primary CSS file](https://knowledge.hubspot.com/design-manager/create-edit-and-attach-css-files-to-style-your-site) (`default_custom_style.min.css`). This file is designed to be a global CSS file that can be added to all templates. To render, the function requires a boolean parameter value of `True`.
```hubl
{{ include_default_custom_css(True) }}
```
```html
<link
  href="//cdn2.hubspot.net/hub/327485/hub_generated/style_manager/1420345777097/custom/styles/default/hs_default_custom_style.min.css"
  rel="stylesheet"
/>
```
## index

Returns the location of the first matching item in a 0-based `array`.

This function accepts 3 parameters, The first parameter is required. The first parameter is the item you are trying to find in the `array`. The second (`start`) and third (`end`) enable you to find that item in a slice of the `array`.
```hubl
{% set shapes = ["triangle","square","trapezoid","triangle"] %}
triangle index: {{shapes.index("triangle")}} <br>
trapezoid index: {{shapes.index("trapezoid")}}
<hr>
adjusted start and end <br>
triangle index: {{shapes.index("triangle",1,5)}}
```
```html
triangle index: 0<br />
trapezoid index: 2
<hr />
adjusted start and end<br />
triangle index: 3
```
## insert

Places an element into a list at the specific index provided.

This function accepts two parameters:

- **Index:** the position where an element is to be inserted.
- **Element:** the item to be inserted.
```hubl
{% set even_numbers = [2,4,8,10]  %}
{% do even_numbers.insert(2,6) %}
{{even_numbers}}
```
```html
[2, 4, 6, 8, 10]
```
## locale_name

Returns a human-readable string representation of a language code, optionally translated to a target language.
```hubl
{{ locale_name("es") }}
{{ locale_name("es", "en") }}
```
```html
Español Spanish
```
| Parameter | Type | Description |
| --- | --- | --- |
| `language_code` | String | The language code. |
| `target_language_code` | String | The language that the output will be translated to. |

## load_translations

Loads translations from a given `_locales` folder path and returns a map of the values.

Learn more about [including field translations in custom modules and themes](/guides/cms/content/multi-language-content#include-field-translations-in-custom-modules-and-themes).
```hubl
{% set template_translations = load_translations('../_locales', 'fr', 'en') %}
{{ partial_footer_address }}
```
```html
123 Rue du Pont, Ville, Région, 12300
```
| Parameter | Type | Description |
| --- | --- | --- |
| `path` | String | The file path to the \_locales directory of the translations. |
| `language_code` | String | The language code. |
| `language_code_fallback` | String | The language code fallback if the specified `language_code` isn't present. |

## menu

Returns the nested link structure of an advanced menu. Menu nodes have a variety of [properties](/reference/cms/hubl/variables#menu-node-variables) that can be used on objects that are returned. If you pass `null` to the menu function it will return an empty pylist. You can also specify a menu by name. In most cases it's safer to use the menu id, as a menu being renamed won't affect the id. If building for the marketplace it makes sense to default to `"default"` if menu is `null`.
This function has a limit of 10 calls per page and per email.
```hubl
{% set node = menu(987) %}
{% for child in node.children %}
	{{ child.label }}<br>
{% endfor %}

{% set default_node = menu("default") %}
{% for child in default_node.children %}
	{{ child.label }}<br>
{% endfor %}
```
```html
page1<br />
page2<br />
page3<br />
```
When using the `menu()` function to generate a menu, [you are fully responsible for making sure your menu is accessible.](/guides/cms/content/accessibility)
| Parameter | Type | Description |
| --- | --- | --- |
| `menu_id` | Id | Required. The id of the menu passed as a number. |
| `root_type` | Enumeration | Root type of the menu (`"site_root"`, `"top_parent"`, `"parent"`, `"page_name"`, `"page_id"`, `"breadcrumb"`).<ul><li>`"site_root"` denotes static - Always show top-level pages in menu.</li><li>`"top_parent"` denotes dynamic by section - Show pages in menu that are related to section being viewed.</li><li>`"parent"` denotes dynamic by page - Show pages in menu that are related to page being viewed.</li><li>`"breadcrumb"` denotes breadcrumb style path menu (uses horizontal flow).</li></ul> |
| `root_key` | String | Root key (id or name) when using `"page_name"` or `"page_id"`. |

## module_asset_url

Gets the URL for an asset attached to a custom module via **Linked Files > Other Files**.
```hubl
{{ module_asset_url("smile.jpg") }}
```
```html
https://cdn2.hubspot.net/hubfs/6178146/logo-black-color.png
```
| Parameter | Type   | Description            |
| --------- | ------ | ---------------------- |
| `name`    | String | The name of the asset. |

## namespace

Creates a namespace object that can hold arbitrary attributes. It can be initialized from a dictionary or with keyword arguments.
```hubl
{% set ns = namespace({"name": "item name", "price":"100"}, b=false) %}
{{ns.name}}, {{ns.b}}
```
```html
item name, false
```
| Parameter | Type | Description |
| --- | --- | --- |
| `dictionary` | Map | The dictionary to initialize with. |
| `kwargs` | String | Keyword arguments to put into the namespace dictionary. |

## oembed

Returns OEmbed data dictionary for given request. Only works in emails.
```hubl
{{ oembed({ url: "https://www.youtube.com/watch?v=KqpFNtbEOh8"}) }}
```
```html
OEmbedResponse{type=video, version=1.0, html=<iframe
  width="480"
  height="270"
  src="https://www.youtube.com/embed/KqpFNtbEOh8?feature=oembed"
  frameborder="0"
  allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
  allowfullscreen
></iframe
>, title=Marketing Is a Marathon — Build a Complete Customer Experience,
authorName=HubSpot, authorUrl=https://www.youtube.com/user/HubSpot,
providerName=YouTube, providerUrl=https://www.youtube.com/,
thumbnailUrl=https://i.ytimg.com/vi/KqpFNtbEOh8/hqdefault.jpg,
thumbnailWidth=480, thumbnailHeight=360}
```
| Parameter | Type | Description |
| --- | --- | --- |
| `request` | String | Request object, `{url: string, max_width: long, max_height: long}`. |

## personalization_token

Returns the value of a contact or contact related property, or a default.
```hubl
Hi {{ personalization_token("contact.firstname", "there") }}!
```
```html
<!-- If the contact is known -->
Hi Dharmesh!

<!-- If the contact is unknown -->
Hi there!
```
| Parameter | Type | Description |
| --- | --- | --- |
| `expression` | String | An expression for the object and property to render. |
| `default` | String | Optional. A default value to use if the expression has no value. |

## pop

Removes the item at the index from the list. Also returns the removed item if printed.
```hubl
{% set even_numbers = [2,3,4,6,8,9,10]  %}
{% do even_numbers.pop(1) %}
{{even_numbers.pop(4)}}
{{even_numbers}}
```
```html
9 [2, 4, 6, 8, 10]
```
## postal_location

The `postal_location` function returns the latitude/longitude location pair for a given postal code and country code (limited to US, CA, and GB).
```hubl
{{ postal_location("02139") }}
{% set location = postal_location("02139", "US") %}
{{ location.latitude }}
{{ location.longitude }}
```
```html
LatLon{latitude=42.3647, longitude=-71.1042} 42.3647 -71.1042
```
This function has a limit of 10 calls per page and per email.
| Parameter | Type | Description |
| --- | --- | --- |
| `postal_code` | String | Postal code of the location. |
| `country_code` | String | Country code for the postal code. If not provided, the country will try to be inferred from the postal code. |

## put

Similar to the [update function](#update), which updates a dict with the elements from another dict object or from an iterable of key-value pairs, except that `put` supports variable names in dicts.
```hubl
{% set dict_var = {"authorName": "Tim Robinson"} %}
{% set key = "key" %}
{% set value = "value" %}
{% do dict_var.put(key, value) %}
{{ dict_var }}
```
```html
{authorName=Tim Robinson, key=value}
```
## range

Returns a list containing an arithmetic progression of integers. With one parameter, range will return a list from 0 up to (but not including) the `value`. With two parameters, the range will start at the first value and increment by 1 up to (but not including) the second `value`. The third parameter specifies the step increment. All values can be negative. Impossible ranges will return an empty list. Ranges can generate a maximum of 1000 values.

Range can be used within a [for loop](/reference/cms/hubl/loops) to specify the number of iterations that should run.
```hubl
{{ range(11) }}
{{ range(5, 11) }}
{{ range(0, 11, 2) }}
{% for number in range(11) %}
{{ number }}
{% endfor %}
```
```html
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] [5, 6, 7, 8, 9, 10] [0, 2, 4, 6, 8, 10] 0 1 2
3 4 5 6 7 8 9 10
```
## require_css

This function enqueues a CSS file to be rendered in the `head` element. All CSS `link` tags are grouped together and render before any JavaScript tags. The HubL is replaced with an empty line and then a `link` tag is added to `{{ standard_header_includes }}`. This method requires an absolute URL; CMS content with a known relative URL can be required by using the `get_asset_url()` function.

To enqueue an inline style to be rendered in the `head` via a `style` tag element, use the `{% require_css %} and {% end_require_css %}` tag instead with your `style` tags and CSS inside of that.

The second parameter is a dictionary of options to modify generated tag. Supports `async` (true/false) [a technique described on web.dev](https://web.dev/articles/defer-non-critical-css#optimize) and any other key-value pair will be added as HTML attributes to the style tag.
```hubl
{{ standard_header_includes }}
<!-- more html -->
{{ require_css("http://example.com/path/to/file.css") }}
{{ require_css(get_asset_url("/relative/path/to/file.css")) }}

<!-- you can tell the browser to load the file asynchronously -->
{{ require_css(get_asset_url("./style.css"), { async: true }) }}
```
```html
<!-- other standard header html -->
<link
  type="text/css"
  rel="stylesheet"
  href="http://example.com/path/to/file.css"
/>
<link
  type="text/css"
  rel="stylesheet"
  href="http://example.com/relative/path/to/file.css"
/>

<!-- you can tell the browser to load the file asynchronously -->
<link
  rel="preload"
  href="//cdn2.hubspot.net/hub/6333443/hub_generated/template_assets/39079350918/1608661506628/example.css"
  as="style"
  onload="this.onload=null;this.rel='stylesheet'"
/>
<noscript
  ><link
    rel="stylesheet"
    href="//cdn2.hubspot.net/hub/6333443/hub_generated/template_assets/39079350918/1608661506628/example.css"
/></noscript>
<!-- more html -->
```
## require_js

Specifies whether a script should be enqueued to render in the head or footer (default). Specify the render location by including the `head` or `footer` parameter. The HubL will be replaced by an empty line, and included in either _the [header or footer includes](/guides/cms/content/templates/types/html-hubl-templates#header-and-footer-includes)._

To enqueue an inline script to be rendered in the footer via a `script` element, wrap your `<script>` tags with `{% require_js %}` and `{% end_require_js %}`.

You can also include additional render options in this function. These will be added as HTML attributes in the script tag. Render options include:

- **position:** `head`/`footer`
- **defer:** `true`/`false`
- **async:** `true`/`false`
- **type:** `string`
```hubl
{{ standard_header_includes }}
<!-- more html -->

{{ require_js("http://example.com/path/to/footer-file.js", "footer") }}
{{ require_js("http://example.com/path/to/head-file.js", "head") }}

<!-- you can add async or defer attributes to the tags that are added. -->
{{ require_js(get_asset_url("./path/to/file.js"), { position: "footer", async: true }) }}
{{ require_js(get_asset_url("./jquery-latest.js"), { position: "footer", defer:true }) }}

{{ standard_footer_includes }}
```
```html
<!-- other standard header html -->
<script
  async
  defer
  src="//cdn2.hubspot.net/hub/6333443/hub_generated/template_assets/37785718838/1605816776975/jquery-latest.min.js"
></script>

<script src="http://example.com/path/to/head-file.js"></script>

<!-- more html -->

<script src="http://example.com/path/to/footer-file.js"></script>

<!-- you can add async or defer attributes to the tags that are added. -->
<script
  async
  src="//cdn2.hubspot.net/hub/######/hub_generated/template_assets/#############/######/jquery-latest.min.js"
></script>
<script
  defer
  src="//cdn2.hubspot.net/hub/######/hub_generated/template_assets/#############/######/jquery-latest.min.js"
></script>

<!-- other standard footer html -->
```
## 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 <u>not</u> 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
<!-- this will cause the page to result in a 404 error -->
```
| 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 `<h3>` 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 %}
    <h3>Table Of Contents</h3>
    {{ super() }}
{% endblock %}
```
```html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>This is the parent template</title>
  </head>
  <body>
    <h3>Table of contents</h3>
    Sidebar content from parent.
  </body>
</html>
```
## 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") %}
<p>{{my_type}}</p>
```
```html
<p>str</p>
```
## 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

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" %}

<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 %}
```

## 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 %}
<h1>This page is under construction.</h1>
<h3>Come back soon!</h3>
{% 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 %}
<a href="http://example.com/some.pdf" {{"download" if dl }} >Download PDF</a>
```

## 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. 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"] %}

<h1>Languages</h1>;
<ul>
    {% for language in languages %}
    <li>{{ language }}</li>
    {% endfor %}
</ul>
```
```html
<h1>Languages</h1>
<ul>
  <li>HTML</li>
  <li>CSS</li>
  <li>Javascript</li>
  <li>Python</li>
  <li>Ruby</li>
  <li>PHP</li>
  <li>Java</li>
</ul>
```
## 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}} <br>
{% endfor %}
```
```html
1. Content <br />
2. Social <br />
3. Contacts <br />
4. Reports <br />
```
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 `<ul>` within a `<ul>` 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"] %}
<ul>
{% for parent in parents %}
<li>{{parent}}<ul>
    {% for child in children %}
    <li>{{child}}</li>
    {% endfor %}
    </ul>
</li>
{% endfor %}
</ul>
```
```html
<ul>
  <li>
    Parent item 1
    <ul>
      <li>Child item 1</li>
      <li>Child item 2</li>
      <li>Child item 3</li>
    </ul>
  </li>

  <li>
    Parent item 2
    <ul>
      <li>Child item 1</li>
      <li>Child item 2</li>
      <li>Child item 3</li>
    </ul>
  </li>

  <li>
    Parent item 3
    <ul>
      <li>Child item 1</li>
      <li>Child item 2</li>
      <li>Child item 3</li>
    </ul>
  </li>
</ul>
```
## 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 }}<br>
{% endfor %}
```
```html
name: Cool Product <br />
price: $20 <br />
size: XL <br />
```
## 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](/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 }}
<!-- 11 + 2 = 13 -->

{{ my_num - my_number }}
<!-- 11 - 2 = 9 -->

{{ my_num / my_number }}
<!-- 11 / 2 = 5.5 -->

{{ my_num % my_number }}
<!-- 11 % 2 = 1 -->

{{ my_num // my_number }}
<!-- 11 // 2 = 5 -->

{{ my_num * my_number }}
<!-- 11 * 2 = 22 -->

{{ my_num ** my_number }}
<!-- 11 ** 2 = 121 -->
```

| 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 }}
<!-- Evaluates to false -->

{{ my_num != my_number }}
<!-- Evaluates to true -->

{{ my_num > my_number }}
<!-- Evaluates to true -->

{{ my_num >= my_number }}
<!-- Evaluates to true -->

{{ my_num < my_number }}
<!-- Evaluates to false -->

{{ my_num <= my_number }}
<!-- Evaluates to false -->
```

| 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" }}
<!-- Evaluates to true -->

Empty string and non-empty string:
{{ "" and "b" }}
<!-- Evaluates to false -->

Two non-zero numbers:
{{ 1 and 2 }}
<!-- Evaluates to true -->

Zero and non-zero number:
{{ 0 and 1 }}
<!-- Evaluates to false -->

Two non-empty lists:
{{ [1] and [2] }}
<!-- Evaluates to true -->

Empty list and non-empty list:
{{ [] and [2] }}
<!-- Evaluates to false -->

Two non-empty dicts:
{{ {a: 1} and {b: 2} }}
<!-- Evaluates to true -->

Empty dict and non-empty dict:
{{ {} and {b: 2} }}
<!-- Evaluates to false -->
```
```hubl
Two non-empty strings:
{{ "a" or "b" }}
<!-- Evaluates to "a" -->

Empty string and non-empty string:
{{ "" or "b" }}
<!-- Evaluates to "b" -->

Two non-zero numbers:
{{ 1 or 2 }}
<!-- Evaluates to 1 -->

Zero and non-zero number:
{{ 0 or 1 }}
<!-- Evaluates to 0 -->

Two non-empty lists:
{{ [1] or [2] }}
<!-- Evaluates to [1] -->

Empty list and non-empty list:
{{ [] or [2] }}
<!-- Evaluates to [2] -->

Two non-empty dicts:
{{ {a: 1} or {b: 2} }}
<!-- Evaluates to {a=1} -->

Empty dict and non-empty dict:
{{ {} or {b: 2} }}
<!-- Evaluates to {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 %}
<!-- Evaluates to Meow -->

Conditional using `not` operator:
{% set string = "strings are a cat's best friend" %}
{% if string is not string_containing "cat" %}
  Meow
{% else %}
  Woof
{% endif %}
<!-- Evaluates to Woof -->

```
```hubl
{% set my_num = 10 %}
{% set my_variable = 5 %}

{{ (my_num + 2) * my_variable }}
<!-- Evaluates to 60 -->
```
```hubl
{% set date = local_dt %}
{{ date ? date : 'No date'}}
<!-- Evaluates to the date variable value -->

{{ todays_date ? todays_date : 'No date'}}
<!-- Evaluates to 'No date' since no value is set for todays_date -->
```
| Symbol | Description |
| --- | --- |
| `and` | Returns `true` if both the left and right operand are truthy. Otherwise, returns `false`. <br /><br />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. <br /><br />This operator is equivalent to `or` in Python and <code>&#124;&#124;</code> 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" }}
<!-- Evaluates to "a" -->

Empty string and non-empty string:
{{ "" or "b" }}
<!-- Evaluates to "b" -->

Defining a fallback value:
{{ some_variable or "default value" }}
<!-- If some_variable is defined, print its value,
otherwise print "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" }}
<!-- Evaluates to true -->

Empty string and non-empty string:
{{ "" and "b" }}
<!-- Evaluates to false -->
```

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] }}
<!-- Evaluates to [2] -->

Empty dict and non-empty dict:
{{ {} and {b: 2} }}
<!-- Evaluates to false -->
```

### 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). |
| <code>&#124;</code> | 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 %}
<style>
{% if widget_data.my_color.color is defined %}
body{
    background: {{ widget_data.my_color.color }};
}
{% else %}
body{
    background: #000;
}
{% endif %}
</style>
```
```html
<style>
  body {
    background: #930101;
  }
</style>
```
### 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 %}
<h3>Available positions</h3>
<ul>
{% for job in jobs %}
    <li>{{ job }}</li>
{% endfor %}
</ul>
{% else %}
<h3>Available position</h3>

{% endif %}
```
```html
<h3>Available positions</h3>
<ul>
  <li>Accountant</li>
  <li>Developer</li>
  <li>Manager</li>
  <li>Marketing</li>
  <li>Support</li>
</ul>
```
### 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 %}
<h3>Favorite genres</h3>
<ul>
{% for genre in genres %}
    <li>{{ genre }}</li>
{% endfor %}
</ul>
{% else %}
<h3>Favorite genre:</h3>

{% endif %}
```
```html
<ul>
  <li>Pop</li>
  <li>Rock</li>
  <li>Disco</li>
  <li>Funk</li>
  <li>Folk</li>
  <li>Metal</li>
  <li>Jazz</li>
  <li>Country</li>
  <li>Hip-Hop</li>
  <li>Classical</li>
  <li>Soul</li>
  <li>Electronica</li>
</ul>
```
### 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 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
<div
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_module widget-type-text widget-type-text"
  data-hs-cos-general-type="widget"
  data-hs-cos-type="module"
  id="hs_cos_wrapper_text"
  style=""
>
  <span
    class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_text"
    data-hs-cos-general-type="widget"
    data-hs-cos-type="text"
    id="hs_cos_wrapper_text_"
    style=""
    >This is a single text line</span
  >
</div>
```
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" %}

<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 %}
```

## 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"] %}

<h1>Types of bears</h1>
<ul>
  {% for bear in bears %}
  <li>{{ bear }}</li>
  {% endfor %}
</ul>
```
```html
<h1>Types of bears</h1>
<ul>
  <li>panda bear</li>
  <li>polar bear</li>
  <li>black bear</li>
  <li>grizzly bear</li>
  <li>koala bear</li>
</ul>
```
## 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 %}
<h3>Sidebar title</h3>
<ul>
  <li>Bullet 1<li>
  <li>Bullet 2<li>
  <li>Bullet 3<li>
</ul>
{% 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

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 %}
  <img src="{{ template_data.quote.associated_objects.company.hs_avatar_filemanager_key }}" width="400" alt="{{ template_data.quote.associated_objects.company.name }}">
{% else %}
  <!-- company does not have an assigned image-->
{% 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 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
<span id="hs_cos_wrapper_module_14162805806361260" class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_follow_me" style="" data-hs-cos-general-type="widget" data-hs-cos-type="follow_me">
    <h2>Follow Us</h2>

</span>
```
## 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
<span
  id="hs_cos_wrapper_crm_slider"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_image_slider"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="image_slider"
>
  <div
    id="hs_cos_flex_slider_crm_slider"
    class="hs_cos_flex-slider flex-slider-main slider-mode-slider"
  >
    <div
      class="hs_cos_flex-viewport"
      style="overflow: hidden; position: relative;"
    >
      <ul
        class="hs_cos_flex-slides hs_cos_flex-slides-main "
        style="width: 800%; -webkit-transition-duration: 0s; transition-duration: 0s; -webkit-transform: translate3d(-1090px, 0px, 0px);"
      >
        <li
          class="hs_cos_flex-slide-main clone"
          aria-hidden="true"
          style="width: 1090px; float: left; display: block;"
        >
          <a href="//www.hubspot.com/" target="_blank"
            ><img
              src="//cdn2.hubspot.net/hubfs/53/Contact-Profile.png?t=1430860504240&t=1430335520686"
              alt="HubSpot CRM Contact Profile"
              draggable="false"
          /></a>

        </li>
        <li
          class="hs_cos_flex-slide-main hs_cos_flex-active-slide"
          style="width: 1090px; float: left; display: block;"
        >
          <a href="//www.hubspot.com/crm" target="_blank"
            ><img
              src="http://go.hubspot.com/hubfs/Contacts-View-1.png?t=1430860504240&t=1430335520686"
              alt="Screenshot of CRM Contacts"
              draggable="false"
          /></a>

        </li>
        <li
          class="hs_cos_flex-slide-main"
          style="width: 1090px; float: left; display: block;"
        >
          <a href="//www.hubspot.com/" target="_blank"
            ><img
              src="//cdn2.hubspot.net/hubfs/53/Contact-Profile.png?t=1430860504240&t=1430335520686"
              alt="HubSpot CRM Contact Profile"
              draggable="false"
          /></a>

        </li>
        <li
          class="hs_cos_flex-slide-main clone"
          aria-hidden="true"
          style="width: 1090px; float: left; display: block;"
        >
          <a href="//www.hubspot.com/crm" target="_blank"
            ><img
              src="http://go.hubspot.com/hubfs/Contacts-View-1.png?t=1430860504240&t=1430335520686"
              alt="Screenshot of CRM Contacts"
              draggable="false"
          /></a>

        </li>
      </ul>
    </div>
    <ol class="hs_cos_flex-control-nav hs_cos_flex-control-paging">
      <li><a class="hs_cos_flex-active">1</a></li>
      <li><a class="">2</a></li>
    </ol>
    <ul class="hs_cos_flex-direction-nav">
      <li><a class="hs_cos_flex-prev" href="#">Previous</a></li>
      <li><a class="hs_cos_flex-next" href="#">Next</a></li>
    </ul>
  </div>
  <script>
    window.hsSliderConfig = window.hsSliderConfig || {};
    window.hsSliderConfig['crm_slider'] = {
      mode: 'slider',
      mainConfig: {
        animationLoop: true,
        direction: 'horizontal',
        slideshowSpeed: 5000.0,
        controlNav: true,
        smoothHeight: false,
        namespace: 'hs_cos_flex-',
        slideshow: true,
        selector: '.hs_cos_flex-slides > li',
        animation: 'slide',
      },
    };
  </script>
</span>
```
| 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 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

    </div>
  </div>
</div>
```
`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 <u>without</u> `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:<br /><ul><li>`TOP`</li><li>`MIDDLE`</li><li>`BOTTOM`</li></ul> |
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
<div
  class="row-fluid-wrapper row-depth-1 row-number-1 unique_name-row-0-background-image dnd-section unique_name-row-0-max-width-section-centering unique_name-row-0-margin unique_name-row-0-padding"
>

  <!--end row-->
</div>
<!--end row-wrapper -->
```
`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:<br /><ul><li>`TOP`</li><li>`MIDDLE`</li><li>`BOTTOM`</li></ul> |
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
<div
  class="span12 widget-span widget-type-cell unique_name-column-1-margin unique_name-column-1-background-color unique_name-column-1-vertical-alignment dnd-column"
  style=""
  data-widget-type="cell"
  data-x="0"
  data-w="12"
></div>
<!--end widget-span -->
```
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:<br /><ul><li>`TOP`</li><li>`MIDDLE`</li><li>`BOTTOM`</li></ul> |
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
<div
  class="row-fluid-wrapper row-depth-1 row-number-1 main-row-0-padding main-row-0-background-color main-row-0-margin"
>

</div>
```
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" %}
    <h1>Hello, world!</h1>
  {% end_module_attribute %}
{% end_dnd_module %}
```
```html
<div
  class="span8 widget-span widget-type-custom_widget"
  style=""
  data-widget-type="custom_widget"
  data-x="0"
  data-w="12"
>
  <div
    id="hs_cos_wrapper_main-module-1"
    class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_module"
    style=""
    data-hs-cos-general-type="widget"
    data-hs-cos-type="module"
  >
    <span
      id="hs_cos_wrapper_module-1_"
      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"
    >
      <h1>Hello, world!</h1>
    </span>
  </div>
</div>
```
## 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.<ul><li>`to bottom`</li><li>`to top`</li><li>`to left`</li><li>`to right`</li></ul> |
| `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:<ul><li>`RGB`</li><li>`RGBA`</li><li>`3 char hex`</li><li>`6 char hex`</li><li>`8 char hex`</li></ul> |

```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.<ul><li>`TOP_LEFT`</li><li>`TOP_CENTER`</li><li>`TOP_RIGHT`</li><li>`MIDDLE_LEFT`</li><li>`MIDDLE_CENTER`</li><li>`MIDDLE_RIGHT`</li><li>`BOTTOM_LEFT`</li><li>`BOTTOM_CENTER`</li><li>`BOTTOM_RIGHT`</li></ul> |
| `backgroundSize` | String | The CSS background size property used for the image.<br />Supported values are:<ul><li>`cover`</li><li>`contain`</li><li>`auto`</li></ul> |
| `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 `</body>` tag on the page in a `<style>` tag.

[Drag and drop styles can also be different at different breakpoints](/guides/cms/content/themes/responsive-breakpoints) to offer a responsive look.


# Creating a related blog post listing with the blog related posts HubL tag

The [related_blog_posts](/reference/cms/hubl/tags/standard-tags#blog-related-posts) HubL tag can be used to create dynamic and related blog post listing based on a variety of parameters. It allows for generating listings of posts across blogs, with the ability to filter by tags, authors, post paths and publish dates. Developers can also specify the HTML output of the module using a macro. This HubL tag can be used on both blog posts and pages. This tutorial will walk through the parameters and usage options for the blog related posts HubL tag.
Please note that the `related_blog_posts` HubL tag does not generate an editable module on the post/page level, it is configured in its entirety with HubL.
## Parameters

The list of posts is generated from a relevancy score based on a comparison of the set parameter values against posts matching these parameters, or relating to the post the HubL tag appears on. None of the parameters are required, however, specifying parameters will allow you to further control which posts are returned. For comma-separated parameters, the more values you set, the more diverse the returned listing will be. The `post_formatter` parameter allows you to specify a [macro](/reference/cms/hubl/variables-macros-syntax#macros) to generate the HTML output of the module. For a full list of parameters and example default HTML output, please see the [related_blog_posts](/reference/cms/hubl/tags/standard-tags#blog-related-posts) spec.

| Parameter | Type | Description | Default |
| --- | --- | --- | --- |
| `limit` | number | The max number of blog posts to list. | `3` |
| `blog_ids` | 'default' or blog id | The ID(s) of a blogs to include posts from. | `none` |
| `tags` | String | The tag(s) that should be used to determine if a post is relevant (comma separated). If a blog post has one of these tags or a similar tag, the post’s relevancy is increased, improving its ranking in the listing. | `none` |
| `blog_authors` | String | The names of authors to include posts from (comma separated) | `none` |
| `blog_post_ids` | String | The ID(s) of a blog posts to use when finding relevant blog posts to list (comma separated). This parameter should only be used when the widget is appearing on pages, as on blog posts, it will default to the post the widget is appearing on. | `none` |
| `post_formatter` | String | The name of a custom macro to render returned blog posts. The macro is passed three parameters which are the blog post object to format, the count in the iteration of blog posts, and the total count of blog posts in the results. If not specified or set to “default”, the built-in formatter will be used to format each post.<br /><br />**Note: It is recommended to use the callback parameter below in place of the ‘post_formatter’ parameter as the HTML of the tag will render more quickly, decreasing page load times.** | `none` |
| `callback` | String | The name of a javascript function to render returned blog posts. The function is passed an array of blog post objects to format. If neither the callback, or post_formatter parameters are specified, the tag will generate HTML in a default format. | `none` |
| `path_prefixes` | String | URL paths or subdirectories to include posts from (comma separated). If a blog post has a similar prefix in its path, the post’s relevancy is increased, improving its ranking in the listing. | `none` |
| `start_date` | date/time | Allows for filtering of posts published after a date/time. | `none` |
| `end_date` | Date/Time | Allows for filtering of posts published before a date/time. | `False` |
| `blog_post_override` | String | The ID(s) of a blog posts which should always show up in the returned listing, despite all other parameter values and filters (comma separated). | `none` |
We strongly recommend using the `callback` parameter instead of the `post_formatter` parameter to ensure faster page load times.
Please note that if the `related_blog_posts` HubL tag is being used on a post, the `blog_post_ids` parameter should not be specified, as on blog posts, it will default to the post the widget is appearing on.
## Example usages of the related_blog_posts HubL tag

Below are some example ways to use this tag to enhance your website.

### Display posts from a specific author across multiple blogs

In this example, we generate a listing of posts written by one of the three specified `blog_authors` across two different blogs.

```hubl
{% related_blog_posts blog_ids="3241539189,3261083894", limit=6, blog_authors="John Smith,Joe Smith,Frank Smith" %}
```

### Display posts with the tag "sales enablement", restricted to a specific publish date time frame

In this example, we generate a listing of 10 posts related to a specific blog post, with the tag "sales enablement", and restricted to a specific publish date time frame. This example specifies the `blog_post_ids` parameter, so it would be used on a page.

```hubl
{% related_blog_posts blog_post_ids="3267910554", limit=10, tags="sales enablement", start_date="2018-02-05", end_date="2018-06-10" %}
```

### Display posts using a JS callback to control HTML output

In this example, we generate a listing of 5 posts using the `callback` parameter to control the HTML output of the post listing. (Instead of the `post_formatter` parameter using a [macro](/reference/cms/hubl/variables-macros-syntax#macros).)

```hubl
{% related_blog_posts limit=5, callback="blog_post_formatter" %}

<script>
  var blog_post_formatter = function(blogposts) {

';

      if (blogpost.featuredImage) {
        formatted += `<img src="${blogpost.featuredImage}" alt="${blogpost.featuredImageAltText}">`;
      }
      formatted += '</div>';
    }
    formatted += '</div>';
    return formatted;
  }
</script>
```


# HubL standard tags

This page is a comprehensive reference guide of the syntax and the available parameters for all standard HubL tags, including [tags for system pages](#system-page-tags), such as the email subscription page. Each tag below contains a sample of the basic syntax, as well as an example with parameters and code output.

If you're building drag and drop areas, learn more about [drag and drop area tags](/reference/cms/hubl/tags/dnd-areas). If you maintain an older website, you may also want to check out the [list of deprecated HubL tags](/reference/cms/hubl/tags/deprecated).
Most of the tags in this page have [default module equivalents.](/reference/cms/modules/default-modules) Modules can be used within [dnd_areas](/guides/cms/content/templates/drag-and-drop/overview) and [flexible columns](#flexible-column), making them more powerful and user friendly than the tags you see here.
## Blog comments

Renders the comments embed code on a blog template, including the comments themselves and the comment form. Learn more about customizing blog comment settings and forms on the [Knowledge Base](https://knowledge.hubspot.com/blog/set-up-and-moderate-your-blog-comments).
```hubl
{% blog_comments "blog_comments" label="Blog comments" select_blog="359485112" %}
```
```html
<span
  id="hs_cos_wrapper_blog_comments"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_blog_comments"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="blog_comments"
>

      <form
        id="hsForm_id"
        method="POST"
        accept-charset="UTF-8"
        enctype="multipart/form-data"
        novalidate=""
        action="https://forms.hsforms.com/submissions/v3/public/submit/formsnext/multipart/{hubID}/{formID}"
        class="hs-form-private hs-form stacked"
        target="target_iframe_07e1eb93-87f2-48039"
        data-instance-id="{formID}"
        data-formid="{formID}"
        data-portal-id="123456"
        data-test-id="hsForm_{formID}"
        data-hs-cf-bound="true"
      >
        <div
          class="hs_firstname hs-firstname hs-fieldtype-text field hs-form-field"
        >
          <label
            id="label-firstname-07e1eb93"
            class=""
            placeholder="Enter your First Name"
            for="firstname-07e1eb9339"
            ><span>First Name</span
            ><span class="hs-form-required">*</span></label
          >
          <legend class="hs-field-desc" style="display: none;"></legend>

        </div>
        <div
          class="hs_lastname hs-lastname hs-fieldtype-text field hs-form-field"
        >
          <label
            id="label-lastname-{formID}"
            class=""
            placeholder="Enter your Last Name"
            for="lastname-{formID}"
            ><span>Last Name</span></label
          >
          <legend class="hs-field-desc" style="display: none;"></legend>

        </div>

        </div>
        <div
          class="hs_website hs-website hs-fieldtype-text field hs-form-field"
        >
          <label
            id="label-website-{formID}"
            class=""
            placeholder="Enter your Website"
            for="website-{formID}"
            ><span>Website</span></label
          >
          <legend class="hs-field-desc" style="display: none;"></legend>

        </div>
        <div
          class="hs_comment hs-comment hs-fieldtype-textarea field hs-form-field"
        >
          <label
            id="label-comment-{formID}"
            class=""
            placeholder="Enter your Comment"
            for="comment-{formID}"
            ><span>Comment</span><span class="hs-form-required">*</span></label
          >
          <legend class="hs-field-desc" style="display: none;"></legend>

        </div>
              <textarea
                id="g-recaptcha-response"
                name="g-recaptcha-response"
                class="g-recaptcha-response"
                style="width: 250px; height: 40px; border: 1px solid rgb(193, 193, 193); margin: 10px 25px; padding: 0px; resize: none; display: none;"
              ></textarea>
            </div>
            <iframe style="display: none;"></iframe>
          </div>
          <input
            type="hidden"
            name="g-recaptcha-response"
            id="hs-recaptcha-response"
            value=""
          />
        </div>

        </div>
        <input name="hs_context" type="hidden" value="{submissionContext}" />
        <iframe name="target_iframe_{formID}" style="display: none;"></iframe>
      </form>
    </div>
  </div>
</span>
```
| Parameter | Type | Description | Default |
| --- | --- | --- | --- |
| `limit` | Number | Sets maximum number of comments. | `5000` |
| `select_blog` | "default" or blog ID | Specifies which blog is connected to the comments embed. This parameter accepts arguments of either `"default"` or a blog ID (available in the URL of the Blog dashboard). If want to use your default blog, this parameter is unnecessary. | `default` |
| `skip_css` | Boolean | Setting this option to True will stop the blog comments CSS from loading. | `false` |
| `message` | String | The message to display when there are no comments. By default, appears as empty (no text displayed). | `""` |

## Blog content

While [drag and drop layouts](/guides/cms/content/templates/types/drag-and-drop-templates) include a blog content module, these modules are not created with a single tag. They instead use conditional logic to define how a blog post and a blog listing should render. [You can learn more about coding blog templates here.](/guides/cms/content/templates/types/blog)

## Blog post filter

Creates a linked listing of posts by topic, posts by month, or posts by author.
This module can only be used in blog post templates.
```hubl
{% post_filter "post_filter" %}
{% post_filter "posts_by_topic"
  select_blog="default",
  expand_link_text="see all",
  overrideable=False,
  list_title="Posts by Topic",
  max_links=5,
  filter_type="topic",
  label="Posts by Topic"
%}
```
```html
<span
  id="hs_cos_wrapper_posts_by_topic"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_post_filter"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="post_filter"
>

  </div>
</span>
```
| Parameter | Type | Description | Default |
| --- | --- | --- | --- |
| `select_blog` | "default" or blog ID | Selects the HubSpot blog to use. This parameter uses either an blog ID or "default" value. | `"default"` |
| `expand_link_text` | String | Text link to display if more posts than max_links number available. Exclude parameter to omit link. | `"see all"` |
| `list_title` | String | List title to display. | `""` |
| `list_tag` | String | Sets the tag used for the list. Value should generally be `"ul"` or `"ol"`. | `"ul"` |
| `title_tag` | String | Sets the tag used for the list title. | `"h3"` |
| `max_links` | Number | Maximum number of filter values to display. Excude parameter to show all. | `5` |
| `filter_type` | Enumeration | Selects the type of filter. Possible values include `"topic"`, `"month"`, and `"author"`. | `"topic"` |

## Blog post Listing

Adds a listing of most popular or top posts.
This tag can only be used in blog post templates. The tag's content is loaded 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 `hsPostListingComplete(feeds)`, where `feeds` is the jQuery selector on all feeds that have been completed. You will want to directly manipulate the DOM object in that function.
```hubl
{% post_listing "post_listing" %}
{% post_listing "top_posts" select_blog="default", label="Recent Posts", overrideable=False, list_title="Recent Posts", listing_type="recent", max_links=5 %}
```
```html
<span
  id="hs_cos_wrapper_module_42751498763_top_posts"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_post_listing"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="post_listing"
>

  </div>
</span>
```
| Parameter | Type | Description | Default |
| --- | --- | --- | --- |
| `select_blog` | "default" or blog ID | Selects the HubSpot blog to use for the listing. This parameter uses either an blog ID or `"default"` value. | `"default"` |
| `list_title` | String | List title to display. | `""` |
| `list_tag` | String | Sets the tag used for the list. Value should generally be `"ul"` or `"ol"`. | `"ul"` |
| `title_tag` | String | Sets the tag used for the list title. | `"h3"` |
| `listing_type` | String | List the blog posts by most recent or most popular in a time range. Possible values include recent, popular_all_time, popular_past_year, popular_past_six_months, and popular_past_month. | `"recent"` |
| `max_links` | Number | Maximum number of blog posts to list. | `5` |
| `include_featured_image` | Boolean | Display featured image along with post link. | `False` |

## Blog related posts

Adds a listing of blog posts based off of a set of parameters shared by posts across blogs. Posts are selected based off of their relevance to the set parameters.

This tag does not generate a page/post-level editable module, it is entirely configured with HubL.
```hubl
{% related_blog_posts
  limit=2,
  blog_ids="1,2",
  tags="Sales enablement,Marketing",
  blog_authors="John Smith,Frank Smith",
  path_prefixes="/business-blog",
  start_date="2018-04-10",
  end_date="2018-04-10",
  blog_post_override="2783035366"
%}
```
```html
<span
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_related_blog_posts"
  data-hs-cos-general-type="widget"
  data-hs-cos-type="related_blog_posts"
  id="hs_cos_wrapper_"
  style=""
  ><!--related-blog-entries--></span
>
    </div>
  </div>
    </div>

  </div>
</div>
```
| Parameter | Type | Description | Default |
| --- | --- | --- | --- |
| `blog_ids` | Number | The IDs of the blogs to include posts from. If not specified, it will include posts from the default blog. |  |
| `blog_post_ids` | String | The IDs of the blog posts to use when finding relevant blog posts to list, separated by commas.Use this parameter only when the widget is appearing on pages. When used on blog posts, it will look for relevant posts based on the currently displaying blog post. |  |
| `blog_post_override` | String | The IDs of the blog posts which should always show up in the returned listing, despite all other parameter values and filters (comma separated). |  |
| `limit` | Number | The maximum number of blog posts to list. | `3` |
| `tags` | String | The blog tags that should be used to determine if a post is relevant (comma separated). Blog posts with the specified tags will rank higher for relevancy. |  |
| `tag_boost` | Number | Increases the weight given to the tags specified in the `tags` parameter to generate related posts. Include this parameter to pull in posts more closely related to the currently displaying or specified posts. Accepts positive numbers. |  |
| `start_date` | datetime | Earliest published date.See an [example below](#examples). |  |
| `end_date` | datetime | Latest published date.See an [example below](#examples). |  |
| `blog_authors` | String | The names of authors to include posts from (comma separated).See an [example below](#examples). |  |
| `path_prefixes` | String | URL paths or subdirectories to include posts from (comma separated). If a blog post has a similar prefix in its path, the post’s relevancy is increased, improving its ranking in the listing. |  |
| `callback` | String | The name of a JavaScript function to render returned blog posts. The function is passed an array of blog post objects to format. If neither the `callback` nor `post_formatter` parameter is specified, the tag will generate HTML in a default format.See an [example below](#examples). | `none` |
| `post_formatter` | String | The name of a custom macro to render returned blog posts. The macro is passed three parameters which are the blogs blog post object to format, the count in the iteration of blog posts, and the total count of blog posts in the results. If not specified or set to `default`, the built-in formatter will be used to format each post. | `default` |
| `allow_any_language` | Boolean | When set to `false`, only posts in the same language as the page the tag is used on will appear. When set to `true`, the language restriction is ignored and all related posts are pulled in regardless of the page language. | `False` |
We strongly recommend using the `callback` parameter instead of `post_formatter` to improve page loading speed.
### Examples

The following example generates a listing of posts written by one of the three specified `blog_authors` across two different blogs:

```hubl
{% related_blog_posts blog_ids="3241539189,3261083894", limit=6, blog_authors="John Smith,Joe Smith,Frank Smith" %}
```

The following example generates a listing of 10 posts related to a specific blog post, with the blog tag "sales enablement," and restricted to a specific publish date time frame. This example specifies the `blog_post_ids` parameter, so it would be used on a page:

```hubl
{% related_blog_posts blog_post_ids="3267910554", limit=10, tags="sales enablement", start_date="2018-02-05", end_date="2018-06-10" %}
```

The following example generates a listing of five posts using the `callback` parameter to control the HTML output of the post listing:

```hubl
{% related_blog_posts limit=5, callback="blog_post_formatter" %}

<script>
  var blog_post_formatter = function(blogposts) {

';

      if (blogpost.featuredImage) {
        formatted += `<img src="${blogpost.featuredImage}" alt="${blogpost.featuredImageAltText}">`;
      }
      formatted += '</div>';
    }
    formatted += '</div>';
    return formatted;
  }
</script>
```

## Blog social sharing

Blog social sharing renders share counters on your blog posts (if enabled in Content Settings).
```hubl
{% blog_social_sharing "blog_social_sharing" %}
{% blog_social_sharing "blog_social_sharing" select_blog="359485112" %}
```
```html
<span
  id="hs_cos_wrapper_blog_social_sharing"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_blog_social_sharing"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="blog_social_sharing"
>

      </li>
      <li
        class="hs-blog-social-share-item hs-blog-social-share-item-google-plus"
      >
        <div
          class="g-plusone"
          data-size="medium"
          data-href="http://designers.hubspot.com/blog"
        ></div>
      </li>
    </ul>
  </div>
</span>
```
| Parameter | Type | Description | Default |
| --- | --- | --- | --- |
| `select_blog` | "default" or blog ID | Species which blog is connected to the share counters. This parameter accepts arguments of either `"default"` or a blog ID (available in the URL of the Blog dashboard). If you want to use your default blog, this parameter is unnecessary. | `default` |
| `downgrade_shared_url` | Boolean | Use HTTP in the url sent to the social media networks. Used to preserve counts when upgrading domains to HTTPS only. | `false` |

## Blog subscription

A blog subscription tag renders the blog subscriber form for a particular blog. This form is automatically created whenever a blog is created in Content Settings, and there is always one subscription form per blog. Please note that the subscribe form's fields are configured within the Forms editor UI.
```hubl
{% blog_subscribe "blog_subscribe" %}
{% blog_subscribe "subscribe_designers_blog" select_blog="default", title="Subscribe to the Designers Blog", response_message="Thanks for Subscribing!", label="Blog Email Subscription", overrideable=False %}
```
```html
<span
  id="hs_cos_wrapper_blog_subscription"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_blog_subscribe"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="blog_subscribe"
>
  <h3
    id="hs_cos_wrapper_blog_subscription_title"
    class="hs_cos_wrapper form-title"
    data-hs-cos-general-type="widget_field"
    data-hs-cos-type="text"
  >
    Subscribe to Designers Blog
  </h3>

  <script charset="utf-8" src="//js.hsforms.net/forms/current.js"></script>
  <script>
    hbspt.forms.create({
      portalId: '327485',
      formId: 'a8d73dc6-0d3a-486d-8938-b19f28b69c3c',
      formInstanceId: '60',
      pageId: 2749976739,
      pageName:
        'Apple, Google & Starbucks: Inside the Web Design Style Guides of 10 Famous Companies',
      redirectUrl:
        'http://designers.hubspot.com/blog/web-design-style-guides-examples?hsFormKey=a56a754bc4c3465015935953363b8ff3#blog_subscription',
      css: '',
      target: '#hs_form_target_blog_subscription',
      formData: { cssClass: 'hs-form stacked' },
    });
  </script>
</span>
```
| Parameter | Type | Description | Default |
| --- | --- | --- | --- |
| `select_blog` | "default" or blog ID | Selects which blog subscription form to render. This parameter accepts arguments of either `"default"` or a blog ID (available in the URL of the Blog dashboard). If want to use your default blog, this parameter is unnecessary. | `default` |
| `title` | String | Defines text in an h3 tag title above the subscribe form. | `"Subscribe Here!"` |
| `no_title` | Boolean | If True, the h3 tag above the title is removed. | `false` |
| `response_message` | String | Defines the inline thank-you message that is rendered when a user submits a form. Supports HTML. | `"Thanks for Subscribing!"` |
| `edit_form_link` | String | This parameter generates a link that allows users to click through to the corresponding Form editor screen. This option will only show in the editor UI if the modules has the parameter overrideable=True.For example, to replace HubID and form ID with the information from the URL of your default blog subscriber form: `edit_form_link=" <ul>\n <li><a href="/forms/HubID/FormID/edit/" target="_blank">Default Blog</a></li> \n</ul> "`.\\n drops the code onto a new line. |  |

## Boolean

A boolean tag creates a checkbox in the UI that prints "true" or "false." In addition to printing the value, this module is useful for defining conditional template logic, when combined with the parameter [`export_to_template_context`](/reference/cms/modules/export-to-template-context).
```hubl
{% boolean "boolean" %}
{% boolean "nav_toggle" label="Hide navigation", value=False, no_wrapper=True %}
```
```html
false
```
| Parameter | Type | Description | Default |
| --- | --- | --- | --- |
| `value` | Boolean | Determines whether the checkbox is checked or unchecked. | `False` |

## Choice

A choice tag creates a dropdown in the content editor UI that prints the value selected by the user. Choice tags are great for giving your users a preset set of options, such as printing the type of page as a page header.

In addition to printing the choice value, this tag is useful for defining conditional template logic, when combined with the parameter [`export_to_template_context`](/reference/cms/modules/export-to-template-context).
```hubl
{% choice "choice" %}
{% choice "type_of_page" label="Choose the type of page", value="About", choices="About, Careers, Contact, Store" %}
```
```html
<span
  id="hs_cos_wrapper_type_of_page"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_choice"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="choice"
>
  About
</span>
```
| Parameter | Type | Description |
| --- | --- | --- |
| `value` | Boolean | The default field value for the dropdown |
| `choices` | Sequence | A comma-separated list of values, or list of value-label pairs. The syntax for value label pairs is as follows: `choices="[[\"value1\", \"Label 1\"], [\"value2\", \"Label 2\"]]"`. The editor will display the label, while it will print the value to the page. |

## Color

The color tag generates a color picker in the page editor UI that prints a HEX color value to a template. Please note that this module can only be used in templates, not CSS files. If using this tag in a `<style>` or inline CSS, you will want to use the `no_wrapper=True` parameter to remove the wrapper `<span>` wrapper.
```hubl
{% color "color" %}
{% color "my_color_picker" label="Choose a color", color="#000000", no_wrapper=True %}
```
```html
#000000
```
| Parameter | Type   | Description                                    |
| --------- | ------ | ---------------------------------------------- |
| `color`   | String | A default HEX color value for the color picker |

## CTA

A Call to Action or CTA tag allows users to add a HubSpot Call to Action button to a predefined area of a page.
```hubl
{% cta "cta" %}
{% cta "my_cta" label="Select a CTA", guid="ccd39b7c-ae18-4c4e-98ee-547069bfbc5b", image_src="https://no-cache.hubspot.com/cta/default/53/c7335b66-a0d4-4d19-82eb-75e1626d02d0.png" %}
```
```html
<span
  id="hs_cos_wrapper_"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_cta"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="cta"
>
  <!--HubSpot Call-to-Action Code -->
  <span
    class="hs-cta-wrapper"
    id="hs-cta-wrapper-ccd39b7c-ae18-4c4e-98ee-547069bfbc5b"
  >
    <span
      class="hs-cta-node hs-cta-ccd39b7c-ae18-4c4e-98ee-547069bfbc5b"
      id="hs-cta-ccd39b7c-ae18-4c4e-98ee-547069bfbc5b"
      style="visibility: visible;"
    >
      <a
        id="cta_button_158015_19a9097a-8dff-4181-b8f7-955a47f29826"
        class="cta_button "
        href="//cta-service-cms2.hubspot.com/ctas/v2/public/cs/c/?cta_guid=19a9097a-8dff-4181-b8f7-955a47f29826&placement_guid=ccd39b7c-ae18-4c4e-98ee-547069bfbc5b&portal_id=158015&redirect_url=APefjpFSOqhysLBE-Yye5WFoP82Swxb2PVWpfI3VxlUjuOCHfiVMcxKic3yM28vuwu9UB8_Jyhk6DGRCEN63hKqQOMtMTGmQZ1UNMK3LtNx0sRrAfQQYna2BfZ9RmgQOs8sKO_PcKOP6G26L5wQ5vdcXXOiMCxFPJxzPzUCcl474iiHKbEo5H8LVtZf6e140VOSGJ37NTpxCcPHLDvH9iFaT6mR0BnKzFReaX0FXBj7Lx2rFLVCZcIC0bdaFEGI1uKOJBMNT9RDtEzeJzUHzFYN0b34uv-ZR4w&hsutk=683eeb5b499fdfdf469646f0014603b4&utm_referrer=http%3A%2F%2Fwww.davidjosephhunt.com%2Fvariables%3Fhs_preview%3D159bC1Cj-2863569740&canon=http%3A%2F%2Fwww.davidjosephhunt.com%2Fvariables"
        style=""
        cta_dest_link="http://www.hubspot.com/free-trial"
        title="Start a HubSpot trial"
      >
        Start a HubSpot trial
      </a>
    </span>
    <script charset="utf-8" src="//js.hscta.net/cta/current.js"></script>
    <script type="text/javascript">
      hbspt.cta.load(158015, 'ccd39b7c-ae18-4c4e-98ee-547069bfbc5b');
    </script>
  </span>
  <!-- end HubSpot Call-to-Action Code -->
</span>
```
| Parameter | Type | Description |
| --- | --- | --- |
| `embed_code` | String | The embed code for the CTA. \\n differentiates line breaks. |
| `full_html` | String | The embed code for the CTA (Same as embed_code). \\n differentiates line breaks. |
| `image_src` | String | Image src url that defines the preview image in the content editor. |
| `image_editor` | String | Markup for the image editor preview |
| `guid` | String | The unique ID number of the CTA. This ID number is available in the URL of the Details screen of a particular CTA. This parameter is used to choose which CTA to display by default. |
| `image_html` | String | CTA image HTML without the CTA script.\* |
| `image_email` | String | Email-friendly version of the CTA code.\* |
\*While these parameters are included here for the sake of being comprehensive, the code generated by HubSpot to populate them is very specific. If you need a default CTA selected, rather than trying to develop the CTA parameters from scratch, it is recommended that set up the CTA on a template layout, and then [clone to file](https://knowledge.hubspot.com/website-and-landing-pages/clone-hubspot-content). You can then copy the HubL CTA module of the CTA with all parameters set correctly for you.

There is also a [CTA function](/reference/cms/hubl/functions#cta) that generates a CTA from the ID.
## Custom HTML

A custom HTML module allows users to enter raw HTML into the content editor. If you need to add extensive default HTML to the tag, you may want to use [block syntax](/reference/cms/modules/using-modules-in-templates#block-syntax).
```hubl
{% raw_html "raw_html" %}
" %}
Block Syntax Example:

{% widget_block raw_html "my_custom_html_module" overrideable=True, label="My custom HTML module"  %}
        {% widget_attribute "value" %}

        {% end_widget_attribute %}
{% end_widget_block %}
```
```html
<span
  id="hs_cos_wrapper_my_custom_html_module"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_raw_html"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="raw_html"
>

</span>
```
| Parameter | Type   | Description                                  |
| --------- | ------ | -------------------------------------------- |
| `value`   | String | Sets the default content HTML of the module. |

## Custom modules

Custom Modules allow HubSpot designers to create a custom group of editable content objects to be used across templates and pages on HubSpot’s CMS, while still allowing marketers to control the specific content appearing within those modules on a page-by-page basis. You can learn more about [custom modules and their simplified HubL syntax, here](/guides/cms/content/modules/overview).

Custom modules must be built in the Custom Module editor, but they can be included into coded templates and HubL modules. You will see a 'Usage Snippet' in the right sidebar of the Custom Module editor under 'Template Usage'.

Custom modules require the ID of the module as a string as well as a path parameter in order to specify which module to load. The usage snippet will also include a label parameter. See the syntax below:
```hubl
{% module "module_15677217712485" path="/Custom/Test custom module"  %}
{% module "module_25642219712432" path="/Assets/Custom calendar module" label="Custom calendar module" %}
```
```html
<div
  id="hs_cos_wrapper_module_15677217712485"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_module"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="module"
>
  content!
</div>
<style>
  @import '//cdn2.hubspotqa.net/qa/hub/126/file-613025667-css/custom_widget_example.css';
</style>

</div>
```
| 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
<span
  id="hs_cos_wrapper_my_flexible_column"
  class="hs_cos_wrapper hs_cos_wrapper_widget_container hs_cos_wrapper_type_widget_container"
  style=""
  data-hs-cos-general-type="widget_container"
  data-hs-cos-type="widget_container"
  ><div
    id="hs_cos_wrapper_rich_text"
    class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_module"
    style=""
    data-hs-cos-general-type="widget"
    data-hs-cos-type="module"
  >
    <span
      id="hs_cos_wrapper_rich_text_"
      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>Something Powerful</h2>
      <h3>Tell The Reader More</h3>
      <p>
        The headline and subheader tells us what you're
        <a href="#">offering</a>, 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.
      </p>
      <p>Remember:</p>
      <ul>
        <li>Bullets are great</li>
        <li>For spelling out <a href="#">benefits</a> and</li>
        <li>Turning visitors into leads.</li>
      </ul></span
    >
  </div>
  <div
    id="hs_cos_wrapper_linked_image"
    class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_module"
    style=""
    data-hs-cos-general-type="widget"
    data-hs-cos-type="module"
  >
    <span
      id="hs_cos_wrapper_linked_image_"
      class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_linked_image"
      style=""
      data-hs-cos-general-type="widget"
      data-hs-cos-type="linked_image"
      ><img
        src="https://static.hubspot.com/final/img/content/email-template-images/placeholder_200x200.png"
        class="hs-image-widget "
        style="width:200px;border-width:0px;border:0px;"
        width="200"
        alt="placeholder_200x200"
        title="placeholder_200x200"
    /></span></div
></span>
```
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"
%}

<!-- Block syntax example -->
    {% 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
<div
  id="hs_cos_wrapper_my_form"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_form"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="form"
>
  <h3
    id="hs_cos_wrapper_module_13885064832664947_title"
    class="hs_cos_wrapper form-title"
    data-hs-cos-general-type="widget_field"
    data-hs-cos-type="text"
  >
    Free Trial
  </h3>

      </div>

      </div>

      </div>

      </div>
    </form>
  </div>
  <script charset="utf-8" src="//js.hsforms.net/forms/current.js"></script>
  <script>
    hbspt.forms.create({
      portalId: '123456',
      formId: '{formID}',
      formInstanceId: '6756',
      pageId: 1,
      redirectUrl: 'http:\/\/www.hubspot.com\/',
      deactivateSmartForm: true,
      css: '',
      target: '#hs_form_target_module_13885064832664947',
      contentType: 'landing-page',
      formData: {
        cssClass: 'hs-form stacked hs-custom-form',
      },
    });
  </script>
</div>
```
| 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
<span
  id="hs_cos_wrapper_page_footer"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_page_footer"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="page_footer"
>
  <footer>
    <span class="hs-footer-company-copyright">© 2020 HubSpot</span>
  </footer>
</span>
```
## 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
<span
  id="hs_cos_wrapper_crm_gallery"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_gallery"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="gallery"
>
  <div
    id="hs_cos_flex_gallery_crm_gallery"
    class="hs_cos_flex-gallery flex-gallery-main gallery-mode-gallery"
  >
    <div
      class="hs_cos_flex-viewport"
      style="overflow: hidden; position: relative;"
    >
      <ul
        class="hs_cos_flex-slides hs_cos_flex-slides-main "
        style="width: 800%; -webkit-transition-duration: 0s; transition-duration: 0s; -webkit-transform: translate3d(-1090px, 0px, 0px);"
      >
        <li
          class="hs_cos_flex-slide-main clone"
          aria-hidden="true"
          style="width: 1090px; float: left; display: block;"
        >
          <a href="//www.hubspot.com/" target="_blank"
            ><img
              src="//cdn2.hubspot.net/hubfs/53/Contact-Profile.png?t=1430860504240&t=1430335520686"
              alt="HubSpot CRM Contact Profile"
              draggable="false"
          /></a>

        </li>
        <li
          class="hs_cos_flex-slide-main hs_cos_flex-active-slide"
          style="width: 1090px; float: left; display: block;"
        >
          <a href="//www.hubspot.com/crm" target="_blank"
            ><img
              src="http://go.hubspot.com/hubfs/Contacts-View-1.png?t=1430860504240&t=1430335520686"
              alt="Screenshot of CRM Contacts"
              draggable="false"
          /></a>

        </li>
        <li
          class="hs_cos_flex-slide-main"
          style="width: 1090px; float: left; display: block;"
        >
          <a href="//www.hubspot.com/" target="_blank"
            ><img
              src="//cdn2.hubspot.net/hubfs/53/Contact-Profile.png?t=1430860504240&t=1430335520686"
              alt="HubSpot CRM Contact Profile"
              draggable="false"
          /></a>

        </li>
        <li
          class="hs_cos_flex-slide-main clone"
          aria-hidden="true"
          style="width: 1090px; float: left; display: block;"
        >
          <a href="//www.hubspot.com/crm" target="_blank"
            ><img
              src="http://go.hubspot.com/hubfs/Contacts-View-1.png?t=1430860504240&t=1430335520686"
              alt="Screenshot of CRM Contacts"
              draggable="false"
          /></a>

        </li>
      </ul>
    </div>
    <ol class="hs_cos_flex-control-nav hs_cos_flex-control-paging">
      <li><a class="hs_cos_flex-active">1</a></li>
      <li><a class="">2</a></li>
    </ol>
    <ul class="hs_cos_flex-direction-nav">
      <li><a class="hs_cos_flex-prev" href="#">Previous</a></li>
      <li><a class="hs_cos_flex-next" href="#">Next</a></li>
    </ul>
  </div>
  <script>
    window.hsSliderConfig = window.hsSliderConfig || {};
    window.hsSliderConfig['crm_gallery'] = {
      mode: 'gallery',
      mainConfig: {
        animationLoop: true,
        direction: 'horizontal',
        slideshowSpeed: 5000.0,
        controlNav: true,
        smoothHeight: false,
        namespace: 'hs_cos_flex-',
        slideshow: true,
        selector: '.hs_cos_flex-slides > li',
        animation: 'slide',
      },
    };
  </script></span
>
```
| 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
<span
  id="hs_cos_wrapper_my_header"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_header"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="header"
>
  <h1>A clear and bold header</h1>
</span>
```
| 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
<span
  id="hs_cos_wrapper_module_42549274798_"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_icon"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="icon"
>
  <svg
    version="1.0"
    xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 448 512"
    aria-hidden="true"
  >
    <g id="layer1">
      <path
        d="M423.9 255.8L411 413.1c-3.3 40.7-63.9 35.1-60.6-4.9l10-122.5-41.1 2.3c10.1 20.7 15.8 43.9 15.8 68.5 0 41.2-16.1 78.7-42.3 106.5l-39.3-39.3c57.9-63.7 13.1-167.2-74-167.2-25.9 0-49.5 9.9-67.2 26L73 243.2c22-20.7 50.1-35.1 81.4-40.2l75.3-85.7-42.6-24.8-51.6 46c-30 26.8-70.6-18.5-40.5-45.4l68-60.7c9.8-8.8 24.1-10.2 35.5-3.6 0 0 139.3 80.9 139.5 81.1 16.2 10.1 20.7 36 6.1 52.6L285.7 229l106.1-5.9c18.5-1.1 33.6 14.4 32.1 32.7zm-64.9-154c28.1 0 50.9-22.8 50.9-50.9C409.9 22.8 387.1 0 359 0c-28.1 0-50.9 22.8-50.9 50.9 0 28.1 22.8 50.9 50.9 50.9zM179.6 456.5c-80.6 0-127.4-90.6-82.7-156.1l-39.7-39.7C36.4 287 24 320.3 24 356.4c0 130.7 150.7 201.4 251.4 122.5l-39.7-39.7c-16 10.9-35.3 17.3-56.1 17.3z"
      ></path>
    </g>
  </svg>
</span>
```
| 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:<ul><li>`fontawesome-5.14.0` </li><li>`fontawesome-5.0.10`</li><li>`fontawesome-6.4.2`</li></ul> |  |
| `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
<span
  id="hs_cos_wrapper_executive_image"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_image"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="image"
>
  <img
    src="//cdn2.hubspot.net/hub/53/file-733888619-jpg/assets/hubspot.com/about/management/brian-home.jpg?width=300"
    class="hs-image-widget "
    width="300"
    alt="Photo of Brian Halligan"
    title="Photo of Brian Halligan"
  />
</span>
```
| 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
<span
  id="hs_cos_wrapper_module_1487954976079503"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_language_switcher"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="language_switcher"
>

  </div>
</span>
```
| Parameter | Type | Description | Default |
| --- | --- | --- | --- |
| `display_mode` | Enumeration | The language of the text in the language switcher. Values are:<ul><li>`Pagelang` means the names of languages will display in the language of the page the switcher is on.</li><li>`Localized` means the name of each language will display in that language.</li><li>`Hybrid` is a combination of the two.</li></ul> | `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
<span
  id="hs_cos_wrapper_executive_image"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_linked_image"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="linked_image"
>
  <a
    href="https://twitter.com/bhalligan"
    target="_blank"
    id="hs-link-executive_image"
    style="border-width:0px;border:0px;"
  >
    <img
      src="//cdn2.hubspot.net/hub/53/file-733888619-jpg/assets/hubspot.com/about/management/brian-home.jpg?width=300"
      class="hs-image-widget "
      style="width:300px;border-width:0px;border:0px;"
      width="300"
      alt="Photo of Brian Halligan"
      title="Photo of Brian Halligan"
    />
  </a>
</span>
```
| 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
<span
  id="hs_cos_wrapper_my_logo"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_logo"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="logo"
>
  <a href="//www.hubspot.com" id="hs-link-my_logo">
    <img
      src="//cdn2.hubspot.net/hub/53/file-1237149549-png/assets/hubspot.com/V2-Global/hubspot-logo-dark.png?t=1430948896766&width=200"
      class="hs-image-widget "
      width="200"
      alt="HubSpot logo"
      title="HubSpo logot"
    />
  </a>
</span>
```
| 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

</span>
```
| 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 `<head>`.

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 }}
<!-- more html -->

{% require_css %}
    <style>
        body {
            color: red;
        }
    </style>
{% end_require_css %}

{{ standard_footer_includes }}
```
```html
<!-- other standard header html -->
<style>
  body {
    color: red;
  }
</style>
<!-- more html -->
<!-- other standard footer html -->
```
## Require_head

A HubL tag that enqueues anything placed inside of it into the `standard_header_includes` which is in the template's `<head>`. 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 %}
  <meta name="third-party-app-verification-id" content="123456">
  <link rel="prefetch" href="http://example.com/large-script.js">
  <!-- these are purely examples, you could add anything that requires being in the head. require_css and require_js should be used instead of this when embedding a style tag or script tag.-->
{% end_require_head %}
```

## Require_js

A HubL tag that enqueues a script element to be rendered. To enqueue a script to render in the `<head />`from a different file via a `<script />` element (as opposed to inline as shown here), use the HubL function [`require_js(absolute_url)`](/reference/cms/hubl/functions#require-js) instead.
```hubl
{{ standard_header_includes }}
<!-- more html -->

{% require_js position="footer" %}
    <script>
        console.log("The CMS is awesome!");
    </script>
{% end_require_js %}

{{ standard_footer_includes }}
```
```html
<!-- other standard header html -->
<!-- more html -->
<script>
  console.log('The CMS is awesome!');
</script>
<!-- other standard footer 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" %}
            <h2>Something Powerful</h2>
            <h3>Tell The Reader More</h3>
            <p>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.</p>
            <p>Remember:</p>
            <ul>
                <li>Bullets are great</li>
                <li>For spelling out <a href="#">benefits</a> and</li>
                <li>Turning visitors into leads.</li>
            </ul>
    {% end_widget_attribute %}
{% end_widget_block %}
```
```html
<span
  id="hs_cos_wrapper_right_column"
  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>Something Powerful</h2>
  <h3>Tell The Reader More</h3>
  <p>
    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.
  </p>
  <p>Remember:</p>
  <ul>
    <li>Bullets are great</li>
    <li>For spelling out benefits and</li>
    <li>Turning visitors into leads.</li>
  </ul>
</span>
```
| Parameter | Type | Description | Default |
| --- | --- | --- | --- |
| `html` | String | Default rich text content for module. | `<h2>Something Powerful</h2>  <h3>Tell The Reader More</h3>  <p>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.</p>  <p>Remember:</p>  <ul><li>Bullets are great</li><li>For spelling out [benefits](#) and</li><li>Turning visitors into leads.</li></ul>` |

## 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
<span
  id="hs_cos_wrapper_module_70642123068_my_rss_listing"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_rss_listing"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="rss_listing"
>
  <h3></h3>
      </div>
    </div>
      </div>
    </div>
      </div>
    </div>
  </div>
</span>
```
| 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 `<p>` 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
<span
  id="hs_cos_wrapper_my_section_header"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_section_header"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="section_header"
>

</span>
```
| 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
<span
  id="hs_cos_wrapper_my_simple_menu"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_simple_menu"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="simple_menu"
>
  <ul></ul>
</span>

<span
  id="hs_cos_wrapper_block_simple_menu"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_simple_menu"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="simple_menu"
>
  <div
    id="hs_menu_wrapper_module_143093417497114626"
    class="hs-menu-wrapper active-branch flyouts hs-menu-flow-horizontal"
    data-sitemap-name=""
  >
    <ul>
      <li class="hs-menu-item hs-menu-depth-1">
        <a href="//www.hubspot.com" target="_self">Home</a>
      </li>
      <li class="hs-menu-item hs-menu-depth-1 hs-item-has-children">
        <a href="//www.hubspot.com/internet-marketing-company" target="_self"
          >About</a
        >
        <ul class="hs-menu-children-wrapper">
          <li class="hs-menu-item hs-menu-depth-2">
            <a href="//www.hubspot.com/company/management" target="_self"
              >Our Team</a
            >
          </li>
        </ul>
      </li>
      <li class="hs-menu-item hs-menu-depth-1">
        <a href="//www.hubspot.com/pricing" target="_self">Pricing</a>
      </li>
    </ul>
  </div>
</span>
```
| 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
<span
  id="hs_cos_wrapper_social_sharing"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_social_sharing"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="social_sharing"
>
  <a
    href="http://www.facebook.com/share.php?u=http%3A%2F%2Fexample.com%2Fmy-page%3Futm_medium%3Dsocial%26utm_source%3Dfacebook"
    target="_blank"
    style="width:24px;border-width:0px;border:0px;"
  >
    <img
      src="//static.hubspot.com/final/img/common/icons/social/facebook-24x24.png?width=24"
      class="hs-image-widget hs-image-social-sharing-24"
      style="max-height:24px;max-width:24px;border-width:0px;border:0px;"
      width="24"
      hspace="0"
      alt="Share on Facebook"
    />
  </a>
  <a
    href="http://www.linkedin.com/shareArticle?mini=true&url=http%3A%2F%2Fexample.com%2Fmy-page%3Futm_medium%3Dsocial%26utm_source%3Dlinkedin"
    target="_blank"
    style="width:24px;border-width:0px;border:0px;"
  >
    <img
      src="//static.hubspot.com/final/img/common/icons/social/linkedin-24x24.png?width=24"
      class="hs-image-widget hs-image-social-sharing-24"
      style="max-height:24px;max-width:24px;border-width:0px;border:0px;"
      width="24"
      hspace="0"
      alt="Share on LinkedIn"
    />
  </a>
  <a
    href="https://twitter.com/intent/tweet?original_referer=http%3A%2F%2Fexample.com%2Fmy-page%3Futm_medium%3Dsocial%26utm_source%3Dtwitter&url=http%3A%2F%2Fexample.com%2Fmy-page%3Futm_medium%3Dsocial%26utm_source%3Dtwitter&source=tweetbutton&text="
    target="_blank"
    style="width:24px;border-width:0px;border:0px;"
  >
    <img
      src="//static.hubspot.com/final/img/common/icons/social/twitter-24x24.png?width=24"
      class="hs-image-widget hs-image-social-sharing-24"
      style="max-height:24px;max-width:24px;border-width:0px;border:0px;"
      width="24"
      hspace="0"
      alt="Share on Twitter"
    />
  </a>
  <a
    href="http://pinterest.com/pin/create/button/?url=http%3A%2F%2Fexample.com%2Fmy-page%3Futm_medium%3Dsocial%26utm_source%3Dpinterest&media=http%3A%2F%2Fcdn1.hubspot.com%2Fhub%2F158015%2F305390_10100548508246879_837195_59275782_6882128_n.jpg"
    target="_blank"
    style="width:24px;border-width:0px;border:0px;"
  >
    <img
      src="//static.hubspot.com/final/img/common/icons/social/pinterest-24x24.png?width=24"
      class="hs-image-widget hs-image-social-sharing-24"
      style="max-height:24px;max-width:24px;border-width:0px;border:0px;"
      width="24"
      hspace="0"
      alt="Share on Pinterest"
  /></a>
  <a
    href="mailto:?subject=Check out http%3A%2F%2Fexample.com%2Fmy-page%3Futm_medium%3Dsocial%26utm_source%3Demail &body=Check out http%3A%2F%2Fexample.com%2Fmy-page%3Futm_medium%3Dsocial%26utm_source%3Demail"
    target="_blank"
    style="width:24px;border-width:0px;border:0px;"
  >
    <img
      src="//static.hubspot.com/final/img/common/icons/social/email-24x24.png?width=24"
      class="hs-image-widget hs-image-social-sharing-24"
      style="max-height:24px;max-width:24px;border-width:0px;border:0px;"
      width="24"
      hspace="0"
      alt="Share on Email"
    />
  </a>
</span>
```
| 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
<span
  id="hs_cos_wrapper_module_spacer"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_space"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="space"
></span>
```
## 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
<span
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_email_simple_subscription"
  data-hs-cos-general-type="widget"
  data-hs-cos-type="email_simple_subscription"
  id="hs_cos_wrapper_email_simple_subscription"
  style=""
></span>

<form
  id="email-prefs-form"
  method="post"
  name="email-prefs-form"
  style="position: relative"
>
  <span
    class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_email_simple_subscription"
    data-hs-cos-general-type="widget"
    data-hs-cos-type="email_simple_subscription"
    id="hs_cos_wrapper_email_simple_subscription"
    style=""
  ></span>

    <span
      class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_email_simple_subscription"
      data-hs-cos-general-type="widget"
      data-hs-cos-type="email_simple_subscription"
      id="hs_cos_wrapper_email_simple_subscription"
      style=""
      ><input
        class="hs-button primary"
        id="submitbutton"
        type="submit"
        value="Unsubscribe"
    /></span>
  </div>
  <span
    class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_email_simple_subscription"
    data-hs-cos-general-type="widget"
    data-hs-cos-type="email_simple_subscription"
    id="hs_cos_wrapper_email_simple_subscription"
    style=""
  ></span>
</form>
```
| 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
<span
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_email_subscriptions"
  data-hs-cos-general-type="widget"
  data-hs-cos-type="email_subscriptions"
  id="hs_cos_wrapper_email_subscriptions"
  style=""
></span>
<form id="email-prefs-form" method="post" name="email-prefs-form">
  <span
    class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_email_subscriptions"
    data-hs-cos-general-type="widget"
    data-hs-cos-type="email_subscriptions"
    id="hs_cos_wrapper_email_subscriptions"
    style=""
  ></span>
        <p>Your email type description</p>
      </div>
    </div>

        <p>Your email type description</p>
      </div>
    </div>

    <input
      class="hs-button primary"
      id="submitbutton"
      type="submit"
      value="Update email preferences"
    />

  </div>
</form>
```
| 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. | `"<p>If this is not your email address, please ignore this page since the email associated with this page was most likely forwarded to you.</p>"` |
| `unsubscribe_single_text` | String | Renders text in a `<p class="header">` above the subscription options. | `"Uncheck the types of emails you do not want to receive:"` |
| `unsubscribe_all_text` | String | Renders text in a `<p class="header">` 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 `<p>` 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
<span
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_email_subscriptions_confirmation"
  data-hs-cos-general-type="widget"
  data-hs-cos-type="email_subscriptions_confirmation"
  id="hs_cos_wrapper_email_subscriptions_confirmation"
  style=""
></span>
```
| 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. | `"<p>If this is not your email address, please ignore this page since the email associated with this page was most likely forwarded to you.</p>"` |
| `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
<span
  id="hs_cos_wrapper_my_login"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_member_login"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="member_login"

  <form
    method="post"
    action="/_hcms/mem/login?domain=default&hs_preview_key=c4H4EEQCL-JSCFBlDzG4wg&portalId=2272014&tc_deviceCategory=desktop&template_file_path=my-test-theme%2Ftemplates%2Fsystem%2Fmembership-login.html&updated=1644243124141&redirect_url=/"
    id="hs-membership-form"
    onsubmit="onFormSubmit()"
    data-hs-do-not-collect=""
  >
    <input name="csrf_token" type="hidden" value="" />
    <input name="redirect_url" type="hidden" value="/" />
    <input
      id="hs-membership-form-hubspotutk"
      name="hubspotutk"
      type="hidden"
      value=""
    />
    </div>
  </form>

  <script type="text/javascript">
    function onFormSubmit() {
      //    document.querySelector('.hs-membership-loader').classList.add('active');
    }

    document.onkeydown = function (e) {
      if (['ArrowUp', 'ArrowDown'].includes(e.code)) {
        var children = [].slice.call(
          document.querySelectorAll(
            '#hs-membership-form input:not([type="hidden"]):not([type="checkbox"]):not([disabled])'
          )
        );
        for (i = 0; i < children.length; i++) {
          var input = children[i];
          if (input === document.activeElement) {
            if (e.code == 'ArrowDown' && children[i + 1] !== undefined) {
              children[i + 1].focus();
              break;
            } else if (e.code == 'ArrowUp' && children[i - 1] !== undefined) {
              children[i - 1].focus();
              break;
            }
          } else if (i + 1 === children.length) {
            children[0].focus();
          }
        }
      } else if ('Enter' === e.code) {
        e.preventDefault();
        var children = [].slice.call(
          document.querySelectorAll(
            '#hs-membership-form input:not([type="hidden"]):not([type="checkbox"]):not([disabled])'
          )
        );
        for (i = 0; i < children.length; i++) {
          var input = children[i];
          if (
            input === document.activeElement &&
            children[i + 1] !== undefined
          ) {
            children[i + 1].focus();
            break;
          } else if (
            input === document.activeElement &&
            i + 1 === children.length
          ) {
            //          document.querySelector('.hs-membership-loader').classList.add('active');
            document.getElementById('hs-membership-form').submit();
          } else if (i + 1 === children.length) {
            children[0].focus();
          }
        }
      }
    };
  </script>
  <script type="text/javascript">
    function show_password(id) {
      var input = document.getElementById(id);
      input.type = input.type === 'password' ? 'text' : 'password';
    }
  </script>
  <script>
    function getCookie(name) {
      var nameEQ = name + '=';
      var ca = document.cookie.split(';');
      for (var i = 0; i < ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) == ' ') c = c.substring(1, c.length);
        if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
      }
      return null;
    }
    document.getElementById('hs-membership-form-hubspotutk').value =
      getCookie('hubspotutk');
  </script>
</span>
```
| 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
<span
  id="hs_cos_wrapper_my_register"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_member_register"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="member_register"

  <form
    method="post"
    action="/_hcms/mem/register?domain=default&hs_preview_key=_zUv_TyY1BCRQ2RviUepiQ&portalId=2272014&tc_deviceCategory=desktop&template_file_path=my-test-theme%2Ftemplates%2Fsystem%2Fmembership-register.html&updated=1644243124290&redirect_url=/_hcms/mem/login?success%3Dtrue"
    id="hs-membership-form"
    onsubmit="onFormSubmit()"
    data-hs-do-not-collect=""
  >
    <input name="csrf_token" type="hidden" value="" />
    <input
      name="redirect_url"
      type="hidden"
      value="/_hcms/mem/login?success=true"
    />
    <input
      id="hs-membership-form-hubspotutk"
      name="hubspotutk"
      type="hidden"
      value=""
    />
    <div
      class="form-input-validation-message"
      id="hs-membership-password-requirements"
    >
      <ul class="no-list hs-error-msgs">
        <li>
          <label
            >Password must be at least 8 characters long and include lower and
            uppercase letters, a number, and a symbol</label
          >
        </li>
      </ul>
    </div>
    </div>
  </form>
  <script type="text/javascript">
    function onFormSubmit() {
      //    document.querySelector('.hs-membership-loader').classList.add('active');
    }

    document.onkeydown = function (e) {
      if (['ArrowUp', 'ArrowDown'].includes(e.code)) {
        var children = [].slice.call(
          document.querySelectorAll(
            '#hs-membership-form input:not([type="hidden"]):not([type="checkbox"]):not([disabled])'
          )
        );
        for (i = 0; i < children.length; i++) {
          var input = children[i];
          if (input === document.activeElement) {
            if (e.code == 'ArrowDown' && children[i + 1] !== undefined) {
              children[i + 1].focus();
              break;
            } else if (e.code == 'ArrowUp' && children[i - 1] !== undefined) {
              children[i - 1].focus();
              break;
            }
          } else if (i + 1 === children.length) {
            children[0].focus();
          }
        }
      } else if ('Enter' === e.code) {
        e.preventDefault();
        var children = [].slice.call(
          document.querySelectorAll(
            '#hs-membership-form input:not([type="hidden"]):not([type="checkbox"]):not([disabled])'
          )
        );
        for (i = 0; i < children.length; i++) {
          var input = children[i];
          if (
            input === document.activeElement &&
            children[i + 1] !== undefined
          ) {
            children[i + 1].focus();
            break;
          } else if (
            input === document.activeElement &&
            i + 1 === children.length
          ) {
            //          document.querySelector('.hs-membership-loader').classList.add('active');
            document.getElementById('hs-membership-form').submit();
          } else if (i + 1 === children.length) {
            children[0].focus();
          }
        }
      }
    };
  </script>

  <script type="text/javascript">
    function show_password(id) {
      var input = document.getElementById(id);
      input.type = input.type === 'password' ? 'text' : 'password';
    }
  </script>
  <script>
    function getCookie(name) {
      var nameEQ = name + '=';
      var ca = document.cookie.split(';');
      for (var i = 0; i < ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) == ' ') c = c.substring(1, c.length);
        if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
      }
      return null;
    }
    document.getElementById('hs-membership-form-hubspotutk').value =
      getCookie('hubspotutk');
  </script>
</span>
```
| 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
<span
  id="hs_cos_wrapper_my_password_reset_request"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_password_reset_request"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="password_reset_request"
  <form
    method="post"
    action="/_hcms/mem/reset/request?domain=default&hs_preview_key=x7aXsEAvlLtUOa1P5t89wQ&portalId=2272014&tc_deviceCategory=desktop&template_file_path=my-test-theme%2Ftemplates%2Fsystem%2Fmembership-reset-password-request.html&updated=1644243124282&redirect_url=/"
    id="hs-membership-form"
    onsubmit="onFormSubmit()"
    data-hs-do-not-collect=""
  >
    <input name="csrf_token" type="hidden" value="" />
    <input name="redirect_url" type="hidden" value="/" />
    </div>
  </form>
  <script type="text/javascript">
    function onFormSubmit() {
      //    document.querySelector('.hs-membership-loader').classList.add('active');
    }

    document.onkeydown = function (e) {
      if (['ArrowUp', 'ArrowDown'].includes(e.code)) {
        var children = [].slice.call(
          document.querySelectorAll(
            '#hs-membership-form input:not([type="hidden"]):not([type="checkbox"]):not([disabled])'
          )
        );
        for (i = 0; i < children.length; i++) {
          var input = children[i];
          if (input === document.activeElement) {
            if (e.code == 'ArrowDown' && children[i + 1] !== undefined) {
              children[i + 1].focus();
              break;
            } else if (e.code == 'ArrowUp' && children[i - 1] !== undefined) {
              children[i - 1].focus();
              break;
            }
          } else if (i + 1 === children.length) {
            children[0].focus();
          }
        }
      } else if ('Enter' === e.code) {
        e.preventDefault();
        var children = [].slice.call(
          document.querySelectorAll(
            '#hs-membership-form input:not([type="hidden"]):not([type="checkbox"]):not([disabled])'
          )
        );
        for (i = 0; i < children.length; i++) {
          var input = children[i];
          if (
            input === document.activeElement &&
            children[i + 1] !== undefined
          ) {
            children[i + 1].focus();
            break;
          } else if (
            input === document.activeElement &&
            i + 1 === children.length
          ) {
            //          document.querySelector('.hs-membership-loader').classList.add('active');
            document.getElementById('hs-membership-form').submit();
          } else if (i + 1 === children.length) {
            children[0].focus();
          }
        }
      }
    };
  </script>
</span>
```
| 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
<span
  id="hs_cos_wrapper_my_password_reset_request"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_password_reset_request"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="password_reset_request"
  <form
    method="post"
    action="/_hcms/mem/reset/request?domain=default&hs_preview_key=x7aXsEAvlLtUOa1P5t89wQ&portalId=2272014&tc_deviceCategory=desktop&template_file_path=my-test-theme%2Ftemplates%2Fsystem%2Fmembership-reset-password-request.html&updated=1644243124282&redirect_url=/"
    id="hs-membership-form"
    onsubmit="onFormSubmit()"
    data-hs-do-not-collect=""
  >
    <input name="csrf_token" type="hidden" value="" />
    <input name="redirect_url" type="hidden" value="/" />
    </div>
  </form>
  <script type="text/javascript">
    function onFormSubmit() {
      //    document.querySelector('.hs-membership-loader').classList.add('active');
    }

    document.onkeydown = function (e) {
      if (['ArrowUp', 'ArrowDown'].includes(e.code)) {
        var children = [].slice.call(
          document.querySelectorAll(
            '#hs-membership-form input:not([type="hidden"]):not([type="checkbox"]):not([disabled])'
          )
        );
        for (i = 0; i < children.length; i++) {
          var input = children[i];
          if (input === document.activeElement) {
            if (e.code == 'ArrowDown' && children[i + 1] !== undefined) {
              children[i + 1].focus();
              break;
            } else if (e.code == 'ArrowUp' && children[i - 1] !== undefined) {
              children[i - 1].focus();
              break;
            }
          } else if (i + 1 === children.length) {
            children[0].focus();
          }
        }
      } else if ('Enter' === e.code) {
        e.preventDefault();
        var children = [].slice.call(
          document.querySelectorAll(
            '#hs-membership-form input:not([type="hidden"]):not([type="checkbox"]):not([disabled])'
          )
        );
        for (i = 0; i < children.length; i++) {
          var input = children[i];
          if (
            input === document.activeElement &&
            children[i + 1] !== undefined
          ) {
            children[i + 1].focus();
            break;
          } else if (
            input === document.activeElement &&
            i + 1 === children.length
          ) {
            //          document.querySelector('.hs-membership-loader').classList.add('active');
            document.getElementById('hs-membership-form').submit();
          } else if (i + 1 === children.length) {
            children[0].focus();
          }
        }
      }
    };
  </script>
</span>
```
| 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
<span
  id="hs_cos_wrapper_password_prompt"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_password_prompt"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="password_prompt"
>
  <form method="post" action="/_hcms/protected/auth">
    <input name="content_id" type="hidden" value="1" />
    <input
      name="redirect_url"
      type="hidden"
      value="https://preview.hs-sites.com/content-rendering/v1/public/_hcms/preview/template/multi"
    />
    <input
      name="password"
      type="password"
      id="hs-pwd-widget-password"
      style="height: 22px; margin-top: -5px"
    />
    <input type="submit" class="hs-button primary large" value="Submit" />
  </form>
  <script type="text/javascript">
    document.getElementById('hs-pwd-widget-password').focus();
  </script>
</span>
```
| 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. | `"<p>Sorry, please try again.</p>"` |
| `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
<span
  id="hs_cos_wrapper_simple_text_field"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_text"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="text"
  >This is the default value for this text field</span
>
```
| 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

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
<!-- defines variable and assigns HEX color -->
{% 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 <your macro name>`
## 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 `<p>` is added into a macro in addition to the two arguments.
```hubl
{% macro render_dialog(title, class) %}

  </div>
 {% endmacro %}

 {% call render_dialog("Hello World", "greeting") %}
     <p>This is a paragraph tag that I want to render in my.</p>
 {% endcall %}
```
```html

</div>
```
## 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
<!-- my_macros.html file -->
{% macro header(tag, title_text) %}
    <header> <{{ tag }}>{{ title_text }} </{{tag}}> </header>
{% endmacro %}

{% macro footer(tag, footer_text) %}
     <footer> <{{ tag }}>{{ footer_text }} </{{tag}}> </footer>
{% 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") }}
<p>Some content</p>
{{ header_footer.footer("h3:", "Company footer info") }}
```
```html
<header><h1>My page title</h1></header>
<p>Some content</p>
<footer><h3>Company footer info</h3></footer>
```
## 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
<footer><h2>My footer info</h2></footer>
```
## 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. [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_ [<code>\|between_times</code>](/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 `<title>` 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 &lt;a&gt;. |

### 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 &lt;head&gt; 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
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

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

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

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

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 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 %}
```


# 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 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

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](/guides/cms/quickstart) in the [CMS development guides](/guides/cms) section.

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


# 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/Headers).

| header | Description |
| --- | --- |
| `accept` | Communicates which content types the client understands, expressed as [MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types). [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept). |
| `accept-encoding` | Communicates the content encoding the client understands. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding). |
| `accept-language` | Communicates which human language and locale is preferred. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language). |
| `cache-control` | Holds directives for caching. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control). |
| `connection` | Communicates whether the network connection stays open. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection). |
| `cookie` | Contains cookies sent by the client. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/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/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/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/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/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 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": "nodejs18.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`Note that HubSpot will [no longer support Node 16](https://developers.hubspot.com/changelog/deprecation-of-node-v16-in-all-serverless-functions) beyond July 12, 2024. |
| `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/Methods) that the endpoint supports. Defaults to [GET](https://developer.mozilla.org/en-US/docs/Web/HTTP/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/Headers).

| header | Description |
| --- | --- |
| `accept` | Communicates which content types expressed as [MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types), the client understands. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept). |
| `accept-encoding` | Communicates the content encoding the client understands. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding). |
| `accept-language` | Communicates which human language and locale is preferred. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language). |
| `cache-control` | Holds directives for caching. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control). |
| `connection` | Communicates whether the network connection stays open. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection). |
| `cookie` | Contains cookies by sent the client. [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/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/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/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/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/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/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/CORS/Errors)
### Get requests

Get requests may be able to make [CORS requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) depending on the client. Do not make [GET](https://developer.mozilla.org/en-US/docs/Web/HTTP/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 | 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'` &#124; `'secondary'` (default) &#124; `'destructive'` | The color variation of the button. |
| `type` | `'button'` (default) &#124; `'reset'` &#124; `'submit'` | The button's HTML `role` attribute. |
| `size` | `'xs'`, `'extra-small'` &#124; `'sm'`, `'small'` &#124; `'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 | 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>
  &lt;/>
  );
});
```

| 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) &#124; `'light'` &#124; `'dark'` &#124; `'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 | 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 (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 | 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 | 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 | 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 | 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 | 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 | 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) &#124; `'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 | 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) &#124; `'subject'` &#124; `'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 | 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 | 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)

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` &#124;`NEQ` &#124; `LT` &#124; `LTE` &#124; `GT` &#124; `GTE` &#124; `BETWEEN` &#124; `IN` &#124; `NOT_IN` &#124; `HAS_PROPTERTY` &#124; `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 &#124; number | The property value to filter by. |
| `values` | String &#124; number | The property values to filter by when using an operator that requires an array, such as `IN`. |
| `highValue` | String &#124; number | The upper value to filter by when using an operator that requires a range, such as `BETWEEN`. |


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

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>
```


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


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


# 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 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. |
| [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 dislplaying 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 | 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>
    &lt;/>
  );
};
```

| 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'` &#124; `'sm'`, `'small'` &#124; `'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 | 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>
    &lt;/>
  );
};
```

| Prop | Type | Description |
| --- | --- | --- |
| `title`  | String | The bolded text of the alert. |
| `variant` | `'info'` (default) &#124; `'tip'` &#124; `'success'` &#124; `'warning'` &#124; `'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 | 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'` &#124; `'center'` &#124; `'end'` &#124; `'baseline'` &#124; `'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'` &#124; `'auto'` (default) &#124; `'none'` &#124; 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 | 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 | 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'` &#124; `'secondary'` (default) &#124; `'destructive'` &#124; `'transparent'` | Sets the color of the button. See [variants section](#variants) for more information. |
| `type` | `'button'` (default) &#124; `'reset'` &#124; `'submit'` | Sets the `role` HTML attribute of the button. |
| `size` | `'xs'`, `'extra-small'` &#124; `'sm'`, `'small'` &#124; `'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)

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)

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)

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 | 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'` &#124; `'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 (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) &#124; `'long'` &#124; `'medium'` &#124; `'standard'` &#124; `'YYYY-MM-DD'` &#124; `L` &#124; `LL` &#124; `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) &#124; `'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 (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` &#124; `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 | 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>
  &lt;/>
  );
};
```

| **Prop** | **Type** | **Description** |
| --- | --- | --- |
| `distance` | `'flush'` &#124; `'extra-small'` &#124; `'small'` (default) &#124; `'medium'` &#124; `'large'` &#124; `'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 (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) &#124; `'secondary'` &#124; `'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'` &#124; `'sm'`, `'small'` &#124; `'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 (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) &#124; `'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)


# 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'` &#124; `'lock'` &#124; `'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 | 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) &#124; `'column'` | Arranges the components horizontally or vertically by setting the main axis. |
| `justify` | `'start'` (default) &#124; `'center'` &#124; `'end'` &#124; `'around'` &#124; `'between'` | Distributes components along the main axis using the available free space. |
| `align` | `'start'` &#124; `'center'` &#124; `'end'` &#124; `'baseline'` &#124; `'stretch'` (default) | Distributes components along the cross-axis using the available free space. |
| `alignSelf` | `'start'` &#124; `'center'` &#124; `'end'` &#124; `'baseline'` &#124; `'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'` &#124; `'nowrap'` (default) | Whether components will wrap rather than trying to fit on one line. |
| `gap` | `'flush'` (default) &#124; `'extra-small'` &#124; `'small'` &#124; `'medium'` &#124; `'large'` &#124; `'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=&#123;'between'&#125;</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=&#123;'around'&#125;</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=&#123;'start'&#125;</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=&#123;'center'&#125;</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=&#123;'end'&#125;</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&amp;height=268&amp;name=ui-extrensions-layout-tile-column-align-start.png"
        />
      </td>
    </tr>
    <tr>
      <td>
        <code>align=&#123;'start'&#125;</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&amp;height=267&amp;name=ui-extrensions-layout-tile-column-align-center.png"
          />
        </p>
      </td>
    </tr>
    <tr>
      <td>
        <code>align=&#123;'center'&#125;</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&amp;height=262&amp;name=ui-extrensions-layout-tile-column-align-end.png"
          />
        </p>
      </td>
    </tr>
    <tr>
      <td>
        <code>align=&#123;'end'&#125;</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 | 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 | 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 | 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 `&nbsp;` 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>
  );
};
```

## Available icons

Below are the currently available icons and their `name` values.

<CardsRow columnSize={140}>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons-add"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons-add.png"
      />
      <code style={{ width: 'fit-content' }}>add</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_33"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_33.png"
      />
      <code style={{ width: 'fit-content' }}>attach</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_bulb"
        src="https://developers.hubspot.com/hs-fs/hubfs/Knowledge_Base_2023_2024/ui-extension-components-icons-bulb-large.png"
      />
      <code style={{ width: 'fit-content' }}>bulb</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_32"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_32.png"
      />
      <code style={{ width: 'fit-content' }}>clock</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_1"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_1.png"
      />
      <code style={{ width: 'fit-content' }}>comment</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_31"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_31.png"
      />
      <code style={{ width: 'fit-content' }}>contact</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_copy"
        src="https://developers.hubspot.com/hs-fs/hubfs/Knowledge_Base_2023_2024/ui-extension-components-icons-copy-large.png"
      />
      <code style={{ width: 'fit-content' }}>copy</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_30"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_30.png"
      />
      <code style={{ width: 'fit-content' }}>dataSync</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_29"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_29.png"
      />
      <code style={{ width: 'fit-content' }}>date</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_28"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_28.png"
      />
      <code style={{ width: 'fit-content' }}>delete</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_27"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_27.png"
      />
      <code style={{ width: 'fit-content' }}>downCarat</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-component-icon-in-button-download"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-component-icon-in-button-download.png"
      />
      <code style={{ width: 'fit-content' }}>download</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_25"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_25.png"
      />
      <code style={{ width: 'fit-content' }}>edit</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_24"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_24.png"
      />
      <code style={{ width: 'fit-content' }}>email</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_23"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_23.png"
      />
      <code style={{ width: 'fit-content' }}>exclamation</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_22"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_22.png"
      />
      <code style={{ width: 'fit-content' }}>faceHappy</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="ui-extension-icon-faceneutral"
        src="https://www.hubspot.com/hubfs/Knowledge_Base_2023_2024/ui-extension-icon-faceneutral.png"
      />
      <code style={{ width: 'fit-content' }}>faceNeutral</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_21"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_21.png"
      />
      <code style={{ width: 'fit-content' }}>faceSad</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_20"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_20.png"
      />
      <code style={{ width: 'fit-content' }}>file</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_home"
        src="https://developers.hubspot.com/hs-fs/hubfs/Knowledge_Base_2023_2024/ui-extension-components-icons-home-large.png"
      />
      <code style={{ width: 'fit-content' }}>home</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_19"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_19.png"
      />
      <code style={{ width: 'fit-content' }}>imageGallery</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_18"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_18.png"
      />
      <code style={{ width: 'fit-content' }}>left</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_17"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_17.png"
      />
      <code style={{ width: 'fit-content' }}>location</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        style={{ width: '100px' }}
        alt="uie-component-icon-in-button-notification2"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-component-icon-in-button-notification2.png"
      />
      <code style={{ width: 'fit-content' }}>notification</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        style={{ width: '100px' }}
        alt="uie-component-icon-in-button-question"
        src="https://developers.hubspot.com/hs-fs/hubfs/Knowledge_Base_2023_2024/ui-extension-components-icons-question-large.png"
      />
      <code style={{ width: 'fit-content' }}>question</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_15"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_15.png"
      />
      <code style={{ width: 'fit-content' }}>refresh</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_14"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_14.png"
      />
      <code style={{ width: 'fit-content' }}>remove</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_13"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_13.png"
      />
      <code style={{ width: 'fit-content' }}>reports</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_12"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_12.png"
      />
      <code style={{ width: 'fit-content' }}>right</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_11"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_11.png"
      />
      <code style={{ width: 'fit-content' }}>robot</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_10"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_10.png"
      />
      <code style={{ width: 'fit-content' }}>save</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_9"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_9.png"
      />
      <code style={{ width: 'fit-content' }}>search</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_settings"
        src="https://developers.hubspot.com/hs-fs/hubfs/Knowledge_Base_2023_2024/ui-extension-components-icons-settings-large.png"
      />
      <code style={{ width: 'fit-content' }}>settings</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_8"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_8.png"
      />
      <code style={{ width: 'fit-content' }}>shoppingCart</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_7"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_7.png"
      />
      <code style={{ width: 'fit-content' }}>star</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_6"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_6.png"
      />
      <code style={{ width: 'fit-content' }}>success</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_5"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_5.png"
      />
      <code style={{ width: 'fit-content' }}>upCarat</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_4"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_4.png"
      />
      <code style={{ width: 'fit-content' }}>upload</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_3"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_3.png"
      />
      <code style={{ width: 'fit-content' }}>video</code>
    </div>
  </Card>
  <Card>
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        flexDirection: 'column',
        height: '100%',
        alignItems: 'center',
      }}
    >
      <img
        alt="uie-components-icons_2"
        src="https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/uie-components-icons_2.png"
      />
      <code style={{ width: 'fit-content' }}>warning</code>
    </div>
  </Card>
</CardsRow>

## Guidelines

Always pair icons with text. If that is not possible, include the `screenReaderText` prop to convey the icon's meaning for users with screen readers.

## Related components

- [Alert](/reference/ui-components/standard-components/alert)
- [Tag](/reference/ui-components/standard-components/tag)
- [Text](/reference/ui-components/standard-components/text)


# 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>
    &lt;/>
  );
};
```

## Related components

- [Link](/reference/ui-components/standard-components/link)
- [Text](/reference/ui-components/standard-components/text)
- [Heading](/reference/ui-components/standard-components/heading)


# 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) &#124; `'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 | 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) &#124; `'light'` &#124; `'dark'` &#124; `'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 | 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) &#124; `'unordered-styled'` &#124; `'ordered'` &#124; `'ordered-styled'` &#124; `'inline'` &#124; `'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 (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 | 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'` &#124; `'sm'`, `'small'` (default) &#124; `'md'`, `'medium'` | The size of the spinner. |
| `layout` | `'inline'` (default) &#124; `'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 | 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) &#124; `'medium'`, `'md'` &#124; `'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) &#124; `'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 | 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 &#124; 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 &#124; 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 | 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 &#124; 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'` &#124; `'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 | 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>
    &lt;/>
  );
};
```

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) &#124; `'md'`, `'medium'` &#124; `'lg'`, `'large'` | The width of the panel. |
| `title` | String | The text that displays at the top of the panel. |
| `variant` | `'modal'` &#124; `'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>
    &lt;/>
  );
};
```
- 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 | 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) &#124; `'warning'` &#124; `'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 | 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>
    &lt;/>
  );
}
```

| Prop | Type | Description |
| --- | --- | --- |
| `name` | String | The input's unique identifier. |
| `value` | String &#124; 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'` &#124; `'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 | 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 &#124; number &#124; boolean | The value of the input. |
| `variant` | `input` (default) &#124; `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 | 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 (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) &#124; `'info'` &#124; `'danger'` &#124; `'warning'` &#124; `'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 | 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'`, &#124; `'sm'`, `'small'` (default) &#124; `'md'`, `'medium'` &#124; `'lg'`, `'large'` &#124; `'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) &#124; `'vertical'` | The orientation of the indicator. |
| `stepNames`  | Array | An array containing the name of each step. |
| `variant` | `'flush'` &#124; `'default'` (default) &#124; `'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 | 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) &#124; `'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 (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'` &#124; `'left'` &#124; `'right'` | Sets the alignment of a table header. |
| `sortDirection` | `'none'` (default) &#124; `'ascending'` &#124; `'descending'` &#124; `'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 &#124; `'min'` &#124; `'max'` &#124; `'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'` &#124; `'left'` &#124; `'right'` | Sets the alignment of a table cell. |
| `width` | Number &#124; `'min'` &#124; `'max'` &#124; `'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) &#124; `'ascending'` &#124; `'descending'` | Visually indicates with an arrow which way the rows are sorted. |
| `onSortChange` | (value: "none" &#124; "ascending" &#124; "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 | 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) &#124; `'warning'` &#124; `'success'` &#124; `'error'` &#124; `'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 | 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'` &#124; `'horizontal'` &#124; `'both'` (default) &#124; `'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 | 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>
    &lt;/>
  );
};
```

| 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) &#124; `'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 &#124; 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 | 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>
  &lt;/>
  );
};
```

| 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 | 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'` &#124; `'checkboxList'` (default) | The type of toggle, whether checkboxes or radio buttons. Radio buttons only allow one option to be selected. |
| `variant` | `'default'` (default) &#124; `'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=&#123;true&#125;</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 | 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'` &#124; `'sm'`, `'small'` &#124; `'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) &#124; `'top'` &#124; `'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 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`. |
| `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. 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
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.

```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"
    }
  ]
}
```

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

## 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.
1.  All of the events for that event definition will be deleted and unrecoverable.
2.  Previously deleted `eventName`'s cannot be used again.
## 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.

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


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 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
    }
  ]
}
```


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


# 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' }; }"
    }
  ]
}
```


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


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.


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


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


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


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


# 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 /> |


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


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


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 (BETA)
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"],
    "allowConversationStart": false,
    "allowMultipleRecipients": false,
    "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 | An optional 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. | `[]` |
| `allowConversationStart` | Boolean | Whether your channel allows HubSpot users to initiate conversations with outgoing messages. | `false` |
| `allowMultipleRecipients` | Boolean | Whether your channel allows messages to be sent with multiple recipients. | `false` |
| `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 | An optional 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`.


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


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


# 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);
  });
};
```


# 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.
}
```


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"
    }
  ]
}
```


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  |


# 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
// Example POST request body
{
  "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
// Example response
{
  "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/objects/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
// Example request body
{
  "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
// Example response
{
  "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
// Example response
{
  "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
// Example search request body
{
  "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
// Example response
{
  "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
// Example response
{
  "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
// Example request body
{
  "properties": {
    "hs_external_status": "fulfilled"
  }
}
```

The response will include a set of default properties along with the property that you just set.

```json
// Example response
{
  "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
// Example response
{
  "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. |


# 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/objects/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/objects/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/use-multiple-currencies-with-stripe#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/objects/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). |


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


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


# 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
// Example response
{
  "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
// Example response
{
  "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
// Example search request body
{
  "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/objects/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
// Example response
{
  "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/objects/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
// Example request body
{
  "inputs": [{ "id": "1526712436" }, { "id": "1526712437" }],
  "properties": ["name", "amount"]
}
```

The response would be formatted as follows:

```json
// Example response
{
 "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"
}
```


# 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
// Example POST request body
{
  "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
// Example response
{
  "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/objects/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
// Example request body
{
  "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
// Example response
{
  "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
// Example response
{
  "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
// Example search request body
{
  "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
// Example response
{
  "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
// Example response
{
  "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
// Example request body
{
  "properties": {
    "hs_shipping_tracking_number": "123098521091"
  }
}
```

The response will include a set of default properties along with the property that you just set.

```json
// Example response
{
  "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
// Example response
{
  "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
// Example request body
{
  "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
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
// Example response
{
  "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
// Example response
{
  "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
// Example search request body
{
  "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/objects/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
// Example response
{
  "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
// Example response
{
  "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
}
```


# 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/objects/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/objects/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/objects/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
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
// Example response
{
  "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
// Example response
{
  "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
// Example search request body
{
  "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/objects/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
// Example response
{
  "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/objects/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
// Example request body
{
  "inputs": [{ "id": "1459694380" }, { "id": "1459694381" }],
  "properties": ["name", "amount"]
}
```

The response would be formatted as follows:

```json
// Example response
{
  "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"
}
```


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


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


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


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


# 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
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
}
```


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


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


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


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


# 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 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**.


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


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


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"
}
```


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

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 starting May 30th, 2025. 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.
## 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

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
}
```


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


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.

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

## Secondary emails

[Secondary email addresses](https://knowledge.hubspot.com/records/add-multiple-email-addresses-to-a-contact) are used when a contact has more than one email. Additional emails are still unique identifiers for contacts, so multiple contacts cannot have the same secondary email address.

- To view secondary 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.
- To add secondary emails to a contact, when creating or editing the contact, in your request body, include emails in the `hs_additional_emails` field. Multiple emails can be separated by a semi-colon.
If you're using the V1 contact APIs, learn how to use the secondary email endpoints in [this reference guide](/reference/api/crm/objects/contacts/v1).
## 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).


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)


# 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"
}
```


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


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
}
```


In HubSpot, [leads](https://knowledge.hubspot.com/prospecting/set-up-leads) 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/prospecting/set-up-leads).

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/crm-setup/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.


# Line Items
In HubSpot, line items are individual instances of [products](/guides/api/crm/objects/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/objects/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
// Example POST request body to https://api.hubapi.com/crm/v3/objects/line_item
{
  "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
// PATCH request to https://api.hubapi.com/crm/v3/objects/line_item/{lineItemId}
{
  "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). |
| `currency` | 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).


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/objects/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 assocatied 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.


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


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.


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
    }
  ]
}
```


# 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": ""}}`.


# 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"]
    }
  ]
}
```


# 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

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

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/objects/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/objects/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.

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.

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"
    }
  ]
}
```


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


# 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"
}
```


## 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 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"
}
```
If you purchased the [business units add-on](https://knowledge.hubspot.com/branding/manage-brands-with-business-unit), you must include the `businessUnitId` field in the request body. You can get a list of business units in your account using the [business units 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 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).


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


# 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

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[] |


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`


# 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 types represent the lawful basis to communicate with your contacts through email. Contacts can [manage their email preferences](https://knowledge.hubspot.com/articles/kcs_article/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 [business units](https://knowledge.hubspot.com/branding/associate-your-assets-with-business-units).

## 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 [Business Units add-on](/guides/api/settings/business-units-api), you can filter subscription types by business unit by including the `businessUnitId` as a query parameter in your request. The default _Account_ business unit ID 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 [Business Units add-on](https://knowledge.hubspot.com/branding/manage-brands-with-business-unit), you'll see the `wideStatusType: "BUSINESS_UNIT_WIDE"` field in the response. Note that the default _Account_ business unit 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 [Business Units add-on](https://knowledge.hubspot.com/branding/manage-brands-with-business-unit), you'll see the `wideStatusType: "BUSINESS_UNIT_WIDE"` field in the response. Note that the default _Account_ business unit 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/&#123;subscriberIdString&#125;`. |
| `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 [Business Units add-on](https://knowledge.hubspot.com/branding/manage-brands-with-business-unit), you'll also need to include the `businessUnitId` query parameter in your request. Note that the _Account_ business unit ID 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 Business Units add-on, the contact will be unsubscribed from all email from the business unit specified in your request, but will still be eligible to receive email from other business units 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 [Business Units add-on](https://knowledge.hubspot.com/branding/manage-brands-with-business-unit), 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 business units.
- `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 Business Units add-on, you can include the `businessUnitId` query parameter to specify which business unit 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 [Business Units add-on](https://knowledge.hubspot.com/branding/manage-brands-with-business-unit), you can include this parameter to specify which business unit 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"
}
```


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


# Business Units
The following endpoint provides information about [business units](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 business units API currently only supports retrieving business unit data and does <u>not</u> support associating assests with a business unit, nor creating a new business unit.
## Get business units tied to a user

To get the business units 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 should include:

```json
// Example GET request to /business-units/v3/business-units/user/{userId}
{
  "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 business units API, click the **Endpoints** tab at the top of this article.


# 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

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]
      }
    }
  }
}
```


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"
  }
]
```


# 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

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)


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


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

The automatic deactivation process will be mandatory on April 7, 2025. If you want to opt-in to this feature proactively, you can opt your account into the beta by following the instructions in [this Knowledge Base article](https://knowledge.hubspot.com/account-management/opt-your-hubspot-account-into-a-public-beta-feature).

---

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


# 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

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 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 |
| `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/objects/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) |


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


# Receive calls in HubSpot when using calling apps
Learn how to [ungate your account](/guides/apps/extensions/calling-extensions/receive-incoming-calls#ungate-your-account) for this beta. By using these instructions you agree to adhere to [HubSpot's Developer Terms](https://legal.hubspot.com/hs-developer-terms) & [HubSpot's Developer Beta Terms](https://legal.hubspot.com/hubspot-beta-terms).
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).


# 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

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 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/sales/calling/aircall), [CloudFiles](https://ecosystem.hubspot.com/marketplace/apps/sales/sales-enablement/cloudfiles), [Reveal](https://ecosystem.hubspot.com/marketplace/apps/sales/partner-relationship-management/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 a publicly available (no sign-in, no paywall) setup documentation specific to your HubSpot integration.
  - Your Setup Guide <u>cannot</u> be your homepage or a general knowledge base. Instead, it must contain the steps to install and configure the integration.
  - For an 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.

## 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://developers.hubspot.com/contact-our-partnerships-team)


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


# 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" which contains a subdomain and leads directly to the page hosting documentation for your app. This link must not lead to your homepage.
- 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)


# 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.
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.
In this article:

- [Create and submit an app listing](#create-and-submit-an-app-listing)
- [Create and update a localized listing for an existing app listing](#create-and-update-a-localized-listing-for-an-existing-app-listing)
- [Edit a live app listing](#edit-a-live-app-listing)
- [Unpublish a live app listing](#unpublish-a-live-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 five tabs of information to fill out.

## Listing info

On the _Listing info_ tab:

- In the _App information_ section, add your **Public app name**, **Company name**, **Tagline**, and **Install Button URL**. For accounts created after March 5th, 2025, you will need to select the Install Button URL from the Redirect URLs set in [app settings](/guides/apps/public-apps/overview#create-a-public-app).

- 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, unpixellated 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.

## 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. You can create listings in other languages even if your app is not available in that language.

- 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.
- All parts of your localized listing should be in the same language, including product screenshots and demo videos.
- Updating your primary listing does not automatically update your localized listings. Each 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.


# 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 ocurred 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.
  - **Acitivity:** 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)


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


# 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

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

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

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


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


# 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

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.


# 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

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

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


# 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/objects/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/objects/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
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
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

[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
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

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

## 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)
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
[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
[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 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 }}</title>
    <meta name="description" content="{{ content.meta_description }}" />
    {{ standard_header_includes }}
  </head>
  <body>
    {% module "page_template_logo" path="@hubspot/logo" label="Logo" %}
    <!-- Button to fetch quote via serverless function -->

    </div>
    <!-- End button section -->

    {{ standard_footer_includes }}

    <!-- JavaScript that invokes the serverless function
     and adds returned data into the "data" text element -->
    <script>
      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...';
        // Replace <domain> with your own domain
        fetch('http://<domain>/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);
          });
      });
    </script>
  </body>
</html>
```

- 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://<domain>/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

</div>
```
```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 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 <u>not</u> 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).
  <ul>
    <li>
      You should never return secret values through console logging or as a
      response, as it will expose the secret to the front-end.
    </li>
    <li>
      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.
    </li>
  </ul>
## 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

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

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

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:<ul><li>`name`: the name of the variant.</li><li>`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.</li><li>`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.</li></ul> |

### 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 <u>not</u> appear in the font selector dropdown menu.


# 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": "<iframe width=\"200\" height=\"113\" src=\"https://www.youtube.com/embed/Y4v6cKm2wqE?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>"
}
```

## 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" %}

  </div>
  {% require_css %}
    <style>
      .oembed_container {
        display: inline-block;
        height: 100%;
        position: relative;
        width: 100%;
      }

      .oembed_container .iframe_wrapper > * {
        height: 100%;
        left: 0;
        margin: 0 auto;
        position: absolute;
        right: 0;
        top: 0;
        width: 100%;
      }

      .iframe_wrapper {
        height: 0;
        padding-bottom: 56.25%;
        padding-top: 25px;
        position: relative;
      }
    </style>
  {% end_require_css %}
  {% require_js %}
    <script>
      const oembedContainer = document.querySelector('.oembed_container');
      const iframeWrapper = document.querySelector('.iframe_wrapper');
      const url = iframeWrapper.dataset.embedUrl;
      if (url) {
        var request = new XMLHttpRequest();
        var requestUrl = "/_hcms/oembed?url=" + url + "&autoplay=0";
        request.open('GET', requestUrl, true);
        request.onload = function() {
          if (request.status >= 200 && request.status < 400) {
            var data = JSON.parse(request.responseText);

            const maxHeight = iframeWrapper.dataset.maxHeight !== undefined && !iframeWrapper.dataset.maxHeight ? data.height : iframeWrapper.dataset.maxHeight;
            const maxWidth = iframeWrapper.dataset.maxWidth !== undefined && !iframeWrapper.dataset.maxWidth ? data.width : iframeWrapper.dataset.maxWidth;
            const height = iframeWrapper.dataset.height !== undefined && !iframeWrapper.dataset.height ? data.height : iframeWrapper.dataset.height;
            const width = iframeWrapper.dataset.width !== undefined && !iframeWrapper.dataset.width ? data.width : iframeWrapper.dataset.width;

            const el = document.createElement('div');
            el.innerHTML = data.html;
            const iframe = el.firstChild;
            iframeWrapper.appendChild(iframe);

            if (maxHeight) {
              const heightStr = maxHeight.toString(10) + "px";
              oembedContainer.style.maxHeight = heightStr;
              iframe.style.maxHeight = heightStr;
            }

            if (maxWidth) {
              const widthStr = maxWidth.toString(10) + "px";
              oembedContainer.style.maxWidth = widthStr;
              iframe.style.maxWidth = widthStr;
            }

            if (height) {
              const heightStr = height.toString(10) + "px";
              oembedContainer.style.height = heightStr;
              iframe.style.height = heightStr;
            }

            if (width) {
              const widthStr = width.toString(10) + "px";
              oembedContainer.style.width = widthStr;
              iframe.style.width = widthStr;
            }
          } else {
            console.error('Server reached, error retrieving results.');
          }
        };
        request.onerror = function() {
          console.error('Could not reach the server.');
        };
        request.send();
      }
    </script>
  {% end_require_js %}
{% else %}

  </div>
  {% require_css %}
    <style>
      .iframe_wrapper {
        height: 0;
        padding-bottom: 56.25%;
        padding-top: 25px;
        position: relative;
      }

      .embed_container {
        display: inline-block;
        height: 100%;
        position: relative;
        width: 100%;
      }

      .embed_container iframe {
        left: 0;
        max-height: 100%;
        max-width: 100%;
        position: absolute;
        right: 0;
        top: 0;
      }
    </style>
  {% 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 <img> 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

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: <ul><li>`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.</li><li>`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.</li><li>`inline`: displays the group and its children inline with no option to collapse the group.</li></ul> |
| `icon` | Object | Adds an icon to the left of the label. Contains the following parameters: <ul><li>`name`: the Font Awesome icon identifier.</li><li>`set`: the Font Awesome [icon set](/reference/cms/fields/module-theme-fields#icon).</li><li>`type`: icon style (e.g., `SOLID`, `REGULAR`).</li></ul> |
| `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": "<h3>This is a title</h3><p>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.</p>"
    }
  ],
  "default": [
    {
      "image": {
        "loading": "lazy"
      },
      "text": "<h3>This is a title</h3><p>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.</p>"
    },
    {
      "image": {
        "loading": "lazy"
      },
      "text": "<h3>This is a title</h3><p>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.</p>"
    }
  ]
}
```

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

</div>
```

## 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 <u>not</u> 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 %}
<style>
{% scope_css %}
  .team-member {
  {% if module.style.gradient.css %}
  background: {{ module.style.gradient.css }};
  {% endif %}
  {{ module.style.bg_img.css }}
  {{ module.style.spacing.css }}
  {{ module.style.border.css }}
  }
{% end_scope_css %}
</style>
{% 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
<!--Looping through a repeating field-->
<ul>
  {% for item in module.ingredient %}
  <li>{{ item }}</li>
  {% endfor %}
</ul>
```

### 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
<h2>Ingredients</h2>
<ul>
  {% for ingredient in module.ingredients %}
  <li>
    <button
      data-quantity="{{ ingredient.quantity }}"
      data-unit="{{ ingredient.measurement }}"
      data-ingredient="{{ ingredient.ingredient }}"
    >
      Add to cart
    </button>
    {{ ingredient.quantity }} {{ ingredient.measurement }} {{
    ingredient.ingredient }}
  </li>
  {% endfor %}
</ul>
```

### 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.<ul><li>If the field is not nested inside a field group, use the field's name (i.e. `field_name`).</li><li>For fields nested in groups, the path should match its grouping structure, separated by a period. For example:<ul><li>`field_group_name.field_name`</li><li>`parent_group.child_group.field_name`</li></ul></li></ul> |
| `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: <ul><li>`NOT_EQUAL`</li><li>`EQUAL`</li><li>`EMPTY`</li><li>`NOT_EMPTY`</li><li>`MATCHES_REGEX`</li></ul> |
| `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.<ul><li>If the field is not nested inside a field group, use the field's name (i.e. `field_name`).</li><li>For fields nested in groups, the path should match its grouping structure, separated by a period. For example:<ul><li>`field_group_name.field_name`</li><li>`parent_group.child_group.field_name`</li></ul></li></ul> |
| `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: <ul><li>`NOT_EQUAL`</li><li>`EQUAL`</li><li>`EMPTY`</li><li>`NOT_EMPTY`</li><li>`MATCHES_REGEX`</li></ul>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 <u>not</u> 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.<ul><li>If the field is not nested inside a field group, use the field's name (i.e. `field_name`).</li><li>For fields nested in groups, the path should match its grouping structure, separated by a period. For example:<ul><li>`field_group_name.field_name`</li><li>`parent_group.child_group.field_name`</li></ul></li></ul> |
| `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: <ul><li>`NOT_EQUAL`</li><li>`EQUAL`</li><li>`EMPTY`</li><li>`NOT_EMPTY`</li><li>`MATCHES_REGEX`</li></ul>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": [ <CSS selectors> ],
    "theme.settings.path.2": [ <CSS selectors> ],
  }
}
```

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 <theme-directory-path>
```

| 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

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 <u>not</u> 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: '<h2>Free</h2>',
    }),

    setFieldParams(defaultRichTextField, {
      name: 'description',
      label: 'Product description',
      default:
        '<p>For teams that need additional security, control, and support.</p>',
    }),
  ];

  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

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 %}

    <!-- Form module tag for use in templates -->
    {% 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 `<section>` 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 `<section>` tag around the HTML content, then include styling to reference the color field, such as:

`<section style="background:{{ module.color_field.color }}">`
## 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 <u>not</u> 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
<script
  charset="utf-8"
  type="text/javascript"
  src="//js.hsforms.net/forms/embed/v2.js"
></script>
<script>
  hbspt.forms.create({
    region: 'na1',
    portalId: '123456',
    formId: 'df93f0c1-2919-44ef-9446-6a29f9a7f',
  });
</script>
```

| 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 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 <partial-file-name>
```

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
<!--
  templateType: global_partial
  label: Page Header
-->
```

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 `<head>` 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 <module_name>
```

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
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
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. 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 %}
    <a href="/_hcms/mem/login">Log In</a>
{% 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 %}
    <a href="/_hcms/mem/login">Log In</a>
{% 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=<true/false>`

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 %}
 <article {{ flag_content_for_access_check(content.id) }}>
 </article>
{% endfor %}
```

This would result in the following HTML output after the script runs to and finds that a blog post is private:

```html
<!-- HTML output after script runs -->
<article hs-member-content-access="true">
</article>
```

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 = <Array of content ID's> // 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.

- `<your domain>/_hcms/mem/login`
- `<your domain>/_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
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/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

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
<div
  id="hs_cos_wrapper_widget_1711642118872"
  class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_module widget-type-menu"
  style=""
  data-hs-cos-general-type="widget"
  data-hs-cos-type="module"
>
  <span
    id="hs_cos_wrapper_widget_1711642118872_"
    class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_menu"
    style=""
    data-hs-cos-general-type="widget"
    data-hs-cos-type="menu"
  >
    <div
      id="hs_menu_wrapper_widget_1711642118872_"
      class="hs-menu-wrapper active-branch flyouts hs-menu-flow-horizontal"
      role="navigation"
      data-sitemap-name="default"
      data-menu-id="162449947934"
      aria-label="Navigation Menu"
    >
      <ul role="menu">
        <li
          class="hs-menu-item hs-menu-depth-1 hs-item-has-children"
          role="none"
        >
          <a
            href="javascript:;"
            aria-haspopup="true"
            aria-expanded="false"
            role="menuitem"
            >Menu item 1</a
          >
          <ul role="menu" class="hs-menu-children-wrapper">
            <li class="hs-menu-item hs-menu-depth-2" role="none">
              <a
                href="//freecrmtest.hubspotpagebuilder.com/test"
                role="menuitem"
                >A</a
              >
            </li>
            <li class="hs-menu-item hs-menu-depth-2" role="none">
              <a href="https://www.wikipedia.org/" role="menuitem">B</a>
            </li>
          </ul>
        </li>
        <li
          class="hs-menu-item hs-menu-depth-1 hs-item-has-children"
          role="none"
        >
          <a
            href="javascript:;"
            aria-haspopup="true"
            aria-expanded="false"
            role="menuitem"
            >Menu item 2</a
          >
          <ul role="menu" class="hs-menu-children-wrapper">
            <li class="hs-menu-item hs-menu-depth-2" role="none">
              <a
                href="//freecrmtest.hubspotpagebuilder.com/test"
                role="menuitem"
                >A</a
              >
            </li>
            <li class="hs-menu-item hs-menu-depth-2" role="none">
              <a href="https://www.hubspot.com/new-page" role="menuitem">B</a>
            </li>
            <li class="hs-menu-item hs-menu-depth-2" role="none">
              <a href="https://www.hubspot.com/blah" role="menuitem">C</a>
            </li>
          </ul>
        </li>
      </ul>
    </div>
  </span>
</div>
```

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 }}<br>
{% endfor %}

{% set default_node = menu("default") %}
{% for child in default_node.children %}
	{{ child.label }}<br>
{% 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.


# Build CMS modules with React

Rather than using HubL to build modules, you can use JavaScript and React instead. Then, after creating a JavaScript module, it can be added to a page either by hardcoding or via the drag and drop editor in HubSpot. In addition to stitching server-rendered React components into the HTML generated by HubL, JavaScript modules and partials support client-side interactivity with [islands](https://www.patterns.dev/posts/islands-architecture). This enables you to have more precise control over where and when JavaScript is shipped and run in the browser.

Get started with the [CMS React documentation](https://github.hubspot.com/cms-react/).

## Benefits of building with React vs. HubL

At a high level, building with React comes with benefits including component composability, code reuse, broader community resources, and real access to JavaScript on the server at render time.

Rendering React on the server means that there's less of a divide between your code that serves the initial page HTML and your interactive browser code. In contrast, creating complex and interactive pages with HubL can lead to:

- An increase of client-side JavaScript that slows down the page until it's all been downloaded and executed.
- Needing to replicate or maintain UI logic across HubL and JavaScript in order to have server HTML that is immediately visible and interactive.

But by building with JavaScript, you can code complex interactive components that either share code or are directly rendered on the server. When paired with the [islands](https://www.patterns.dev/posts/islands-architecture) model, this can enable you to more easily code web experiences that have good Core Web Vital scores (LCP, FID, CLS) even as complexity increases.

Additionally, by building on top of JavaScript and the open source framework of React, you can use the wealth of tooling, third-party libraries via npm, example code, and more that are available in the broader ecosystem. For example, since JavaScript modules and partials are built on top of Vite, you'll get things like ESM, TypeScript, JSX, CSS modules, and tree-shaking out of the box.

## Building and deploying

CMS React is built on the [developer projects framework](/guides/crm/developer-projects/create-a-project), which uses a local build and deploy process. A project can be uploaded for build and deploy using the `hs project upload` command. This command can be run in the root of your project, or you can include a directory path in the command.

```shell
// In root directory
hs project upload

// Specified path
hs project upload path/to/project
```

The upload command will kick off the project build. By default, projects are configured to [auto-deploy](/guides/crm/developer-projects/create-a-project#view-the-project-in-hubspot) after successful build. You can turn this setting off in HubSpot after your first successful deploy. If you turn off auto-deploy, you can manually deploy your project after successful build by running `hs project deploy`.
For more information about `hs project` CLI commands, check out the [documentation](/guides/crm/developer-projects/project-cli-commands) or run `hs project --help`.
## Limitations

The following features are not currently available for React-based modules:

- Importing JavaScript modules into JavaScript partials.
- Online code editing within the design manager.
- Some HubL features, such as certain tags and filters, may not be readily available in JavaScript components.


# 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 <u>template</u>, 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
<!--
templateType: page
isAvailableForNewContent: true
label: Ticket listing
screenshotPath: ../images/template-previews/ticket-listing.png
hiddenModules:
- '@hubSpot/follow_me'
- ../modules/form
hiddenSections:
- ../sections/card
-->
```


# 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": "<p>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.</p>"
  }
]
```

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
<section class="team-member">
  <img class="team-member__image" src="{{ module.team_member_image.src }}" alt="{{ module.team_member_image.alt }}">
  <h3 class="team-member__name">{{ module.team_member_name }}</h3>
  <p class="team-member__position">{{ module.team_member_position }}</p>

</section>
```

## 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

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 init
```

- 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 `hubspot.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 <src> <destination>
```

| 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=<accountNameOrId>` 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.html -->
{{module.customer_name}}
<img src={{module.profile_pic.src}} alt="{{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 <hs_src> <destination>`:

  - `<hs_src>` 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**.

    

  - `<destination>` 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": "<p>I’ve had nothing but wonderful experiences with this company.</p>"
  }
]
```

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 <src> <destination>`.

```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="<h2>Provide more details here.</h2><p>Use text and images to tell your company’s story. Explain what makes your product or service extraordinary.</p>"
      offset=8,
      width=4
    %}
    {% end_dnd_module %}
  {% end_dnd_section %}
```
When adding a module to a Drag and Drop area, the module tag does <u>not</u> 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 <src> <dest>`. 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

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 `<head>` 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):

  `<link rel="alternate" hreflang="[**_lang_code_**](https://support.google.com/webmasters/answer/189077#language-codes)" href="_url_of_page_" />`

- 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
<html lang="{{ html_lang }}" {{ html_lang_dir }}>
```

## 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 #}

          </div>
        {% endif %}

      </div>
      {# 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 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 contain HubL/HTML markup, CSS, and JavaScript that together build reusable and editable components of a website.

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, or JavaScript 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.

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

[NavigatFields](/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 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 }}"' : '' %}
<img src="{{ module.image_field.src }}" alt="{{ module.image_field.alt }}" {{ loadingAttr }}>
```

### 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 }}"' : '' %}
<img src="{{ module.image_field.src }}" alt="{{ module.image_field.alt }}" {{ loadingAttr }}>
```


# 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 <u>not</u> 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

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

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 <u>not</u> 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 <u>not</u> 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: `<HubID>.<suffix>`. 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 ​<u>all</u>​ 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 <u>not</u> 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.

<Video
  src="https://www.hubspot.com/hubfs/Knowledge_Base_2021/pre-provision-ssl-for-proxy.mp4"
  height="auto"
  width="600"
  controls={true}
  autoplay={true}
  loop={true}
></Video>

## 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 `<value>.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 `<HubID>.<suffix>` 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 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 (<abbr>CSS</abbr>)
- The loading of JavaScript (<abbr>JS</abbr>)
- 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 (<abbr>CLS</abbr>) 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 (<abbr>DOM</abbr>)](https://developer.mozilla.org/en-US/docs/Glossary/DOM) and [Cascading Style Sheet Object Model (<abbr>CSSOM</abbr>)](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 `<link rel="preload" as="image" href="http://example.com/img_url.jpg">` 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 (<abbr>JS</abbr>) 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 `<head>` 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 `<head>` 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 `</body>` 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 `<details>` and `<summary>`. For modals use `<dialog>`.
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

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 `<label>` element, with the intent to ensure that all `<label>` elements in a form are white.

```css
label,
legend {
  color: white !important;
}
```

While this rule theoretically works when content is rendered on your live website page, it will also render all `<label>` tags in the content editor as white, as shown below.
Instead, you should use more specific selectors, as shown in the code below, to target only the form module labels.

```css
.hs-form label,
.hs-form .hs-form-field > label,
.hs-form .hs-field-desc {
  color: white;
}
```

## Include editor-specific code

When developing a theme, template, or module, you can use CSS classes, JavaScript variables, and HubL to change how content is rendered with content editors and preview screens. This enables you to provide extra context for users in the content editors and preview pages, while still controlling the output on the live page. In addition, you can use these methods to prevent problematic code from rendering within HubSpot.

Below, learn about the different methods of conditionally rendering content based on whether it's rendered in HubSpot or on the live page.

### Rendering CSS with .hs-inline-edit

The editor uses an iframe to load a preview of the content into HubSpot’s content editor, and the `<html>` element within this iframe is assigned a class of `.hs-inline-edit`. Using this class, you can write CSS that conditionally renders based on the presence of that iframe.

For example, the following CSS takes advantage of the pseudo-class `:not()` so that the rule does not apply when loaded in the editor:

```css
:not(.hs-inline-edit) label {
  color: white;
}
```

As another example, if you're seeing that the rich text toolbar in the editor is being hidden behind a page's header due to a z-index rule, you could update your CSS to apply a lower z-index value to the header. By using the `.hs-inline-edit class`, the new rule will only apply in the editor, not the live page. For reference, the rich text toolbar's z-index is `2147483647`.

```css
.hs-inline-edit .header {
  z-index: 2147483600;
}
```

### Rendering JavaScript window.hsInEditor

When content is loaded in the editor, `window.hsInEditor` will return `true`. You can include this variable in your JavaScript to conditionally run code based on whether the content is within the context of the editor. This can be useful when JavaScript is negatively impacting the in-app editing experience.

For example, you might find that your JavaScript isn't working as expected because it's running before the page editor has loaded. If your site uses jQuery, you could use the document ready handler to run the code only once the page editor has loaded fully.

```js
jQuery(document).ready(function ($) {
  if (window.hsInEditor) {
    return;
  }
  // other stuff
  $('.some_widget').fancyThing();
});
```

### Rendering content with HubL

HubSpot provides a [set of HubL variables](/reference/cms/hubl/variables#in-app-editor-and-preview-variables) that will return `true` in various editor and preview contexts. This includes variables that can check for any editor or preview context, as well as specific editor and preview contexts.

For example, the if statement below would render its content only when the user is in the blog post editor.

```hubl
{% if is_in_blog_post_editor %}
Helpful contextual information for blog authors.
{% endif %}
```


# Update email templates to use default email modules

When building email templates, HubSpot provides a set of [default email modules](/reference/cms/modules/default-email-modules) to get you started. These modules are similar to [default web modules](/reference/cms/modules/default-modules), but are split up to enable HubSpot to release updates to these modules for better email client support, while also releasing updates to the web versions of the modules separately. Existing email templates using the web modules will still function, but you'll need to update your email templates to use these new modules for forwards compatibility and to avoid errors in the design manager and CLI.

The following are the new email-specific modules that should replace existing usage of the web default modules:

- [email_cta](/reference/cms/modules/default-email-modules#email-call-to-action) (replaces `cta`)
- [email_header](/reference/cms/modules/default-email-modules#email-header) (replaces `header`)
- [email_linked_image](/reference/cms/modules/default-email-modules#email-linked-image) (replaces `linked_image`)
- [email_logo](/reference/cms/modules/default-email-modules#email-logo) (replaces `logo`)
- [email_post_filter](/reference/cms/modules/default-email-modules#email-blog-post-filter) (replaces `post_filter`)
- [email_post_listing](/reference/cms/modules/default-email-modules#email-blog-post-listing) (replaces `post_listing`)
- [email_section_header](/reference/cms/modules/default-email-modules#email-section-header) (replaces `section_header`)
- [email_social_sharing](/reference/cms/modules/default-email-modules#email-social-sharing) (replaces `social_sharing`)
- [email_text](/reference/cms/modules/default-email-modules#email-one-line-of-text) (replaces `text`)

Below, read more about the new email modules and how to update your email templates to use them.

## Updating to the new modules

To update an email template to use the new default email modules, you'll need to update either the module path or ID. All email module paths are prepended by `email_`, and all email modules have been assigned a new ID. You can r[eview the section below](#new-default-email-modules) for a full list of new module paths and IDs.

Below is an example of updating an email template to use the new email logo module, either by referencing its new path or ID.

### Updating the module by path

**Original**

```hubl
{% module "logo_with_path" path="@hubspot/logo" %}
```

**Updated**

```hubl
{% module "logo_with_path" path="@hubspot/email_logo" %}
```

### Updating the module by ID

**Original**

```hubl
{% module "logo_with_id" module_id="1155232" label="Logo" %}
```

**Updated**

```hubl
{% module "logo_with_id" module_id="122980089981" label="Logo" %}
```

## New default email modules

The following modules have been added for email templates. When reference these modules in your email templates, you can use either the module path or ID.

- [Email blog post filter](#email-blog-post-filter)
- [Email blog post listing](#email-blog-post-listing)
- [Email call-to-action](#email-call-to-action)
- [Email header](#email-header)
- [Email linked image](#email-linked-image)
- [Email logo](#email-logo)
- [Email one line of text](#email-one-line-of-text)
- [Email section header](#email-section-header)
- [Email social sharing](#email-social-sharing)

### Email blog post filter

```hubl
{% module "email_post_filter" path="@hubspot/email_post_filter" %}
```
### Email blog post listing

```hubl
{% module "email_post_listing" path="@hubspot/email_post_listing" %}
```
### Email call-to-action

```hubl
{% module "email_cta" path="@hubspot/email_cta" %}
```
### Email header

```hubl
{% module "email_header" path="@hubspot/email_header" %}
```
### Email linked image

```hubl
{% module "email_linked_image" path="@hubspot/email_linked_image" %}
```
### Email logo

```hubl
{% module "email_logo" path="@hubspot/email_logo" %}
```
### Email one line of text

```hubl
{% module "email_text" path="@hubspot/email_text" %}
```
### Email section header

```hubl
{% module "email_section_header" path="@hubspot/email_section_header" %}
```
### Email social sharing

```hubl
{% module "email_social_sharing" path="@hubspot/email_social_sharing" %}
```


# Specify a drag and drop area in a custom email template
[Drag and drop areas](/guides/cms/content/templates/drag-and-drop/overview) allow developers to create sections of custom email templates that support layout, stylistic and content changes directly within the email editor. This allows developers to create fewer email templates with global structure, that support content creators making a multitude of pages with various purposes and layouts.
Custom email templates can only contain one drag and drop area.
## 1. Create a new HTML template

Create a new html template that will include the HubL and HTML code which will make up your drag and drop section:

- In your HubSpot account, navigate to **Content** > **Design manager.**.
- In the left sidebar, create a new file by clicking **File** > **New file**.
- In the dialog box, click the **What would you like to build today?** dropdown menu and select **HTML + HubL**.
- Click **Next**.
- Click the **Template type** dropdown menu then select **Email**.
- Enter a **name** for the template.
- To update the template's location, click **Change** under _File location_, then select a new folder where the template will be created.
- Click **Create**.

## 2. Add HubL tags to template head

Review the following required and optional tags you can include in the `<head>` section of your email template for styling and compatibility purposes:

### Required tag

`{{ dnd_area_stylesheet }}`

This tag will provide the following:

- Adds associated media queries
- Fixes known styling issues in Outlook
- Resets margins and paddings
- Enables anti-aliasing
- Adds some CSS

### Optional tags

To ensure that your template renders consistently across all major email clients, you can also include the following optional HubL tags to your template:

- `{{ email_header_includes }}`: this tag injects CSS into your template to help render styling consistently, fix common styling issues, and add metadata to the email's HTML. When parsing this HubL tag, the following content will be added within the `<head>` of the email's HTML:

| Parameter | Description |
| --- | --- |
| `<meta name="x-apple-disable-message-reformatting">` | Prevent iOS 11 from automatically scaling emails |
| `<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">` | Informs browsers and email clients how you expect them to interpret different characters. |
| `<meta http-equiv="X-UA-Compatible" content="IE=edge">` | Enables CSS3 and media queries on Windows Phone 7.5, informs Internet Explorer to render content in the highest mode possible, and allows the browser to choose which version of Internet Explorer that the email should be rendered with. |
| `<meta name="viewport" content="width=device-width, initial-scale=1.0"/>` | Sets the viewable area to the width of the device screen, which helps make your email responsive. |

- `{{ reset_css_stylesheet }}`: including this tag will fix a number of styling issues in Outlook, and will also reset your email's margins and paddings.

- `{{ outlook_background_snippet }}`: this tag will set the background image or color in Outlook, provided that you've specified an associated background CSS rule.

## 3. Create a drag and drop area

After you add the `{{ dnd_area_stylesheet }}` HubL tag and any other optional tags to the `<head>` of your template, you can configure a `dnd_area` in the template body.

### Create an empty drag and drop area

A `dnd_area` is the 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.

The drag-and-drop area can be enclosed within either a `<div>` or a `<table>`. One constraint of the drag-and-drop area is that the minimum width is set to 624 pixels, and this value cannot be overridden.

This tag on its own will generate a drop zone for content creators to drag modules into within the email editor.

The following code would specify an empty drag and drop area:

```hubl
{% dnd_area "main" %}
	<!-- generates an empty drag and drop area drop-section -->
{% end_dnd_area %}
```

### Create a drag and drop area with empty columns

You can also customize your drag and drop area to specify default sections and columns by using the `dnd_section` and `dnd_column` fields.

For example, the following HubL code would specify a drag and drop area with 3 columns:

```hubl
{% dnd_area "main" %}
  {% dnd_section padding={'top': 25, 'bottom': '20} %}
    {% dnd_column width=4 %}
    {% end_dnd_column %}
    {% dnd_column width=4 %}
    {% end_dnd_column %}
    {% dnd_column width=4 %}
    {% end_dnd_column %}
  {% end_dnd_section %}
{% end_dnd_area %}
```
The `dnd_row` HubL tag is <u>not</u> currently supported in email templates.
## 4. Add a drag and drop area with modules

To pre-populate a section with content, you can use the `dnd_module` tag to include a module by referencing its path. The module must be added within a section and column to pre-populate the drag-and-drop area with content.

In the example below, a default HubSpot module is referenced, but you can also include modules you've built, by specifying their path within your Design Tools file tree.

To specify a default value for the `dnd_module`, you can use the `module_attribute` tag.

```hubl
{% dnd_area "main", full_width=False %}
	{% dnd_section padding={
	            'top':'25',
	            'bottom':'20'
	            }, full_width=False %}
	  {% dnd_column width=6 %}
	    {% dnd_module path='@hubspot/image_email', img={
	                    'alt':'NavyLogo',
	                    'height':38,
	                    'src':'email_dnd_template_images/NavyLogo.png',
	                    'width':120
	                    }, alignment='center', hs_enable_module_padding=True, hs_wrapper_css={
	                    'padding-bottom':'10px',
	                    'padding-left':'20px',
	                    'padding-right':'20px',
	                    'padding-top':'10px'
	                    } %}
	    {% end_dnd_module %}
	  {% end_dnd_column %}
	  {% dnd_column width=6 %}
	  {% end_dnd_column %}
	{% end_dnd_section %}
{% end_dnd_area %}
```

## 5. Further customize and style your drag and drop template

Each drag and drop HubL tag (e.g., `dnd_area`, `dnd_section`, `dnd_column` etc.) includes different parameters that you can use to provide default styling and specify other behavior, such as the label that will appear in the email editor sidebar.

To learn more about the parameters available for each tag, check out the links below for each tag:

- [dnd_area](/reference/cms/hubl/tags/dnd-areas#dnd-area)
- [dnd_section](/reference/cms/hubl/tags/dnd-areas#dnd-section)
- [dnd_column](/reference/cms/hubl/tags/dnd-areas#dnd-column)
- [dnd_module](/reference/cms/hubl/tags/dnd-areas#dnd-module)


# Drag and Drop areas overview

Drag and drop areas enable you to create areas of pages and global partials where content creators can place modules, change layout, and add styling within the content editor. Using drag and drop areas, you can create fewer templates, as content creators are able to create layouts on their own.

Below, learn more about the `dnd_area` experience and concepts. Once you're ready to build, see [getting started with dnd_area](/guides/cms/content/templates/drag-and-drop/tutorial), and the [dnd_area reference](/reference/cms/hubl/tags/dnd-areas).
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).
## The content creator experience

When a content creator creates a page using a template that has drag and drop areas, they first see the page with predefined modules in the layout that you've defined as the developer. This initial layout sets the precedent for how pages using this template might look. Using drag and drop areas, the content creator can then build the page, including:

- Adding modules, sections, rows, and columns.
- Resizing modules and updating their content and styling, such as adjusting alignment and adding backgrounds.

This gives content creators enough flexibility to make simple page changes without needing a developer for small tweaks.
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 <u>without</u> `dnd_area` tags can only be swapped for other coded templates without `dnd_area` tags.

When the page's template is swapped, any existing content added to drag and drop areas of the first template will be retained.
## The developer experience

Developing with drag and drop areas is similar to working with common CSS frameworks and their grids. First you'll lay out the page using containers, called [sections](/guides/cms/content/templates/drag-and-drop/sections), which contain rows. Inside of those rows are modules and columns. Learn more about these [elements](#drag-and-drop-area-elements) below.

While you could hard-code nearly everything into the template, the goal of developing with drag and drop areas is that you instead build the default page content which can later be edited by a content creator.

View the [HubSpot CMS Boilerplate templates](https://github.com/HubSpot/cms-theme-boilerplate/tree/main/src/templates) to see `dnd_area` tags in use.
## Drag and drop area elements

When building a page with drag and drop areas, you'll include the following elements:

- [**dnd_area**](/reference/cms/hubl/tags/dnd-areas#dnd-area)**:** the highest-level drag and drop element which enables dragging and dropping modules in the content editor. You cannot nest drag and drop areas. For example, a `dnd_section` cannot contain a `dnd_area` tag.
- [**dnd_section**](/reference/cms/hubl/tags/dnd-areas#dnd-section)**:** the outermost container in a `dnd_area`. Can contain `dnd_row`, `dnd_column`, and `dnd_module`. You cannot nest a `dnd_section` within other drag and drop elements.
- [**dnd_column**](/reference/cms/hubl/tags/dnd-areas#dnd-column)**:** can contain `dnd_row`. Multiple columns within a `dnd_row` will align the row's contents horizontally.
- [dnd_row](/reference/cms/hubl/tags/dnd-areas#dnd-row)**:** can contain `dnd_module` and `dnd_column`.
- [dnd_module](/reference/cms/hubl/tags/dnd-areas#dnd-module)**:** a module wrapper where module layout, styles, and content can be added.

Learn more about each of these tags below. In addition, you may want to walk through the [Getting started with drag and drop areas guide](/guides/cms/content/templates/drag-and-drop/tutorial) for a hands-on approach.
The diagram below further breaks down the hierarchy of the various drag and drop elements.
For a video walkthrough of how to visualize drag and drop areas, check out the video below:
Continue reading to learn more about drag and drop sections, rows, columns, and modules.

### Sections

Sections are a special type of row which are created using the [`dnd_section`](/reference/cms/hubl/tags/dnd-areas#drag-and-drop-section-code-dnd-section-code-) tag. They are the only drag and drop element that can be a direct descendant of a `dnd_area`. You can think of sections like an outer wrapping container. They can enable content to be full width or have a confined center max-width. Because sections wrap around columns and modules, it makes it easy to rearrange and implement large portions of content. The `dnd_section` tag does not render an HTML `<section>` element.

Below is a screenshot of how a section appears in the page editor.
In addition to the `dnd_section` tag, you can also create [section templates](/guides/cms/content/templates/drag-and-drop/sections#create-reusable-sections) which are pre-defined reusable sections that content creators can access in the page editor. For example, you could build the section shown in the screenshot above as a section template so that a content creator could quickly add it to pages as needed. Section templates have some unique capabilities, including being able to use them similar to a standard hubL template partial.

### Columns

Columns are wrappers for rows and modules and can be placed inside of a row or section. Columns are created using the [`dnd_column`](/reference/cms/hubl/tags/dnd-areas#drag-and-drop-column-code-dnd-column-code-) tag.

Use multiple columns inside of a row to place rows and the modules they contain horizontally.

Columns are vertical regions that can contain rows. You can make columns of different sizes by changing their width. A row’s size is 12 "columns" wide, this refers to the CSS grid. The columns inside of a row can be any size smaller than 12 but cannot add up to more than 12.

When multiple rows are placed inside of a column the modules inside of those rows will appear vertically stacked. Since modules are columns themselves, a module cannot be a direct descendant of a column, they must be contained within a row.
The appearance of a column in the page editor.

### Rows

Rows are wrappers for columns. Rows are created in templates using the [`dnd_row`](/reference/cms/hubl/tags/dnd-areas#drag-and-drop-row-code-dnd-row-code-) tag. Since modules are columns you can place them directly inside of a row. This will cause the modules to appear horizontally adjacent to each other.

Modules can be organized vertically by placing them inside of rows. If you want to place a module above another you would place that module inside a row. You would then add another module in a row above or below that first row.
The appearance of a row in the page editor.

### Modules

[Modules](/guides/cms/content/modules/overview) are a fundamental part of the HubSpot CMS, acting as reusable building blocks that you use to piece together a site, and display content. When building a template you place modules inside of drag and drop rows and sections using the [`dnd_module`](/reference/cms/hubl/tags/dnd-areas#drag-and-drop-module-code-dnd-module-code-) tag. Modules are also columns. Meaning if you place two module tags, or a module and a column directly next to each other, they will appear side-by-side horizontally.

No drag and drop elements can be placed within a module. Modules cannot be direct children of a [`dnd_area`](/reference/cms/hubl/tags/dnd-areas#drag-and-drop-area-code-dnd-area-code-).

## HTML structure and styling

Drag and drop areas and their elements when rendered have class names for a 12 column grid based on bootstrap 2. To make it easy to get up and running, you can use the [\_layout.css](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/css/objects/_layout.css) file from the [HubSpot CMS Boilerplate](https://github.com/HubSpot/cms-theme-boilerplate/). This provides default styles for those class names.

You are not required to use this stylesheet and can provide your own styles instead. If you're building your site based off of the CMS Theme Boilerplate and want to use your own CSS, you will want to [remove layout.css from being called in base.html](https://github.com/HubSpot/cms-theme-boilerplate/blob/5abaf2a4c45a95dbed1d459f7f0f6407350752ac/src/templates/layouts/base.html#L8). For your own CSS grid you will need to target those same grid class names, but the styling is up to you.

Drag and drop areas when rendered create divs with classes that are used by the page editor. Examples would be `widget-span` and `widget-type-cell`. You should not directly target these classes as they are used by page-editor and could change down the road.

Instead in your [`dnd_area`](/reference/cms/hubl/tags/dnd-areas#drag-and-drop-area-code-dnd-area-code-) HubL add a class parameter with a class name you would like to use

```html

      <!--end widget-span -->
    </div>
  </div>
</div>
```

### Editor and attribute styling

With drag and drop areas content creators can have some effect on styling of the page. For example they can set a section to have a background. Developers can pass default values for those settings through attributes.

When the page is actually rendered, the styles that are generated based on those settings is added to the `standard_header_includes`.

## Migrating flexible columns

If you are changing templates built with flexible columns to now use drag and drop areas, keep the following in mind about flexible columns.

Flexible columns are not the same as drag and drop areas, and you can't swap from a template that only has a flexible column to one that only has a drag and drop area. This limitation was put in place because the content would not map from the flexible column to the drag and drop area. To illustrate why this is, suppose you built your new template so you have a sidebar and a main content area. Your sidebar is a flexible column, your main content is a drag and drop area. The swapping tool would map the flexible column to the flexible column.

Learn more about [adding drag and drop areas to existing pages](/guides/cms/improve-older-sites/add-theme-features-to-existing-sites#drag-and-drop-areas).

## Related resources

- [Adding drag and drop areas to existing websites](/guides/cms/improve-older-sites/add-theme-features-to-existing-sites#drag-and-drop-areas)
- [Creating a drop area](/guides/cms/content/templates/drag-and-drop/tutorial)
- [Visualizing Drag and Drop Areas - YouTube](https://www.youtube.com/watch?v=RUs24n8DfzA&feature=youtu.be)


# Sections

The outermost container in a [drag and drop area](/guides/cms/content/templates/drag-and-drop/overview) is called a section. Sections can't be nested within any other [dnd element](/guides/cms/content/templates/drag-and-drop/overview#drag-and-drop-areas-and-their-elements), but can contain modules, [rows](/guides/cms/content/templates/drag-and-drop/overview#row), and [columns](/guides/cms/content/templates/drag-and-drop/overview#column). In the page editor, content creators can add sections to the page, then modify and style them as needed. Content creators can also create and save sections to use on other pages within the same theme, making content creation more efficient.

In this article, learn more about sections and how to use them in the page editor. If you're developing a theme, check out the guide on [hiding modules and sections](/guides/cms/content/modules/hide-modules-and-sections) from the page editor to create a more streamlined content creation experience.
## Overview

Sections can be created either [in the content editor by a content creator](https://knowledge.hubspot.com/website-pages/edit-page-content-in-a-drag-and-drop-area#create-a-section) or built by a developer into a [`dnd_area`](/guides/cms/content/templates/drag-and-drop/overview), with the [`dnd_section`](/reference/cms/hubl/tags/dnd-areas#drag-and-drop-section-code-dnd-section-code-) tag.

The styling options available in the editor are available when coding a template as well. For example:

```hubl
<main class="body-container-wrapper">
  {% dnd_area 'dnd_area'
    label='Main section',
  %}
    {% 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 %}

  {% end_dnd_area %}
</main>
```

For full documentation of all available drag and drop element parameters and usage examples, learn more about [dnd_area tags](/reference/cms/hubl/tags/dnd-areas).

You can use sections to quickly scaffold out templates that are easy to read. Since you are only specifying in context where the template specific instances are different, you can still go back and modify that section template.
Modifying a section will update it across all instances of that section, <u>except</u> for existing pages that use a template that references the section. Pages previously created with a template that had an included section in it will instead need to be manually updated to use the new version of the section. This prevents accidentally making breaking changes. To update a section to the latest version, a content creator can navigate to the page editor, add the new section to the page, then delete the old version.
## Create reusable sections

Within a theme, you can include preconfigured sections that content creators can add to pages using that theme within the page editor. These reusable sections are built as section template files and are coded within the same syntax that you would use inside a `dnd_area`.
To make a section available for multiple themes, you'll need to add the section template file to each theme. Similarly, sections created by content creators in the content editor will only be available within that theme.
Below, learn how to create section template files and then reference them in other template files.

### Section template files

Section templates are denoted with `templateType: section` in their [template annotation](/guides/cms/content/templates/types/html-hubl-templates#template-annotations).

```html
<!--
 templateType: section
 label: Banner
 description: "A banner typically used at the top of a page highlighting a product or main topic."
 isAvailableForNewContent: true
 screenshotPath: ../images/section-previews/banner.png
-->
```

| Parameter | Type | Description | Value |
| --- | --- | --- | --- |
| `templateType` | String | Sets the template type used to determine where the template can be used and what data is available to it. | `section` |
| `label` | String | Used in the page editor to provide a human readable name of the section. |  |
| `description` | String | Further description of the section beyond what you can do with a label. Displays in the page editor. 255 characters maximum. |  |
| `screenshotPath` | String/path | Path to a screenshot of the section. This is used to give content creators an easy visual reference for what the section looks like. |  |

A section template must begin and end with a `dnd_section` tag. Only one `dnd_section` can exist within a section template. Inside of that section, you can place modules, rows and columns, following the same `dnd_area` rules that apply when [adding a dnd_area to a page template](/reference/cms/hubl/tags/dnd-areas#dnd-area). The exception is that you are defining the content for just a section and its child drag and drop elements.

```hubl
<!--
 templateType: section
 label: Banner
 description: "A banner typically used at the top of a page highlighting a product or main topic."
 isAvailableForNewContent: true
 screenshotPath: ../images/section-previews/banner.png
-->
{% dnd_section
 padding={
   'top': 200,
   'right': 20,
   'bottom': 200,
   'left': 20
 },
 background_image={
   'backgroundPosition': 'MIDDLE_CENTER',
   'backgroundSize': 'cover',
   'imageUrl': context.backgroundImage || get_asset_url('../images/blank-page-banner.png')
 },
 max_width=778,
 vertical_alignment='MIDDLE'
%}
 {% dnd_column %}
   {% dnd_row %}
     {% dnd_module path='@hubspot/rich_text' %}
     {% module_attribute 'html' %}

     {% end_module_attribute %}
     {% end_dnd_module %}
   {% end_dnd_row %}
   {% dnd_row %}
     {% dnd_module
       path='../modules/button',
       button_text={{ context.buttonText || 'Subscribe' }}
       horizontal_alignment='CENTER'
     %}
     {% end_dnd_module %}
   {% end_dnd_row %}
 {% end_dnd_column %}
{% end_dnd_section %}
```

### Add a section partial to a template

After creating a section, you can reference it within a `dnd_area` by using a `include_dnd_partial` tag. This tag provides the path pointing to the section file, as shown below:

```hubl
{% dnd_area 'dnd_area' class='body-container body-container--home-page', label='Main section' %}
   {# Banner Section #}
   {% include_dnd_partial path='../sections/banner.html' context={} %}
   {# End Banner Section #}
{% end_dnd_area %}
```
In the above example, note the context argument in the `include_dnd_partial` tag. This allows you to pass instance specific variables from the page template to the section, overriding the default values in the section file. [See section context](#section-context) for more information.
### Section context

You can use section context variables to override section level and module level default values. Section context variables are defined by you and are not associated directly with the modules and their fields.

In your page template you can set these context variables through the `context` parameter in the `include_dnd_partial` tag.

```hubl
{% dnd_area 'dnd_area' class='body-container body-container--home-page', label='Main section' %}

   {# Banner Section #}
   {% include_dnd_partial path='../sections/banner.html'
     context={
       'content': '<h1 style="color: #fff;">Home Page</h1><p style="color: #fff;">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus dapibus posuere mi, in pretium ante posuere a. Aliquam a risus at eros molestie pretium.</p>',
       'buttonText': 'Buy Now'
     }
   %}
   {# End Banner Section #}
{% end_dnd_area %}
```

Any variables you add to your `context` parameter will become available to reference within your section template. The following example shows how to set the image URL and rich text area and button content set in context if it exists.

```html
<!--
 templateType: section
 label: Banner
 description: "A banner typically used at the top of a page highlighting a product or main topic."
 isAvailableForNewContent: true
 screenshotPath: ../images/section-previews/banner.png
-->
{% dnd_section background_image={ 'backgroundPosition': 'MIDDLE_CENTER',
'backgroundSize': 'cover', 'imageUrl': context.backgroundImage ||
get_asset_url('../images/blank-page-banner.png') }, max_width=778 %} {%
dnd_column %} {% dnd_row %} {% dnd_module path='@hubspot/rich_text' %} {%
module_attribute 'html' %}

{% end_module_attribute %} {% end_dnd_module %} {% end_dnd_row %} {% dnd_row %}
{% dnd_module path='../modules/button', button_text={{ context.buttonText ||
'Subscribe' }} horizontal_alignment='CENTER' %} {% end_dnd_module %} {%
end_dnd_row %} {% end_dnd_column %} {% end_dnd_section %}
```
Notice everywhere context variables are used, there is an `||` _OR_ filter to provide fallback default content if none is provided. For example, in the button module, if `context.buttonText` has a value, the page will use it. Otherwise, the text is set to `Subscribe`.
### Section classes

In section templates, you can add classes to the section wrapper using the class parameter. This will add the class you specify to the class field of the dnd section's html element. It's recommended wherever possible that you use styling controls built into sections to enable content creators to be able to modify them.
Section classes are only supported in section templates.
```hubl
{% dnd_section
 class='my-hero-section'
 padding={
   'top': 200,
   'right': 20,
   'bottom': 200,
   'left': 20
 },
 background_image={
   'backgroundPosition': 'MIDDLE_CENTER',
   'backgroundSize': 'cover',
   'imageUrl': context.backgroundImage || get_asset_url('../images/blank-page-banner.png')
 },
 max_width=778,
 vertical_alignment='MIDDLE'
%}
...
```
Content creators can't edit, add, or remove classes. They can only be "removed" by recreating a section manually in the editor.

Additionally you should avoid changing the layout of section children using CSS or JavaScript. Doing so can create an unpleasant page editor experience for the content creator.
### Previewing your section

The easiest way to preview your section while developing, is to use the Design Manager. Open a template containing a [`dnd_area`](/reference/cms/hubl/tags/dnd-areas) which calls your section template using a `include_dnd_partial` tag. In the top right corner click preview. This way you can keep updating your section and see your changes reflected right away. This is much more efficient than having to create a new page for each change you make.

## Copy section HubL

In the page editor, you can copy the HubL markup for a section to reuse the code as needed. This can be helpful when wanting to recreate drag and drop sections in a coded file.

- First, enable [developer mode](/guides/cms/debugging/troubleshooting#developer-mode-in-the-page-editor) in the page editor by loading the editor with the following query parameter added to the URL: `?developerMode=true`.
- Then, hover over the section you want to copy, then click the **down arrow icon** in the hover toolbar. Select **Copy as HubL**. The HubL markup will then be copied to your clipboard.
- To exit developer mode, click **Exit developer mode** in the upper right.
Learn more about [using sections in the page editor](https://knowledge.hubspot.com/website-pages/edit-page-content-in-a-drag-and-drop-area#use-sections).

## Related resources

- [Drag and Drop Areas overview](/guides/cms/content/templates/drag-and-drop/overview)
- [Drag and Drop Areas HubL tags](/reference/cms/hubl/tags/dnd-areas)
- [Using modules in templates](/reference/cms/modules/using-modules-in-templates)
- [Getting started with drag and drop areas](/guides/cms/content/templates/drag-and-drop/tutorial)


# Getting started with drag and drop areas
[Drag and drop areas](/guides/cms/content/templates/drag-and-drop/overview) are sections of a template that act as empty containers that can be modified directly from the page editor. By building drag and drop areas into a template, it enables content creators to add and manage modules in the page editor as needed, rather than relying on static modules built into the template.
In addition to using drag and drop areas as empty drop zones for content creators, you can also pre-populate drag and drop areas with various modules, layouts, and content to act as a starting point.

This tutorial will take you through setting up a simple drag and drop area. For more developer resources on drag and drop areas, see the [boilerplate](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/home.html) for best practices on implementation as well as the [drag and drop area HubL tag reference documentation](/reference/cms/hubl/tags/dnd-areas).
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.

- Drag and drop templates built with the visual 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.

When the page's template is swapped, any existing content added to drag and drop areas of the first template will be retained.
## 1. Create a new HTML template

Create a new HTML template to contain the HubL and HTML which will make up your drag and drop section.
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 are responsible for adding 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).
## 2. Create a drag and drop area

A `dnd_area` is the 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.

This tag on its own will generate a drop zone for content creators to drag modules into within the content creator.

```hubl
{% dnd_area "body_dnd_area" %}
	<!-- generates an empty drag and drop area drop-section -->
{% end_dnd_area %}
```

## 3. Create a section with a module

A `dnd_section` is a top-level row, and can only be a direct child of a `dnd_area`. Sections support a variety of parameters that control default values for stylistic controls content creators have for sections within the content creators.

As an example, the code below creates a section with a background image (`background_image`). This section is centered by a `vertical-alignment` parameter, and has a max width of `1000px` field for child content. For a full list of supported parameters on the drag and drop HubL tags, see the [drag and drop area HubL tag reference](/reference/cms/hubl/tags/dnd-areas) documentation.

To pre-populate the section with content, the code also includes a `dnd_module` tag to include a module by referencing its path. For this example, the `dnd_module` is pulling in HubSpot's default rich text module, as set by the `path` parameter. A default value for the rich text module is specified using the `module_attribute` tag.

```hubl
{% dnd_area "body_dnd_area" %}
    {% dnd_section
        background_image = {
            'backgroundPosition': 'MIDDLE_CENTER',
            'backgroundSize': 'cover',
            'imageUrl': 'https://www.dragndrop.com/bg-image.jpg'
          },
          max_width=1000,
          vertical_alignment='MIDDLE'
    %}
        {% dnd_module path='@hubspot/rich_text' %}
            {% module_attribute "html"%}
                This is your main headline.
                Use this space to tell everyone about what you have to offer.
            {% end_module_attribute %}
        {% end_dnd_module %}
    {% end_dnd_section %}
{% end_dnd_area %}
```

This results in the drag and drop area containing a module that content creators can edit within the content editor. Note how setting the `max_width` on the `dnd_section` is affecting the content.
## 4. Include multiple modules

To include more than one module, use multiple `dnd_module` tags. By setting the `offset` and `width` parameters, which are based off of a 12 column grid, you can place an image module next to the rich text module, as shown below.

```hubl
{% dnd_area "body_dnd_area" %}
    {% dnd_section
        background_image={
            'backgroundPosition': 'MIDDLE_CENTER',
            'backgroundSize': 'cover',
            'imageUrl': 'https://www.dragndrop.com/bg-image.jpg'
        },
        max_width=1000,
        vertical_alignment='MIDDLE'
    %}
        {% dnd_module path='@hubspot/rich_text', width=8, offset=0, label="Rich Text" %}
            {% module_attribute "html"%}
            	<h1>This is your main headline.</h1>
            	<p>Use this space to tell everyone about what you have to offer.</p>
            {% end_module_attribute %}
        {% end_dnd_module %}
        {% dnd_module path='@hubspot/linked_image',
          width=4,
          offset=8,
          img={
            "src": "https://www.dragndrop.com/placeholder-image.jpg",
            "alt": "Stock placeholder image"
          }
        %}
        {% end_dnd_module %}
    {% end_dnd_section %}
{% end_dnd_area %}
```

Now the page has an editable image module as well as a drag handle, allowing content creators to change the width and offset of the modules. Note how setting a `vertical_alignment` on the `dnd_section` is vertically centering our content.
## 5. Incorporate columns and rows

To make the drag and drop area more complex, you can incorporate rows and columns using the `dnd_row` and `dnd_column` tags. Rows and columns act similarly to sections in the content editor, where content creators can drag them around, as well as clone, delete, and style them.

```hubl
{% dnd_area "body_dnd_area" %}
  {% dnd_section
    background_image={
        'backgroundPosition': 'MIDDLE_CENTER',
        'backgroundSize': 'cover',
        'imageUrl': 'https://www.dragndrop.com/bg-image.jpg'
    },
    max_width=1000,
    vertical_alignment='MIDDLE'
  %}
    {% dnd_module path='@hubspot/linked_image',
      width=6,
      img={
        "src": "https://www.dragndrop.com/placeholder-image.jpg",
        "alt": "Stock placeholder image"
      }
    %}
    {% end_dnd_module %}
    {% dnd_column width=6, offset=6 %}
      {% dnd_row
        padding={
            'bottom': 15
        }
      %}
        {% dnd_module path='@hubspot/rich_text' %}
          {% module_attribute "html"%}
              <h1>This is your main headline.</h1>
              <p>Use this space to tell everyone about what you have to offer.</p>
          {% end_module_attribute %}
        {% end_dnd_module %}
      {% end_dnd_row %}
      {% dnd_row %}
        {% dnd_module path='@hubspot/form' %}
        {% end_dnd_module %}
      {% end_dnd_row %}
    {% end_dnd_column %}
  {% end_dnd_section %}
{% end_dnd_area %}
```

Now, content creators will have further stylistic and layout control over specific rows and columns, in addition to modules and sections.
## 6. Set generic drag and drop component styles

The various components of drag and drop areas, sections, columns, rows and modules all have classes which can be styled using CSS. The editable styles and options for these components can be set using CSS rather than HubL. For example, default padding can be set on `dnd_sections` with the CSS:

```css
.dnd-section {
  padding: 80px 20px;
}
```

The generic CSS selectors for the drag and drop area components are `.dnd-section`, `.dnd-column`, `.dnd-row` and `.dnd-module`. Aside from these `dnd` prefixed classes, the actual grid class names in the markup are based on bootstrap 2 names. This does not mean you need to use bootstrap 2 with drag and drop areas. When you add a `dnd_area` to your page, you are responsible for providing the styles that make the grid work. An example of layout styles you could implement can be found in the [HubSpot CMS boilerplate](https://github.com/HubSpot/cms-theme-boilerplate/tree/main/src/css/objects). Your stylesheet can be added to the template using [`{{ require_css() }}`](/reference/cms/hubl/tags/standard-tags#require-css).

## Related tutorials

- [Getting started with themes](/guides/cms/content/themes/getting-started)
- [Getting started with custom modules](/guides/cms/content/modules/quickstart)
- [How to add theme capabilities to an existing site](/guides/cms/improve-older-sites/add-theme-features-to-existing-sites)


# Templates overview

Templates define the layout of your HubSpot pages, emails, and themes. A template consists of modules and partials, and can reference other assets such as stylesheets and JavaScript files. Templates can be created either using the [HubSpot CLI](/guides/cms/quickstart) or in [HubSpot's design manager](https://knowledge.hubspot.com/design-manager/create-page-email-and-blog-templates-in-the-layout-editor). For content creators, the template is the first thing they'll select when creating a page or email.

Below, learn how to create a template, the different types of templates, and what's included with your templates.

## Create a template

You can create a template either [in HubSpot](https://knowledge.hubspot.com/design-manager/create-page-email-and-blog-templates-in-the-layout-editor) or by [using the CLI](/guides/cms/quickstart).

- To create a template using the CLI, run the following command:

```shell
hs create template <name> [dest]
```

| Parameter | Description |
| --- | --- |
| `name` | The template's name |
| `dest` | The path of the local directory that you want to create the template in. If not included, the template will be created in the directory you're currently in. |

- Using the arrow keys, navigate to the [type of template](#template-types) you want to create, then hit **enter**.

The template will then be created locally. When you want to make the template available for use in your HubSpot account, [upload](/guides/cms/tools/local-development-cli#upload) the template to your account. You can also use the [watch](/guides/cms/tools/local-development-cli#watch) command to automatically upload all new files and edits to existing files in the current working directory and child directories.

## Preview a template

After updating a template, you can preview it to ensure it looks and acts as you expect. There are a few ways in HubSpot to preview a template, such as:

- **Previewing a template in the design manager:** best for quick visual checks or when needing to preview a blog post/listing/combined template.
- **Creating a new asset from a template:** best for testing the drag and drop editor and content creation experience.

### Preview with the design manager

Previewing templates using the design manager can be especially helpful for quick visual checks. The template previewer also enables you to configure display options, such as viewport dimensions.

To preview a template in the design manager:

- In your HubSpot account, navigate to **Marketing** > **Files and Templates** > **Design Tools**
- Using the left sidebar file explorer, click the **template** you'd like to preview.
- In the upper right, click **Preview**.
  - Select **Live preview with display options** to preview the template with options to test responsiveness and domain settings such as stylesheets. This option displays the page within an iframe. This also enables you to select between blogs and the blog post or listing view for blog templates.
  - Select **Preview without display options** to preview the template without additional options.
### Preview with a new page

When changing drag and drop areas, default parameter values on modules, or other significant changes, it’s best to [create a website page](https://knowledge.hubspot.com/website-and-landing-pages/create-and-customize-pages), [blog](https://knowledge.hubspot.com/blog/create-a-new-blog), [blog post](https://knowledge.hubspot.com/blog/create-and-publish-blog-posts) or [email](https://knowledge.hubspot.com/marketing-email/create-marketing-emails-in-the-drag-and-drop-email-editor) using the template. You can then try out different module field values and test what your template will look like in the real world and optimize for the best content creator experience. You can either publish these assets or leave them in draft mode for testing purposes.

In addition, you can use [content staging](https://knowledge.hubspot.com/website-pages/redesign-and-relaunch-your-site-with-content-staging#new) or a [developer sandbox account](https://offers.hubspot.com/free-cms-developer-sandbox) to create and view assets without impacting a production account.

## Template types

Templates can be used for different types of content, such as website pages and blog posts. In coded templates, you designate the type of template by adding an annotation at the top of the file.

Below, learn about the different types of templates and annotations you can use to designate each type.
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 <u>without</u> `dnd_area` tags can only be swapped for other coded templates without `dnd_area` tags.
### Page

```html
templateType: page
```

Page templates are the most open-ended template type. They can serve as any flavor of web page or [landing page](https://blog.hubspot.com/blog/tabid/6307/bid/7177/what-is-a-landing-page-and-why-should-you-care.aspx). Page [Layouts](#layout-css) are not pre-populated with any components. Coded page templates come pre-populated with sparse markup including suggested HubL tags for meta info, title, and required header/footer [includes](#included-cms-files). Examples of pages that would typically use a page template would include but are not limited to:

- [Homepage](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/home.html)
- [About us](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/about.html)
- [Contact](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/contact.html)

### Email

```html
templateType: email
```

Email templates are used by the [email tool](https://www.hubspot.com/products/marketing/email). They have the most stringent set of requirements since they need to be viewed in many different email clients and conform to best practices to ensure proper deliverability. Both [HTML + HubL](/guides/cms/content/templates/types/html-hubl-templates) and design manager drag and drop email templates come pre-populated with baseline components upon creation. Templates with this `templateType` are only visible for template selection when [creating an email](https://knowledge.hubspot.com/marketing-email/create-and-send-marketing-emails-with-the-updated-classic-editor).

In order to stay [CAN-SPAM](https://knowledge.hubspot.com/marketing-email/can-i-customize-my-can-spam-email-footer) compliant, email templates have a set of [required variables](/reference/cms/hubl/variables#required-email-template-variables) that must be included.

Email templates also have a built-in functionality to [inline-css](/guides/cms/content/templates/types/email-template-markup#hs-inline-css-and-data-hse-inline-css) added to `<style>` elements with a special class name or data attribute. Inlining CSS in emails is a method used to get better support across email clients. Fortunately, [most of the popular email clients now support embedded css](https://litmus.com/blog/do-email-marketers-and-designers-still-need-to-inline-css), that however is not representative of your specific recipients. Use good judgment to do what is right by your recipients.

[Learn more about building email templates.](/guides/cms/content/templates/types/email-template-markup)

### Partial

```html
templateType: page isAvailableForNewContent: false
```

Partials are template files that can be included in other coded files. Unlike global partials, partials can be edited individually without impacting other instances of the partial.

Learn more about [partials](/guides/cms/content/templates/types/html-hubl-templates#partials).

### Global partial

```html
templateType: global_partial
```

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. Updating a global partial will update all instances of the global partial.

Learn more about [global partials](/guides/cms/content/templates/types/html-hubl-templates#global-partials) and [global content](/guides/cms/content/global-content).

### Blog

When [creating a blog](https://knowledge.hubspot.com/blog/create-a-new-blog), [blog templates](/guides/cms/content/templates/types/blog) are similar in structure to standard [page templates](#page-templates). The crucial difference is that they can be selected in [content settings](https://app.hubspot.com/login?loginRedirectUrl=https%3A%2F%2Fapp.hubspot.com%2Fshortlink%2Fsettings%2Fwebsite%2Fpages%2Fall-domains%2Fsystem-pages) as blog templates whereas page templates cannot. Templates created with `blog_listing`, `blog_post`, or `blog`, `templateType` **do not appear** when a user is creating a web page, in the template selection screen. Blog templates actually have two forms, blog listing pages and blog post detail pages.

### Blog listing

```html
templateType: blog_listing
```

The [blog listing template](/guides/cms/content/templates/types/blog) is the template users will see when navigating to the blog's URL. Typically this template is used to list summaries, titles and featured images of all of the posts on the blog, additionally typically displays pagination to get to older posts.

### Blog post

```html
templateType: blog_post
```

The blog post template, is the template users will see when viewing an individual post in the blog. These templates typically show the full post content.

#### Combined blog post and listing template

A single blog template can handle the layout for [both listing and detail pages](/guides/cms/content/templates/types/blog#if-is-listing-view-statement) but more typically these are separated into separate templates. Combined templates will show in the blog settings as selectable for both listing and blog post options. If you are creating a template that is intended to only be used for posts, or listings, you should use `blog_post`, or `blog_listing`, instead.

```html
templateType: blog
```
For a simpler development and content creator experience it is recommended to use the separate `blog_post`, and `blog_listing` `templateTypes` instead of combined templates.
### System pages

System page templates are flagged internally for their specific purpose. In your account's [content settings](https://app.hubspot.com/login?loginRedirectUrl=https%3A%2F%2Fapp.hubspot.com%2Fshortlink%2Fsettings%2Fwebsite%2Fpages%2Fall-domains%2Fsystem-pages), you can select these templates for their specified purpose in the system tab.
The only type of system page template that can be created through the CLI is the search results page template.
#### Error pages

Error pages can be set in [Content Settings](https://app.hubspot.com/login?loginRedirectUrl=https%3A%2F%2Fapp.hubspot.com%2Fshortlink%2Fsettings%2Fwebsite%2Fpages%2Fall-domains%2Fsystem-pages) as either [404](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/system/404.html) or [500](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/system/500.html) pages. Both templates use the same templateType. Templates created with this templateType do not appear when a user is creating a web page, in the template selection screen.

```html
templateType: error_page
```

#### Email subscription preferences

The email subscription preferences page. Lists all of the available subscription types a user can opt into or out of. Required to contain the `{% email_subscriptions "email_subscriptions"  %}` HubL Tag. [See the subscription preferences template in the cms-theme-boilerplate](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/system/subscription-preferences.html).

```html
templateType: email_subscription_preferences_page
```

#### Email backup unsubscribe

A system template for email unsubscribe pages. Required to contain the `{% email_simple_subscription "email_simple_subscription" %}` HubL tag. [See the email backup unsubscribe template in the cms-theme-boilerplate.](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/system/backup-unsubscribe.html)

```html
templateType: email_backup_unsubscribe_page
```

#### Email unsubscribe confirmation

A system template for email unsubscribe confirmation pages. This is where users are sent when they go to the URL generated by the [`{{ unsubscribe_link_all }}`](/reference/cms/hubl/variables#email-variables) variable. [See the subscription confirmation template in the cms-theme-boilerplate](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/system/subscriptions-confirmation.html).

```html
templateType: email_subscriptions_confirmation_page
```

#### Password prompt

Password prompt templates provide a branded page content creators can display to require a password before a visitor can view the page’s actual content. Password prompt templates are set via [Content Settings](https://app.hubspot.com/login?loginRedirectUrl=https%3A%2F%2Fapp.hubspot.com%2Fshortlink%2Fsettings%2Fwebsite%2Fpages%2Fall-domains%2Fsystem-pages). [How to make a page on HubSpot password protected](https://knowledge.hubspot.com/website-pages/password-protect-a-page). See the [password protected page prompt](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/system/password-prompt.html) in the boilerplate.

```html
templateType: password_prompt_page
```

#### Search results page

A system template for the built-in CMS site search listing functionality. See the [search results page template](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/system/search-results.html) in the boilerplate.

```html
templateType: search_results_page
```

### Membership

HubSpot accounts with the [memberships functionality](/guides/cms/content/memberships/overview) (**CMS Hub** Enterprise _only_) can [create pages on their sites that only users apart of specific lists in the CRM can access](https://knowledge.hubspot.com/website-pages/require-member-registration-to-access-private-content). This enables site visitors to have accounts with login credentials. These templates give you control over the appearance of these pages.
Only [HTML + HubL templates](/guides/cms/content/templates/types/html-hubl-templates) can be membership templates.
#### Membership login

This is the login page that displays when a user tries to access content that is access controlled through the memberships functionality. Typically contains the `{% member_login "member_login" %}` module. See the example [membership login template](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/system/membership-login.html) in the boilerplate.

```html
templateType: membership_login_page
```

#### Membership register

This is the user registration page that allows users to create an account to view the content that users of this list can access. Typically contains the `{% member_register "member_register" %}` HubL Tag. See the example [membership registration template](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/system/membership-register.html) in the boilerplate.

```html
templateType: membership_register_page
```

#### Membership password reset

This is the password reset page. Users provide their new password on this page. Typically contains the `{% password_reset "password_reset" %}` HubL Tag. See the example [membership password reset template](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/system/membership-reset-password.html) in the boilerplate.

```html
templateType: membership_reset_page
```

#### Membership reset request

This is the request a password reset page. Displaying a form to request a password reset email. Typically contains the `{% password_reset_request "password_reset_request" %}` HubL Tag. See the example [membership password reset request template](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/system/membership-reset-password-request.html) in the boilerplate.

```html
templateType: membership_reset_request_page
```

## Included CMS files

There are certain JavaScript and CSS files that are attached to CMS templates. Some files are automatically included and cannot be removed, while others can be optionally included. To learn more about the order in which stylesheets are attached to CMS content, [check out this article](https://knowledge.hubspot.com/design-manager/create-edit-and-attach-css-files-to-style-your-site).

### jQuery

[jQuery](https://jquery.com/) is optionally included in the head tag of HubSpot templates. If included, it is rendered as part of the [`standard_header_includes`](/reference/cms/hubl/variables#required-page-template-variables) HubL variable.

In **Settings > Website > Pages**, you can change the jQuery version to 1.11.x, version 1.7.1, or disable it completely. You can also choose to include a jQuery migrate script for backward compatibility with older browsers. You can move jQuery to the footer to improve page performance, but moving jQuery can break JavaScript relying on it. It's recommended to test this out before moving it by adding `?hsMoveJQueryToFooter=True` to the end of your website page URLs.

While jQuery was historically included by default, currently CMS Hub does not require jQuery. Most of jQuery's functionality now has [modern vanilla javascript equivalents](http://youmightnotneedjquery.com/), and it's recommended to use them instead. If you need to use jQuery, we encourage disabling the default version in settings and using the latest version loaded above the `</body>` tag.

To test if removing jQuery on your site will break anything, add `?hsNoJQuery=true` to the end of the URL while viewing your site, especially pages with heavy interactivity.

### layout.css

Formerly known as `required_base.css`, this file is responsible for styling HubSpot's responsive grid. This file is automatically included in any drag and drop template, but is <u>not</u> included by default in custom coded templates. When using [dnd_area](/reference/cms/hubl/tags/dnd-areas) tags in coded HTML + HubL templates, you don't need to load the `layout.css` file, but a version of it is [included in the CMS boilerplate](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/css/objects/_layout.css) to make it easier to get up and running quickly.

In addition to the responsive grid CSS, the file includes some classes that can be used to show and hide elements at different viewports. To learn more, [view the file directly](https://cdn2.hubspot.net/hubfs/327485/Designers_Docs_2015/layout.css).

### HubSpot tracking code

The [HubSpot tracking code](https://knowledge.hubspot.com/account/how-does-hubspot-track-visitors) is automatically added to any HubSpot template, excluding email templates, with the [**standard_footer_includes**](/reference/cms/hubl/variables) HubL variable. The tracking code loads an analytics JavaScript file named `your_HubID.js` (example: `158015.js`). This tracking code is directly integrated with [HubSpot's GDPR functionality](https://knowledge.hubspot.com/privacy-and-consent/how-do-i-turn-on-gdpr-functionality-in-my-hubspot-account).

## Related resources

- [Drag and Drop Areas](/guides/cms/content/templates/drag-and-drop/overview)
- [Section templates](/guides/cms/content/templates/drag-and-drop/sections)
- [How to use web components in templates, and modules](https://developers.hubspot.com/blog/use-web-components-in-hubspot-cms-development)
- [Build headers footers and more using global content](/guides/cms/content/global-content)


# Blog templates
HubSpot blogs consist of blog listing pages and the individual blog posts. In addition to listing the individual blog posts, the blog listing template is also used for rendering the author and tag listing pages. You can either create a single template to render all listing and blog post pages, or you can create two separate templates.

Below, learn about blog template markup, template components, and customization options.

## Create a shared template for the listing and post pages

To create one template that renders the listing and post pages, add the `templateType: blog` [annotation](/guides/cms/content/templates/types/html-hubl-templates#template-annotations) to the top of your template file. When using one template to render both, you'll use an [if statement](/reference/cms/hubl/if-statements) that evaluates whether the user is looking at a listing page or an individual post. If you are using the [drag and drop design manager layouts](/guides/cms/content/templates/types/drag-and-drop-templates), this `if` statement is built into the UI of blog content module buttons.

By using the `if is_listing_view` statement, you can write your post and listing code separately.

```hubl
{% if is_listing_view %}
    Markup for blog listing template
{% else %}
    Markup for blog post template
{% endif %}
```

## Create separate listing and post templates

Alternatively, you can choose to have separate templates for [blog post](/guides/cms/content/templates/overview#blog-post) and [listing pages](/guides/cms/content/templates/overview#blog-listing) which can help make your code cleaner and easier to read as a developer, while making the templates easier to select for content creators. Rather than using the `templateType: blog` annotation at the top of the one template, include the following [annotations](/guides/cms/content/templates/overview#template-types) at the top of your two templates:

- **Blog post template:** `templateType: blog_post`
- **Blog listing template:** `templateType: blog_listing`
When building separate post and listing templates, the `is_listing_view` check is not required. Instead, you'll manually [select separate templates](https://knowledge.hubspot.com/blog/manage-your-blog-template-and-settings#select-your-blog-templates) within the account's blog settings.

You can also [migrate an existing unified blog template to be either a blog post template or blog listing template](https://developers.hubspot.com/docs/cms/guides/migrate/blog-to-blog_listing).

### Listing page templates

The `templateType: blog_listing` annotation makes the template available for [selection under blog settings](https://knowledge.hubspot.com/blog/manage-your-blog-template-and-settings) specifically for the listing view. With this template type, content creators can also edit the listing page within the page editor. By also including [drag and drop areas](/guides/cms/content/templates/drag-and-drop/overview) in the template, modules can be added and removed in the page editor like they can for other CMS pages. Check out the [CMS boilerplate blog templates](https://github.com/HubSpot/cms-theme-boilerplate/pull/349) to see examples of including drag and drop areas.

The listing of posts is generated by a [for loop](/reference/cms/hubl/loops) that iterates through your blog posts. `contents` is a predefined sequence of content that contains all the posts contained in that blog.

```hubl
{% for content in contents %}

{% endfor %}
```
It's recommended to make all text strings on your blog listing template controlled by fields. This makes it easier to create [multilingual](/guides/cms/content/multi-language-content) blogs and gives content creators more control.
## Create a blog listing module

You can enable content creators to place modules on the perimeter of the blog listing content, such as on the sides, or above and below. To enable this, it's recommended to create a blog listing module that uses a [blog listing for loop](/guides/cms/content/templates/types/blog#blog-listing-for-loop). Check out the [CMS boilerplate blog listing module](https://github.com/HubSpot/cms-theme-boilerplate/tree/main/src/modules/blog-listings.module) for an example. [](https://github.com/HubSpot/cms-theme-boilerplate/tree/main/src/modules/blog-listings.module)

While HubSpot provides [blog settings](https://knowledge.hubspot.com/blog/manage-your-blog-template-and-settings#select-your-blog-templates) for showing summaries and using featured images, you can also build these features into your module. This enables content creators to set these features within the page editor, rather than blog settings.

## Blog author, tag, and simple listing pages

In additional to the blog post and blog listing pages, HubSpot blogs also have pages for blog authors, blog post tags, and simple listing pages. These additional pages use the same template as the blog listing page to render their content.
Because the listing page template is also shared by the blog author, tag, and simple listing page, updates published to the template will also apply to those pages.
To configure the layout of these pages individually, you can use `if` statements to conditionally render content for each type of page.

### If blog_author

Within the standard HubSpot blog listing markup, there is an `if blog_author` statement. This statement evaluates as true when viewing an author's page, which lists the posts published by the author. The boilerplate template includes the author's name, bio, and social media accounts.

```hubl
{% if blog_author %}

      {% endif %}
    </div>
  </div>
{% else %}
```

### If tag

You can use an `if tag` statement to only render code on a blog topic listing page, which visitors can see when clicking a blog topic on your site. The example below is a snippet that uses the page title variable to automatically print the tag name at the top of a tag listing page.

```hubl
{% if tag %}
    <h3>Posts about {{ page_meta.html_title|split(" | ")|last }}</h3>
{% endif %}
```

### If not simple_list_page

There are two types of blog listing pages that can be rendered to display blog post listings: the regular listing page, and a simple listing page:

- The regular listing iterates through the number of posts specified by the post listing blog setting and paginates accordingly.
- A simple listing is a listing of all your posts and does not support pagination. The simple listing is not affected by the [post limit blog setting](/guides/cms/content/website-settings#number-of-posts-per-listing-page) and generally just contains links to the most recent 200 blog posts. The address of your simple listing page is the URL for your blog with `/all` added to the end of the path.

You can use an `if not simple_list_page` statement to determine what to render in a simple versus regular listing. A simplified version of this statement is shown below.
Note that the `if` statement uses reverse logic, which means that the `else` defines the simple listing view. Optionally, you could use an [unless statement](/reference/cms/hubl/if-statements#unless-statements) instead.
```hubl
{% if not simple_list_page %}
    Iterated post markup for regular listing
{% else %}
    <h2 class="post-listing-simple"><a href="{{content.absolute_url}}">{{ content.name }}</a></h2>
{% endif %}
```

## Listing pagination

Blog listing pages have auto-generated pagination. Your listing template can include logic to allow visitors to easily pages through your blog posts. The [boilerplate blog](https://boilerplate.hubspotcms.com/blog) accomplishes simple, number pagination through the [following markup](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/modules/blog-pagination.module/module.html):

```hubl
{% if contents.total_page_count > 1 %}

{% endif %}
```

## Boilerplate markup

Below, view the boilerplate markup for the blog post and blog listing page templates. You can also view this markup in the CMS boilerplate on GitHub, as listed in each section.

### Post template markup

All blog posts in a blog are generated by a single blog template. `Content` is a predefined object of data that contains information about the requested blog post. [Boilerplate posts](https://boilerplate.hubspotcms.com/blog) are rendered with the [following markup](https://github.com/HubSpot/cms-theme-boilerplate/blob/master/src/templates/blog-post.html):

```hubl

    </div>

    {% if content.tag_list %}

    {% endif %}
  </div>

</div>
```

Blog post author information is also available within the `content` data.

```hubl
<img alt="{{ content.blog_post_author.display_name }}" src="{{ content.blog_post_author.avatar }}">
<h3>Written by <a class="author-link" href="{{ blog_author_url(group.id, content.blog_post_author.slug) }}">{{ content.blog_post_author.display_name }}</a></h3>
<p>{{ content.blog_post_author.bio }}</p>
{% if content.blog_post_author.has_social_profiles %}

    </div>
{% endif %}
```

### Listing template markup

The [boilerplate blog listing page](https://boilerplate.hubspotcms.com/blog) contents for loop is rendered with the [following markup](https://github.com/HubSpot/cms-theme-boilerplate/blob/master/src/templates/blog-index.html):

```hubl
{% for content in contents %}
    {# On the blog homepage the first post will be featured above older posts #}
    {% if (loop.first && current_page_num == 1 && !topic) %}

    </div>
    {% else %}

    </div>
    {% endif %}
{% endfor %}
```

## Related resources

- [Blog variables](/reference/cms/hubl/variables#blog-variables)
- [Blog templates](/guides/cms/content/templates/types/blog)
- [HubSpot Theme Boilerplate](/guides/cms/content/themes/hubspot-cms-boilerplate)
- [How to create a blog](https://knowledge.hubspot.com/blog/create-a-new-blog)
- [Import your blog into HubSpot](https://knowledge.hubspot.com/blog/import-a-blog-into-hubspot)


# Drag and drop templates
**Drag and drop templates are not recommended for new templates.** These templates are not able to be part of a theme, and so they do not support theme functionality like theme fields, and theme modules. Drag and drop templates are **NOT** supported in CMS Hub Starter, use `dnd_area` instead. Drag and drop templates do not support several of the newer features of the CMS ([memberships](/guides/cms/content/memberships/overview), [drag and drop areas](/guides/cms/content/templates/drag-and-drop/overview), [reusable sections](/guides/cms/content/templates/drag-and-drop/sections), GraphQL and many other features). They lack extensibility and do not provide as good of a content creator and developer experience as coded templates. Instead, we recommend coded [HTML + HubL templates](/guides/cms/content/templates/types/html-hubl-templates) with [drag and drop areas](/guides/cms/content/templates/drag-and-drop/overview), which deliver a better experience for developers and marketers. For a quick start on the CMS, we recommend checking out the [HubSpot Theme Boilerplate](https://github.com/HubSpot/cms-theme-boilerplate) which is built using coded templates.
Drag and drop [templates](/guides/cms/content/templates/overview) were designed for less technical users to be able to easily create a website on the HubSpot CMS. Drag and Drop templates take advantage of a visual template builder, which generates HTML + HubL under the hood.

In order to make the visual builder work the HTML output is controlled by HubSpot and you do not have direct control over the structural portions of it. In-addition **a layout.css file is loaded onto the page which enables a basic 12 column grid** based on bootstrap 2. This makes all sites built with drag and drop templates responsive by default causing rows of content to stack vertically, for more complicated and custom responsiveness you'll want to add your own responsive styles.

Drag and Drop templates are built in the Design Manager, when interacting with them via the API or the [local development tools](/guides/cms/setup/getting-started-with-local-development) they are represented as JSON. Due to the nature of drag and drop templates the only recommended way to edit them is through the Design Manager interface. **If you prefer to code, use version control or simply want to be able to edit using your preferred tools locally, HTML + HubL coded templates offer the best experience for developers.** The [dnd_area](/reference/cms/hubl/tags/dnd-areas) functionality for HTML + HubL templates also provides a better experience for content creators than the design manager drag and drop interface as it keeps them in the visual content editor.

## The Drag and Drop Template Builder

To create a drag and drop template, open the Design Manager and in the finder, create a new file, choose a template, and the [type of template](/guides/cms/content/templates/overview#template-types) you're creating.

Drag and drop templates are made up of building blocks, these blocks are modules, groups, global groups, and flexible columns.

[Learn how to use the Drag and Drop template builder.](https://knowledge.hubspot.com/structure-and-customize-template-layouts)

### Modules

Modules are reusable components that are editable portions of a page. Modules are made up of an HTML + HubL template fragment, CSS, JS, and fields. Modules can be placed inside of flexible columns and dnd_areas by content editors, and are the primary way that content editors manage the content of their website. You can build modules to handle any number of functions, search, image galleries, menus, complex marketing animations, calculators, product comparisons, the possibilities are down to your imagination and what you think provides a good experience for content creators. The fields are how the content editor controls their output. Modules are not unique to drag and drop templates, they are a very important core building block for the HubSpot CMS. In Drag and Drop templates the default values for module fields can be configured in the [inspector](https://knowledge.hubspot.com/cos-general/use-the-inspector-to-style-your-template). [Learn more about Modules.](/guides/cms/content/modules/overview)
### Groups

Groups are wrappers for other groups and modules, they can have CSS classes, and wrapping HTML. Groups manifest themselves as wrapping HTML with layout classes to facilitate placement and sizing of the groups. Groups can also have an internal-only name. With this, you can [group modules](https://knowledge.hubspot.com/structure-and-customize-template-layouts#group-modules) in the Design Manager making it easier to visually identify parts of a page. An example of this might be for sidebars or banners.
Being that [HTML + HubL](/guides/cms/content/templates/types/html-hubl-templates) files are the recommended path for new sites, columns, sections, and rows of [Drag and drop areas](/guides/cms/content/templates/drag-and-drop/overview) largely replace the purposes of groups.

Additionally a developer can create partials, and [global partials](/guides/cms/content/global-content), which can contain freeform code in-addition to drag and drop areas.

Drag and drop areas, partials, and global partials are not supported in drag and drop templates.
### Global Groups

Global groups are groups that when edited affect the entire website and not just the page they are on. Global groups can exist in multiple templates and most commonly are used for site headers and footers. Global groups are similar to partials but are limited to the structure that drag and drop templates enforce. They can be embedded into HTML + HubL files if needed, but most of the time it makes more sense to use a global partial instead.
### Flexible columns

Flexible columns are a special type of group. They can contain a default set of modules, but content editors can add, remove, and move modules within them. Flexible columns allow reordering of modules in a one dimensional way, vertically up and down. [Unlike dnd_area tags](/guides/cms/content/templates/drag-and-drop/overview), **flexible columns do not support sections, or the styling capabilities, afforded to modules within them.** Flexible columns are not unique to drag and drop templates, there is a [HubL tag](/reference/cms/hubl/tags/standard-tags#flexible-column) which can be used in [HTML + HubL](/guides/cms/content/templates/types/html-hubl-templates) templates. [Learn how to add a flexible column to a drag and drop template.](https://knowledge.hubspot.com/structure-and-customize-template-layouts#add-a-flexible-column)
It is generally recommended to use drag and drop areas, as most of the time they are more useful and provide all of the capabilities flexible columns provide.

There may still be useful times to use a flexible column, for something like a sidebar. For main content areas drag and drop areas are much more flexible.
Why use flexible columns? Websites are not usually rigid. They are built and maintained over long periods of time. A homepage for example for a business may show off featured products, and may frequently change as the business's marketing needs change over time. Since content within a flex column can be added/removed and modified by content editors, the experience is less painful for marketers and enables developers to focus on what they enjoy, building cool stuff instead of making minor page adjustments.

Similarly, pages throughout a site may have different structural needs. Flex columns give the marketers control to create those pages using custom modules.
### Add custom code to your drag and drop templates

There are a handful of ways to add custom code to your drag and drop templates. The primary method is through custom modules. Sometimes though you may need to add code that wraps around modules or groups. In order to do this, click on the module or group to view it in the inspector and find the wrapping HTML field. Add your HTML there.

Sometimes you may need to add code to the head of your HTML or code right above the `</body>`. With your template open make sure you don't have anything selected. The inspector will show fields for the template itself. Here you can link Stylesheets, and javascript files while also adding additional HTML in the head or just before the `</body>` tag.

For this you'll use the [inspector](https://knowledge.hubspot.com/cos-general/use-the-inspector-to-style-your-template). The inspector has fields at the template level for [stylesheets, javascript](https://knowledge.hubspot.com/cos-general/use-the-inspector-to-style-your-template##attach-stylesheets-and-javascript-files), [`<head>` markup](https://knowledge.hubspot.com/cos-general/use-the-inspector-to-style-your-template#customize-your-template-s-head-and-body-options), etc.


# Email template markup
In this article, learn about how standard HubSpot email templates are coded. When building custom email templates, keep in mind that email clients only support [certain HTML and CSS features](http://www.campaignmonitor.com/css/). Because of this, coding email templates that render consistently across clients requires a fair amount of experience and patience.

Check out HubSpot's Knowledge Base to learn how to [create template layouts in-app](https://knowledge.hubspot.com/design-manager/create-page-email-and-blog-templates-in-the-layout-editor) or how to [save an existing email as a template](https://knowledge.hubspot.com/marketing-email/save-your-marketing-email-as-a-template).

## Required email template variables

In order to send emails with HubSpot, your templates must include certain information. See the [required email HubL variables](/reference/cms/hubl/variables#required-email-template-variables). When working with coded templates, you will also want to use [modules](/guides/cms/content/modules/overview) to ensure email content can be easily edited on a per-email basis.

## hs-inline-css and data-hse-inline-css

One challenging aspect of coding email templates that render correctly, across all clients, is [the lack of support for CSS](http://www.campaignmonitor.com/css/) in a `<style>` within the `<head>`.

To make coding email templates easier, HubSpot coded email templates support a special style tag that gives designers the ability to write CSS that will be compiled and converted into inline CSS and will be added to the targeted elements. Any code added to a style tag with the ID of `hs-inline-css`, will be added to the targeted tags.

For example, Microsoft Outlook will apply a default font-family to all text contained in `<td>` tags, unless you specify a font-family inline for that table column. The example below uses a `hs-inline-css` style tag to add a font-family to all the table columns in the template. Please note that any media queries should be included in a separate `<style>`, as they can not be made inline.

Note that the `data-hse-inline-css` attribute on a `<style>` tag in the **Edit** > **Edit Head** section of drag and drop templates is in place of `hs-inline-css` to achieve this same goal. In coded files, either method may be used (as long as there is only one `style#hs-inline-css` per template). You may have multiple `style[data-hs-inline-css="true"]` elements.
```hubl
<!doctype html>
<html>
    <head>
   {# coded email templates ONLY: #}
        <style type="text/css" id="hs-inline-css">
            td {
                font-family: Arial, sans-serif;
            }
        </style>
    {# coded OR drag-and-drop email templates: #}
        <style type="text/css" data-hse-inline-css="true">
            table {
                font-family: 'Courier New', sans-serif;
            }
        </style>
    </head>
    <body>
        <table>
            <tr>
                <td>Column 1</td>
                <td>Column 2</td>
            </tr>
        </table>
    </body>
</html>
```
```html
<!doctype html>
<html>
  <head>
    <style type="text/css" id="hs-inline-css">
      td {
        font-family: Arial, sans-serif;
      }
    </style>
    <style
      type="text/css"
      data-hse-inline-css="true"
      data-hse-element-inlined="true"
    >
      table {
        font-family: 'Courier New', sans-serif;
      }
    </style>
  </head>
  <body>
    <table style="font-family:'Courier New', sans-serif">
      <tr>
        <td style="font-family: Arial, sans-serif">Column 1</td>
        <td style="font-family: Arial, sans-serif">Column 2</td>
      </tr>
    </table>
  </body>
</html>
```
## Responsive email template

HubSpot's default email template layout uses the markup below. These responsive layouts include media queries that make the images and tables responsive. The responsive layouts use [color and font variables](/reference/cms/hubl/variables#color-and-font-settings) that connect to **Settings > Marketing > Email**.
When working with HubSpot's responsive layout, any `<td>` with a class that includes the text "column" will be made responsive.
```hubl
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>{% if content.html_title and content.html_title != "" %}{{ content.html_title }}{% else %}{{ content.body.subject }}{% endif %}</title>
        <meta property="og:title" content="{% if content.html_title and content.html_title != "" %}{{ content.html_title }}{% else %}{{ content.body.subject }}{% endif %}">
        <meta name="twitter:title" content="{% if content.html_title and content.html_title != ""%}{{ content.html_title }}{% else %}{{ content.body.subject }}{% endif %}">
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        {% if content.meta_description %}<meta name="description" content="{{ content.meta_description }}"/>{% endif %}
         <style type="text/css" id="hs-inline-css">
        /*<![CDATA[*/
            /* everything in this node will be inlined */

            /* ==== Page Styles ==== */

            body, #backgroundTable {
                background-color: {{ background_color }}; /* Use body to determine background color */
                font-family: {{ primary_font }};
            }

            #templateTable {
                width: {{ email_body_width }}px;
                background-color: {{ body_color }};
                -webkit-font-smoothing: antialiased;
            }

            h1, .h1, h2, .h2, h3, .h3, h4, .h4, h5, .h5, h6, .h6 {
                color:{{ primary_font_color }};
                display:block;
                font-family: {{ primary_font }};
                font-weight:bold;
                line-height:100%;
                margin-top:0;
                margin-right:0;
                margin-bottom:10px;
                margin-left:0;
                text-align:left;
            }

            h1, .h1 {
                font-size:26px;
            }

            h2, .h2 {
                font-size:20px;
            }

            h3, .h3 {
                font-size:15px;
            }

            h4, .h4 {
                font-size:13px;
            }

            h5, .h5 {
                font-size:11px;
            }

            h6, .h6 {
                font-size:10px;
            }

            /* ==== Header Styles ==== */

            #headerTable {
                background-color: {{ background_color }};
                color:{{ primary_font_color }};
                font-family:{{ primary_font }};
                font-size:10px;
                line-height:120%;
                text-align:right;
                border-collapse: separate !important;
                padding-right: {{email_body_padding}}px;
            }

            #headerTable a:link, #headerTable a:visited, /* Yahoo! Mail Override */ #headerTable a .yshortcuts /* Yahoo! Mail Override */{
                font-weight:normal;
                text-decoration:underline;
            }

            /* ==== Template Wrapper Styles ==== */

            #contentCell {
                padding: 10px 20px;
                background-color: {{ background_color }};
            }

            #contentTableOuter {
                border-collapse: separate !important;

                background-color: {{ body_color }};
                {% if email_main_body_box_shadow_css and email_main_body_box_shadow_css != "" %}
                box-shadow: {{ email_main_body_box_shadow_css }};
                {% endif %}

                padding: {{email_body_padding}}px;
            }

            #contentTableInner {
                width: {{email_body_width}}px;
            }

            /* ==== Body Styles ==== */

            .bodyContent {
                color:{{ primary_font_color }};
                font-family:{{ primary_font }};
                font-size: {{primary_font_size }};
                line-height:150%;
                text-align:left;
            }

            /* ==== Column Styles ==== */

            table.columnContentTable {
                border-collapse:separate !important;
                border-spacing:0;

                background-color: {{ body_color }};
            }

            td[class~="columnContent"] {
                color:{{ primary_font_color }};
                font-family:{{ primary_font }};
                font-size:{{primary_font_size }};
                line-height:120%;
                padding-top:20px;
                padding-right:20px;
                padding-bottom:20px;
                padding-left:20px;
            }

            /* ==== Footer Styles ==== */

            #footerTable {
                background-color: {{ background_color }};
            }

            #footerTable a {
                color: {{ secondary_font_color }};
            }

            #footerTable {
                color:{{ secondary_font_color }};
                font-family:{{ primary_font }};
                font-size:12px;
                line-height:120%;
                padding-top:20px;
                padding-right:20px;
                padding-bottom:20px;
                padding-left:20px;
                text-align:center;
            }

            #footerTable a:link, #footerTable a:visited, /* Yahoo! Mail Override */ #footerTable a .yshortcuts /* Yahoo! Mail Override */{
                font-weight:normal;
                text-decoration:underline;
            }

            .hs-image-social-sharing-24 {
                max-width: 24px;
                max-height: 24px;
            }

            /* ==== Standard Resets ==== */
            .ExternalClass{
                width:100%;
            } /* Force Hotmail to display emails at full width */
            .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {
                line-height: 100%;
            } /* Force Hotmail to display normal line spacing */
            body, table, td, p, a, li, blockquote{
                -webkit-text-size-adjust:100%;
                -ms-text-size-adjust:100%;
            } /* Prevent WebKit and Windows mobile changing default text sizes */
            table, td {
                mso-table-lspace:0pt;
                mso-table-rspace:0pt;
            } /* Remove spacing between tables in Outlook 2007 and up */
            img {
                vertical-align: bottom;
                -ms-interpolation-mode:bicubic;
            } /* Allow smoother rendering of resized image in Internet Explorer */

            /* Reset Styles */
            body {
                margin:0;
                padding:0;
            }
            table {
                border-collapse:collapse !important;
            }
            body, #backgroundTable, #bodyCell{
                height:100% !important;
                margin:0;
                padding:0;
                width:100% !important;
            }
            a:link, a:visited {
                border-bottom: none;
            }

            /* iOS automatically adds a link to addresses */
            /* Style the footer with the same color as the footer text */
            #footer a {
                color: {{ secondary_font_color }};;
                -webkit-text-size-adjust: none;
                text-decoration: underline;
                font-weight: normal
            }

            /* Hide preview text on rendering */
            #preview_text {
                display: none;
            }
        /*]]>*/
    </style>
         <style type="text/css">
        /*<![CDATA[*/
            /* ==== Mobile Styles ==== */

            /* Constrain email width for small screens */
            @media screen and (max-width: 650px) {
                table[id="backgroundTable"] {
                    width: 95% !important;
                }

                table[id="templateTable"] {
                    max-width:{{ email_body_width }}px !important;
                    width:100% !important;
                }

                table[id="contentTableInner"] {
                    max-width:{{ email_body_width }}px !important;
                    width:100% !important;
                }

                /* Makes image expand to take 100% of width*/
                img {
                    width: 100% !important;
                    height: auto !important;
                }

                #contentCell {
                    padding: 10px 10px !important;
                }

                #headerTable {
                    padding-right: {{email_body_padding_num / 2 |int}}px !important;
                }

                #contentTableOuter {
                    padding: {{email_body_padding_num / 2 |int}}px !important;
                }
            }

            @media only screen and (max-width: 480px) {
                /* ==== Client-Specific Mobile Styles ==== */
                body, table, td, p, a, li, blockquote{
                    -webkit-text-size-adjust:none !important;
                } /* Prevent Webkit platforms from changing default text sizes */
                body{
                    width:100% !important;
                    min-width:100% !important;
                } /* Prevent iOS Mail from adding padding to the body */

                /* ==== Mobile Reset Styles ==== */
                td[id="bodyCell"] {
                    padding:10px !important;
                }

                /* ==== Mobile Template Styles ==== */

                table[id="templateTable"] {
                    max-width:{{ email_body_width }}px !important;
                    width:100% !important;
                }

                table[id="contentTableInner"] {
                    max-width:{{ email_body_width }}px !important;
                    width:100% !important;
                }

                /* ==== Image Alignment Styles ==== */

                h1, .h1 {
                    font-size:26px !important;
                    line-height:125% !important;
                }

                h2, .h2 {
                    font-size:20px !important;
                    line-height:125% !important;
                }

                h3, .h3 {
                    font-size:15px !important;
                    line-height:125% !important;
                }

                h4, .h4 {
                    font-size:13px !important;
                    line-height:125% !important;
                }

                h5, .h5 {
                    font-size:11px !important;
                    line-height:125% !important;
                }

                h6, .h6 {
                    font-size:10px !important;
                    line-height:125% !important;
                }

                .hide {
                    display:none !important;
                } /* Hide to save space */

                /* ==== Body Styles ==== */

                td[class="bodyContent"] {
                    font-size:16px !important;
                    line-height:145% !important;
                }

                /* ==== Footer Styles ==== */

                td[id="footerTable"]{
                    padding-left: 0px !important;
                    padding-right: 0px !important;
                    font-size:12px !important;
                    line-height:145% !important;
                }

                /* ==== Image Alignment Styles ==== */

                table[class="alignImageTable"] {
                    width: 100% !important;
                }

                td[class="imageTableTop"] {
                    display: none !important;
                    /*padding-top: 10px !important;*/
                }
                td[class="imageTableRight"] {
                    display: none !important;
                }
                td[class="imageTableBottom"] {
                    padding-bottom: 10px !important;
                }
                td[class="imageTableLeft"] {
                    display: none !important;
                }

                /* ==== Column Styles ==== */

                td[class~="column"] {
                    display: block !important;
                    width: 100% !important;
                    padding-top: 0 !important;
                    padding-right: 0 !important;
                    padding-bottom: 0 !important;
                    padding-left: 0 !important;
                }

                td[class~=columnContent] {
                    font-size:14px !important;
                    line-height:145% !important;

                    padding-top: 10px !important;
                    padding-right: 10px !important;
                    padding-bottom: 10px !important;
                    padding-left: 10px !important;
                }

                #contentCell {
                    padding: 10px 0px !important;
                }

                #headerTable {
                    padding-right: {{email_body_padding_num / 2 |int}}px !important;
                }

                #contentTableOuter {
                    padding: {{email_body_padding_num / 2 |int}}px !important;
                }
            }

            #preview_text {
                display: none;
            }
        /*]]>*/
    </style>
        <!-- http://www.emailon@cid.com/blog/details/C13/ensure_that_your_entire_email_is_rendered_by_default_in_the_iphone_ipad -->
        <!--                                                                                                                     -->
        <!--                                                                                                                     -->
        <!--                            _/    _/            _/          _/_/_/                        _/                         -->
        <!--                           _/    _/  _/    _/  _/_/_/    _/        _/_/_/      _/_/    _/_/_/_/                      -->
        <!--                          _/_/_/_/  _/    _/  _/    _/    _/_/    _/    _/  _/    _/    _/                           -->
        <!--                         _/    _/  _/    _/  _/    _/        _/  _/    _/  _/    _/    _/                            -->
        <!--                        _/    _/    _/_/_/  _/_/_/    _/_/_/    _/_/_/      _/_/        _/_/                         -->
        <!--                                                               _/                                                    -->
        <!--                                                              _/                                                     -->
        <!--                                                                                                                     -->
        <!--                                                 Extra White Space!                                                  -->
        <!--                                                                                                                     -->
        <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
    </head>
    <body leftmargin="0" marginwidth="0" topmargin="0" marginheight="0" offset="0">
        <!-- Preview text (text which appears right after subject) -->
        <!--  The  backgroundTable table manages the color of the background and then the templateTable maintains the body of
        the email template, including preheader & footer. This is the only table you set the width of to, everything else is set to
        100% and in the CSS above. Having the width here within the table is just a small win for Lotus Notes. -->

        <!-- Begin backgroundTable -->
        <table align="center" bgcolor="{{ background_color }}" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="backgroundTable">
            <tr>
                <td align="center" valign="top" id="bodyCell"> <!-- When nesting tables within a TD, align center keeps it well, centered. -->
                    <!-- Begin Template Container -->
                    <!-- This holds everything together in a nice container -->
                    <table border="0" cellpadding="0" cellspacing="0" id="templateTable">
                        <tr>
                            <td align="center" valign="top">
                                <!-- Begin Template Preheader -->
                                <table border="0" cellpadding="0" cellspacing="0" width="100%" id="headerTable">

{% endif %}

                            </div><!--end widget-span -->
                        </td>
                    </tr>
                </table>
            </td>
        </tr>
</div><!--end header wrapper -->
                                </table>
                                <!-- End Template Preheader -->
                            </td>
                        </tr>
                        <tr>
                            <td align="center" valign="top" id="contentCell">
                                <!-- Begin Template Wrapper -->
                                <!-- This separates the preheader which usually contains the "open in browser, etc" content
                                from the actual body of the email. Can alternatively contain the footer too, but I choose not
                                to so that it stays outside of the border. -->
                                <table border="0" cellpadding="0" cellspacing="0" width="100%" id="contentTableOuter" style="{{email_body_border_css}}">
                                    <tr>
                                        <td align="center" valign="top">
                                            <table border="0" cellpadding="0" cellspacing="0" id="contentTableInner">
<!--end layout-widget-wrapper -->
                            </div><!--end widget-span -->
                        </td>
                    </tr>
                </table>
            </td>
        </tr>
        <tr>
            <td align="center" valign="top" class="bodyContent" width="100%" colspan="12">
                <table cellpadding="0" cellspacing="0" border="0" width="100%" class="templateColumnWrapper">
                    <tr>
                        <td align="center" valign="top" colspan="12" width="100.0%" class="column" style="text-align: left; font-family: {{ primary_font }}; font-size: {{ primary_font_size }}; line-height: 1.5em; color: {{ primary_font_color }}; ">
<!--end widget-span -->
                        </td>
                    </tr>
                </table>
            </td>
        </tr>
        <tr>
            <td align="center" valign="top" class="bodyContent" width="100%" colspan="12">
                <table cellpadding="0" cellspacing="0" border="0" width="100%" class="templateColumnWrapper">
                    <tr>
                        <td align="center" valign="top" colspan="12" width="100.0%" class="column" style="text-align: left; font-family: {{ primary_font }}; font-size: {{ primary_font_size }}; line-height: 1.5em; color: {{ primary_font_color }}; ">
<!--end layout-widget-wrapper -->
                            </div><!--end widget-span -->
                        </td>
                    </tr>
                </table>
            </td>
        </tr>
        <tr>
            <td align="center" valign="top" class="bodyContent" width="100%" colspan="12">
                <table cellpadding="0" cellspacing="0" border="0" width="100%" class="templateColumnWrapper">
                    <tr>
                        <td align="center" valign="top" colspan="12" width="100.0%" class="column" style="text-align: left; font-family: {{ primary_font }}; font-size: {{ primary_font_size }}; line-height: 1.5em; color: {{ primary_font_color }}; ">
<!--end layout-widget-wrapper -->
                            </div><!--end widget-span -->
                        </td>
                    </tr>
                </table>
            </td>
        </tr>
</div><!--end body wrapper -->
                                            </table>
                                        </td>
                                    </tr>
                                </table>
                                <!-- End Template Wrapper -->
                            </td>
                        </tr>
                        <tr>
                            <td align="center" valign="top">
                                <!-- Begin Template Footer -->
                                <table border="0" cellpadding="0" cellspacing="0" width="100%" id="footerTable">
<!--end widget-span -->
                        </td>
                    </tr>
                </table>
            </td>
        </tr>
</div><!--end footer wrapper -->
                                    <tr>
                                        {% hubspot_footer %}
                                    </tr>
                                </table>
                                <!-- End Template Footer -->
                            </td>
                        </tr>
                    </table>
                    <!-- End Template Container -->
                </td>
            </tr>
        </table>
    </body>
</html>
```

## Basic email template

Basic HubSpot template layouts use different markup and do not include the media queries that make the email responsive. The basic layouts also use [color and font variables](/reference/cms/hubl/variables#color-and-font-settings) that connect to **Settings > Marketing > Email**.

```hubl
<!DOCTYPE html>
<html bgcolor="{{ background_color }}" style="margin: 0; padding: 0; background-color: {{ background_color }}">
    <head>
        <title>{% if content.html_title and content.html_title != "" %}{{ content.html_title }}{% else %}{{ content.body.subject }}{% endif %}</title>
        <meta property="og:title" content="{% if content.html_title and content.html_title != "" %}{{ content.html_title }}{% else %}{{ content.body.subject }}{% endif %}">
        <meta name="twitter:title" content="{% if content.html_title and content.html_title != "" %}{{ content.html_title }}{% else %}{{ content.body.subject }}{% endif %}">
        {% if content.meta_description %}<meta name="description" content="{{ content.meta_description }}" />{% endif %}
        <style type="text/css">
        /*<![CDATA[*/
            /* iOS automatically adds a link to addresses */
            /* Style the footer with the same color as the footer text */
            #footer a {
                color: {{ secondary_font_color }};
                -webkit-text-size-adjust: none;
                text-decoration: underline;
                font-weight: normal
            }

            {% if email_main_body_box_shadow_css and email_main_body_box_shadow_css != '' %}
            #main_body {
                box-shadow: {{ email_main_body_box_shadow_css }};
            }
            {% endif %}

            /* Hide preview text on rendering */
            #preview_text {
                display: none;
            }

        /*]]>*/
        </style>
        <!-- http://www.emailon@cid.com/blog/details/C13/ensure_that_your_entire_email_is_rendered_by_default_in_the_iphone_ipad -->
        <!--                                                                                                                     -->
        <!--                                                                                                                     -->
        <!--                            _/    _/            _/          _/_/_/                        _/                         -->
        <!--                           _/    _/  _/    _/  _/_/_/    _/        _/_/_/      _/_/    _/_/_/_/                      -->
        <!--                          _/_/_/_/  _/    _/  _/    _/    _/_/    _/    _/  _/    _/    _/                           -->
        <!--                         _/    _/  _/    _/  _/    _/        _/  _/    _/  _/    _/    _/                            -->
        <!--                        _/    _/    _/_/_/  _/_/_/    _/_/_/    _/_/_/      _/_/        _/_/                         -->
        <!--                                                               _/                                                    -->
        <!--                                                              _/                                                     -->
        <!--                                                                                                                     -->
        <!--                                                 Extra White Space!                                                  -->
        <!--                                                                                                                     -->
        <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
    </head>
    <body bgcolor="{{ background_color }}" style="margin: 0; padding: 0;  background-color: {{ background_color }}" marginheight="0" marginwidth="0" topmargin="0">

        <!-- Preview text (text which appears right after subject) -->
        <!-- start container -->
        <table bgcolor="{{ background_color }}" style="background-color: {{ background_color }}; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none" cellpadding="0" cellspacing="0" border="0" width="100%">
             <tr>
                <td bgcolor="{{ background_color }}" style="background-color: {{ background_color }};">

{% endif %}

                </div><!--end widget-span -->
            </td>
        </tr>
    </table>
</div><!--end header wrapper -->
                                    <!-- end header section -->
                                </td>
                            </tr>
                        </table>
                    </div>
                </td>
            </tr>
            <tr>
                <td bgcolor="{{ background_color }}" style="padding: 10px 20px; background-color: {{ background_color }}">
<!--end layout-widget-wrapper -->
                </div><!--end widget-span -->
            </td>
        </tr>
        <tr>
            <td valign="top" colspan=12 width="100.0%" class="" style="width:100.0%; text-align: left; padding: 0; font-family: {{ primary_font }}; font-size: {{ primary_font_size }}; line-height: 1.5em; color: {{ primary_font_color }}; ">
<!--end widget-span -->
            </td>
        </tr>
        <tr>
            <td valign="top" colspan=12 width="100.0%" class="" style="width:100.0%; text-align: left; padding: 0; font-family: {{ primary_font }}; font-size: {{ primary_font_size }}; line-height: 1.5em; color: {{ primary_font_color }}; ">
<!--end layout-widget-wrapper -->
                </div><!--end widget-span -->
            </td>
        </tr>
        <tr>
            <td valign="top" colspan=12 width="100.0%" class="" style="width:100.0%; text-align: left; padding: 0; font-family: {{ primary_font }}; font-size: {{ primary_font_size }}; line-height: 1.5em; color: {{ primary_font_color }}; ">
<!--end layout-widget-wrapper -->
                </div><!--end widget-span -->
            </td>
        </tr>
    </table>
</div><!--end body wrapper -->
                                                                        <!-- end body section -->
                                                                    </div>
                                                                </td>
                                                            </tr>
                                                        </table>
                                                    </div>
                                                </td>
                                            </tr>
                                            <!-- end content -->
                                        </table>
                                    </div>
                                </td>
                            </tr>
                        </table>
                    </div>
                </td>
            </tr>
            <tr>
                <td bgcolor="{{ background_color }}" style="background-color: {{ background_color }}; padding: 13px {{ email_body_padding }}px">
<!--end widget-span -->
            </td>
        </tr>
    </table>
</div><!--end footer wrapper -->
                                    <!-- end footer section -->
                                </td>
                            </tr>
                            <tr>
                                {% hubspot_footer %}
                            </tr>
                        </table>
                    </div>
                </td>
            </tr>
        </table>
        <!-- end container -->
    </body>
</html>
```

## Starting from scratch

When you start from scratch by creating a .html email in Design Manager, HubSpot will automatically generate the markup below.

```hubl
<!doctype html>
<html>
    <head>
        <title>{% if content.html_title and content.html_title != '' %}{{ content.html_title }}{% else %}{{ content.body.subject }}{% endif %}</title>
        {% if content.meta_description %}<meta name="description" content="{{ content.meta_description }}"/>{% endif %}
        <style type="text/css" id="hs-inline-css">
            /*<![CDATA[*/
            /* everything in this style tag will be inlined onto matching elements */
            .sample-rule {
            }
            /*]]>*/
        </style>
    </head>
    <body>
        <!-- Preview text (text which appears right after subject in certain email clients) -->

        <!-- View as webpage link -->
        {% if content.create_page %}

        {% endif %}
        <!-- Insert body here -->
        <!-- Office location information and unsubscribe links -->
        <p id="footer">
            {{ 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/>
            You received this email because you are subscribed to {{ subscription_name }} from {{ site_settings.company_name }}.
            <br/>
            Update your <a class="hubspot-mergetag" data-unsubscribe="true" href="{{ unsubscribe_link }}">email preferences</a> to choose the types of emails you receive.
            <br/>
            <a class="hubspot-mergetag" data-unsubscribe="true" href="{{ unsubscribe_link_all }}">Unsubscribe from all future emails</a>
        </p>
    </body>
</html>
```


# HTML + HubL templates
HTML + HubL templates can be used for [every type of template](/guides/cms/content/templates/overview#template-types) on the HubSpot CMS. These templates are .html files that support the [HubL templating language](/reference/cms/hubl/overview). Because these coded templates support HubL the best previewing experience is using the [template preview in the Design Manager](https://knowledge.hubspot.com/cos-general/review-your-hubspot-template-setup) or viewing pages on a sandbox account. HTML + HubL templates can contain [partials](#partials), which can be used to separate commonly used chunks of code, such as a header or footer.
HTML + HubL templates give greater control to developers than [visual design manager drag and drop templates](/guides/cms/content/templates/types/drag-and-drop-templates). Developers in-turn can provide better experiences for content creators through [drag and drop functionality](/reference/cms/hubl/tags/dnd-areas), which is only possible with HTML + HubL templates.
The above template is the [base.html template](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/layouts/base.html) included in the [HubSpot CMS boilerplate](/guides/cms/content/themes/hubspot-cms-boilerplate), which is a great way to get started developing with HubSpot.
## Familiarity and tooling

Since HTML + HubL templates are coded files, you can use your preferred tools to edit them locally. It's recommended to use HubSpot's own [local development tools](/guides/cms/tools/local-development-cli) so that you can [upload](/guides/cms/tools/local-development-cli#upload), [fetch](/guides/cms/tools/local-development-cli#fetch), [watch](/guides/cms/tools/local-development-cli#watch), [create](/guides/cms/tools/local-development-cli#create) and otherwise securely manage files in the developer file system as well as the file manager.

Building HTML + HubL templates with HubSpot is similar to using other templating language you may have used before. The core difference is that HubSpot takes an opinionated stance on the best ways to do some things to offer the best experience for content creators, and also takes much of the maintenance and performance optimization work off of the developer.

For example, if you want to load CSS file on a page for certain modules, instead of using `<link rel="stylesheet" type="text/css" href="theme.css">`, you should include the stylesheet through `css_assets` in the [module's meta.json file](/reference/cms/modules/configuration#adding-css-and-javascript-dependencies). This enables HubSpot to conditionally load the CSS only when the module is present on a page, minimizing the amount of unnecessary CSS loaded.

Learn more about [optimizing your HubSpot development workflow](/guides/cms/setup/creating-an-efficient-development-workflow).

## Template annotations

Template annotations, included at the top of a template, configure important template settings, such as the template type and whether it can be used to create new content. Template annotations can be changed at any time during the development process. Below, learn more about available template annotations.

```html
<!--
  templateType: page
  isAvailableForNewContent: false
  enableDomainStylesheets: false
  label: Homepage
  screenshotPath: ../images/template-previews/home.png
-->

<!doctype html>

<html>
  ...
</html>
```

| Annotation | Type | Description |
| --- | --- | --- |
| `templateType` | String | Specifies which template type a file is. Values include:<br /><ul><li>Standard templates:<ul><li>[page](/guides/cms/content/templates/overview#page)</li><li>[email](/guides/cms/content/templates/overview#email)</li><li>[blog_listing](/guides/cms/content/templates/overview#blog-listing)</li><li>[blog_post](/guides/cms/content/templates/overview#blog-post)</li><li>[blog](/guides/cms/content/templates/overview#blog)</li></ul></li><li>Partials: [global_partial](/guides/cms/content/templates/types/html-hubl-templates#global-partials)</li><li>System templates:<ul><li>[error_page](/guides/cms/content/templates/overview#error-pages)</li><li>[password_prompt_page](/guides/cms/content/templates/overview#password-prompt)</li><li>[membership_login_page](/guides/cms/content/templates/overview#membership-login)</li><li>[membership_register_page](/guides/cms/content/templates/overview#membership-register)</li><li>[membership_reset_page](/guides/cms/content/templates/overview#membership-password-reset)</li><li>[membership_reset_request_page](/guides/cms/content/templates/overview#membership-reset-request)</li><li>[email_subscription_preferences_page](/guides/cms/content/templates/overview#email-subscription-preferences)</li><li>[email_backup_unsubscribe_page](/guides/cms/content/templates/overview#email-backup-unsubscribe)</li><li>[email_subscriptions_confirmation_page](/guides/cms/content/templates/overview#email-subscription-unsubscribe-confirmation)</li><li>[search_results_page](/guides/cms/content/templates/overview#search-results-page)</li></ul></li></ul> |
| `isAvailableForNewContent` | String | Specifies if a template is available for selection in the content creation process. Values include: `true`, `false`.Templates set to `false` do not need to include the [required variables](/reference/cms/hubl/variables#required-page-template-variables). Templates of the `page` type that are set to false can also be used as [standard partials](#partials). |
| `enableDomainStylesheets` | String | Specifies if the template should load [domain stylesheets](https://knowledge.hubspot.com/design-manager/create-edit-and-attach-css-files-to-style-your-site#attach-or-remove-stylesheets-on-a-domain-level). Values include: `true`, `false`. |
| `Label` | String | User-friendly description of the template, displayed in the template selection screen. For example, `About Page`, `Homepage`, `Pricing`. |
| `screenshotPath` | String | The screenshot to display when a content creator is selecting a template. This screenshot should make it easy to differentiate between your templates. |

## Header and footer includes

HubSpot's templates require two tags to be present unless the file is a template partial. The two tags are:

- `{{ standard_header_includes }}` - Used to intelligently add combined and minified required CSS.
- `{{ standard_footer_includes }}` - Used to intelligently add javascript to the bottom of a page dynamically, for things like the HubSpot tracking script, and modules.

These tags must be present in a template or it's [partial](#partials) children to be published and used.

## Partials

Template partials are HTML + HubL files that can be included in other coded files. Partials enable you to take a more modular approach by sharing markup between multiple templates. For example, a header can be made into a partial so that you can easily include it as a component without needed to code it again.

### Standard partials

A standard partial is a reusable template or component containing content that can be edited on individual pages. This enables content creators to change the content as needed, as opposed to global partials which always share content. For example, the following code would pull the sidebar module into another template file.

Standard partials must include the following [annotations](/guides/cms/content/templates/types/html-hubl-templates#template-annotations) at the top of the template file:

- `templateType: page`
- `isAvailableForNewContent: false`

```hubl
{% include "../partial/sidebar.html" %}
```

### Global partials

A global partial is a type of [global content](/guides/cms/content/global-content#global-partials) that can be used across multiple templates with content that is shared across all instances of the partial. Global partials are often used for headers and footers, which you can see an example of in the HubSpot CMS Boilerplate [header](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/partials/header.html) and [footer](https://github.com/HubSpot/cms-theme-boilerplate/blob/master/src/templates/partials/footer.html). These partials are then called in [base.html](https://github.com/HubSpot/cms-theme-boilerplate/blob/master/src/templates/layouts/base.html) using the `global_partial` tag.

Global partials must include the [annotation](/guides/cms/content/templates/types/html-hubl-templates#template-annotations) `templateType: global_partial` at the top of the file.

```hubl
{% global_partial path="../partials/header.html" %}
```

### Blocks and extends

When creating complex templates, you can create compartmentalized blocks that extend a parent template.

For example, you can create a parent 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 define a unique block using the following syntax where `body` is a unique name:

```hubl
{% block body %}
<!-- Content to display -->
{% endblock body %}
```

Then, in the child template, you can extend the parent template, then insert more content into the `body` block.

```hubl
{% extends "./layouts/base.html" %}
{% block body %}
<h3>Page Content</h3>
<ul>
  <li>Bullet 1<li>
  <li>Bullet 2<li>
  <li>Bullet 3<li>
</ul>
{% endblock %}
```

This method is used in the [base.html](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/layouts/base.html) template of the HubSpot CMS boilerplate, which then is extended by the other templates in the [templates folder](https://github.com/HubSpot/cms-theme-boilerplate/tree/main/src/templates).

### Global groups

[Global groups](https://knowledge.hubspot.com/cms-general/use-global-content-across-multiple-templates) created using the drag and drop template builder in the Design Manager, can also be included. The syntax is displayed below:

```hubl
{% include "/path/to/global_header.template.json" %}
```


# Custom quote templates
For businesses that use quotes during the sales process, a sales rep can [create a deal](https://knowledge.hubspot.com/records/create-deals), then [create a quote associated with that deal](https://knowledge.hubspot.com/quotes/use-quotes) all within HubSpot. The sales rep would then send the quote to a customer using the generated URL or PDF. The prospect then accepts or declines the quote. In some cases payment is exchanged right away, in some cases an e-signature is used.

Similar to the themes and templates you can create for the CMS as a developer, you can create quote themes and templates to enable sales teams to send quotes that are tailored to their business needs.
You can create custom quote themes and templates with any HubSpot subscription, including _**CMS Free**_, but an account will need _**Sales Hub**_ _Professional_ or _Enterprise_ to use those templates for their quotes.
## Overview

Custom quote templates are built using the same underlying systems that other types of templates use. For example:

- Domain-level settings apply to quotes, including head and footer HTML and domain stylesheets. You can disable domain stylesheets using [template annotations.](/guides/cms/content/templates/types/html-hubl-templates#template-annotations)
- Most of HubL's functionality works on quote templates, including functions, filters, if conditions, imports, and includes.
- When using [personalization tokens](https://knowledge.hubspot.com/website-pages/personalize-your-content) in a quote, HubSpot will not render them dynamically. Instead, the token is rendered at the time of publishing the quote, and will not update upon signing. For this reason, you should not use personalization tokens for properties that are updated after a quote is published, including:
  - Payment status
  - Payment date
  - Esign date
  - Esign completed signatures

Due to the specific use-case of quotes, there are some key differences between how quotes work from the way page and email templates work:

- More data is available to the quote template that is restricted for other template types. For example, quote and deal-related data is available to a quote template. You can also include contact data for quote recipients in a quote template.
- There is not currently a [drag and drop](/guides/cms/content/templates/drag-and-drop/overview) editor for building quote templates. Instead, there is a module-based editor for customizing or hiding the modules that are already in a template.
Because more data is available to quote templates without requiring password protection. You should take care to only expose necessary information.
## Creating quote templates

Quote templates are contained within quote themes, similar to the relationship between CMS templates and themes. HubSpot provides a set of [default quote modules](/reference/cms/modules/default-modules#quote-download) that you can use to enable downloading to PDF form, facilitating payment, and collecting signatures. In addition, you'll use [quote template variables](/reference/cms/hubl/quote-variables) to access quote data and other CRM data directly from the quote template.

To get started, you can create a custom quotes theme, along with templates, modules, and more, using [HubSpot's boilerplate CMS quotes theme](/guides/cms/content/themes/quotes-themes).

## Custom enumeration properties

Enumeration properties such as dropdown menus, or multiple checkboxes, can be incorporated into custom quote templates. Learn more about HubSpot's [property types](https://knowledge.hubspot.com/properties/property-field-types-in-hubspot).

The JSON returned for custom enumeration properties includes both the internal value and the external label.
By default, quotes created after September 13th, 2024 will display the property label rather than the internal value.
```json
custom_properties: {
  my_custom_property: {
    label:"Label1",
    internal:"value1"
  },
  another_custom_property: {
    label:"Label99",
    internal:"value99"
  }
}
```

For CMS Developers using custom quotes templates, the [`crm_property_definition`](/reference/cms/hubl/functions#crm_property_definition) function can be used to retrieve quote property data, and will allow you to replace the label with the value where necessary.

```hubl
{% set dealEnum = template_data.quote.associated_objects.deal.deal_enum %}
{% set dealEnumProp = crm_property_definition("DEAL", "deal_enum").options|selectattr('label', "equalto", dealEnum)|first %}
{{ dealEnumProp.value }}
```

## Enable e-signatures

In _**Sales Hub**_ _Starter_, _Professional_, and _Enterprise_ accounts, quotes can be configured to include [e-signature functionality](https://knowledge.hubspot.com/quotes/use-e-signatures-with-quotes). To enable this for custom quote templates, add the [quote_signature module](/reference/cms/modules/default-modules#quote-signature) to the quote template.

```hubl
<section class="signature">
  {% module "signature" path="@hubspot/quote_signature" %}
</section>
```

Because Dropbox Sign renders the print version of a quote for signing, ensure that the signature field is displayed in the [print version](#print-and-pdf-versions-of-custom-quotes) of your quote. Otherwise, Dropbox Sign will display an error when the user goes to verify their signature.
## Print and PDF versions of custom quotes

If you'd like to enable users to print or download a quote, it's recommended to include the [download module](/reference/cms/modules/default-modules#quote-download). Alternatively, since a quote is a web page you can use JavaScript and a button element to provide an easy way to print the quote.

To optimize and style the print and PDF version of a quote template, you can use the [`@media print` media query](https://developer.mozilla.org/en-US/docs/Web/CSS/@media#print) in the template's stylesheet. For example, HubSpot's default _Basic_ quote theme includes the following print styling in the `basic.css` stylesheet:

```css
@media print {
  .hs-quotes--basic {
    max-width: unset;
  }

  .hs-quotes--basic .line-items__total-name {
    float: left;
  }

  .hs-quotes--basic .comments,
  .hs-quotes--basic .terms {
    break-inside: avoid;
  }
}
```

To preview the print version in Chrome:

- Open the web version of a quote.
- Right-click the page, then select **Inspect**.
- In the top right of the DevTools panel, click the **three vertical dots** **⋮**, then select **More tools**, then select **Rendering**.

  

- In the _Rendering_ panel, scroll to the _Emulate CSS media type_ section. Then, click the **dropdown menu** and select **print**.

  

You can now continue to testing out styling in Chrome. When you're ready to apply the styling to the template, copy the styles into your `@media print` media query, then upload the template to HubSpot.

Updated styling will only apply to quotes created after updating the template. Existing quotes using the template will not be updated.

## Related Resources

- [Getting started with the CMS quotes theme](/guides/cms/content/themes/quotes-themes)
- [Custom quote variable reference](/reference/cms/hubl/quote-variables)
- [Create and use custom quote templates (from the sales, sales ops/manager perspective)](https://knowledge.hubspot.com/quotes/create-custom-quote-templates)


# Build and deploy themes using projects

Using HubSpot's developer projects framework, you can build a CMS theme locally, then deploy it to your HubSpot account. This method of theme development has a few advantages over the classic method of developing a HubSpot theme:

- Themes built using this method can include React-based modules.
- By developing with a project, you'll be able to use HubSpot's build and deploy pipeline for better version control.

By the end of this tutorial, you will have:

- Used the HubSpot CLI to set up your local environment for development.
- Uploaded a boilerplate theme and project to your account.
- Viewed the project and theme in HubSpot.

## Prerequisites

Before getting started, you'll need to create a HubSpot account (any subscription, including a free account). It's recommended to [sign up for a CMS Sandbox account](https://offers.hubspot.com/free-cms-developer-sandbox), where you can create test accounts for safer iteration.

It’s also recommended to have a basic familiarity with the following:

- HTML, HubL, and React. Some knowledge of TypeScript may also be helpful.
- Using the command line to run commands locally. When developing locally with HubSpot, you'll use the HubSpot CLI, which this guide will walk through installing.

## 1. Run the setup script

Because you'll be developing the theme locally then uploading it to your account, you'll first need to set up your local development environment. To speed up this process, HubSpot provides a script that you can run from the command line to handle basic setup tasks, including:

- Generating a JavaScript-based HubSpot theme
- Installing the latest version of the HubSpot CLI if not already installed
- Installing all necessary dependencies
- Authenticating with your HubSpot account to connect it to the HubSpot CLI

To get started:

- Create a local directory where you'll be storing your project, or decide on an existing directory to use (e.g., `Dev/Themes/newProject`). Then, using the command line, navigate into the directory using the `cd` command.

```shell
cd ~/path/of/working-directory
```

- Run the setup script.

```shell
npx @hubspot/create-cms-theme@latest
```

- Follow the prompts in your terminal to proceed through the setup.

During the authentication step, the terminal will prompt you to open the personal access key of your HubSpot account.

- If you've already connected your HubSpot account using `hs init`, this step will be skipped. If you want to connect additional accounts to the HubSpot CLI, you can run `hs auth` after the setup script is complete.
- To create a new personal access key:

  - In the browser, ensure all permissions checkboxes are selected.

  

  - Click **Generate personal access key**.
  - In the _Personal Access Key_ section, click **Show**, then **Copy**, then paste your key into the terminal.
  - Continue following the prompts to finish authenticating.

After authentication is completed, a `hubspot.config.yml` file will be created in your working directory, and will look similar to the example below:

```json
defaultPortal: account-name
portals:
  - name: account-name
    portalId: 1234567
    env: prod
    authType: personalaccesskey
    auth:
      tokenInfo:
        accessToken: >-
          <accessTokenValue>
        expiresAt: '2025-02-13T15:44:59.399Z'
    accountType: STANDARD
    personalAccessKey: >-
      <personalAccessKeyValue>

```

After authenticating, you should see a message in your terminal confirming that your starter theme was created successfully. Your working directory should now also contain a theme directory with the following structure:

```shell
my-cms-theme/
│── node_modules/
├── src/theme/
│   ├── assets/
│   ├── components/
│   │   └── modules/
│   ├── node_modules/
│   ├── styles/
│   ├── templates/
│   ├── fields.json
│   ├── Globals.d.ts
│   ├── package.json
│   ├── theme.json
│   └── tsconfig.json
│── .prettierrc
│── hsproject.json
└── package.json
```

## 2. Preview theme assets locally

Start a local development server to view the theme locally before uploading to HubSpot.

- In the terminal, navigate into the theme directory that was created by the setup script.

```shell
cd my-cms-theme
```

- Start the local development server with the `npm run start` command.

```shell
npm run start
```

After the local development server starts, the terminal will provide a URL to open in your browser ([http://hslocal.net:3000](http://hslocal.net:3000)).

```shell
HubSpot CMS dev server hosting: your_directory/theme (at http://hslocal.net:3000, hubid=<yourHubId>)
```

- Open the URL in the browser to view the local development server page.
At the top of the page, you can view a summary of the assets that are available to preview in your project. For this example, you'll see one module listed.

- To quickly view the details of a module, including its local path and `fields.json` code, click the **name** of the module, then review its details in the right sidebar.
- To preview the module, click **View local version**. A new browser tab will open containing a preview of the module.
With the local development server running, the local version preview will automatically update when you save code changes. As an example, make a change to the module's styling and view how it updates the module preview:

- In your local editor, open the `getting-started.module.css` file in `/src/theme/styles`.
- Note that the `.wrapper` declaration block contains a `background-color` rule with a value of `var(--accent-color)`. This is the rule you'll need to update in order to change the module's background color.
- In the `:root` declaration block at the top of the file, add a new custom color variable of `--custom-color:#5e6ab8`.

```css
:root {
  --primary-color: #ff7a59;
  --accent-color: #2d3e50;
  --custom-color: #5e6ab8;
}
```

- In the `.wrapper` declaration block, update the `background-color` rule to use the new color variable.

```css
background-color: var(--custom-color);
```

- With the module preview open, save your changes. On save, the browser will automatically refresh and display your updated styling.
## 3. Upload to HubSpot

Upload your local theme to HubSpot to make it available for content creators. After uploading, you'll also be able to access all of the project's assets in a centralized projects UI in HubSpot, where you can view build and deploy history, review logs, and more.

To upload your theme, run [`hs project upload`](/guides/crm/developer-projects/project-cli-commands#upload-to-hubspot).

```shell
hs project upload
```

After the project builds and deploys to your account, the terminal will confirm that the project has been successfully uploaded. Note that, by default, HubSpot auto-deploys projects after a successful build. You can [turn this off in the project settings](#4.-view-the-project-in-hubspot) in HubSpot if you'd prefer to manually deploy with the [`hs project deploy`](/guides/crm/developer-projects/project-cli-commands#deploy-to-hubspot) command.

## 4. View the project in HubSpot

With your project and theme uploaded to the account, open the project in HubSpot to view its initial build and deploy history and familiarize yourself with the projects UI.

- To navigate to your project in HubSpot using the command line, run the `hs project open` command in your terminal.

```shell
hs project open
```

The project settings page will then open in your browser.
You can also get to this page in your account by navigating to **CRM Development** in the main navigation bar, then selecting **Projects** in the left sidebar. Then, click the **name** of the project.
- On the _Details_ tab, view an overview of the current state of the project as well as most recent activity.
- On the _Activity_ tab, view a full history of builds and deploys. You can click **View details** to see more information about each activity.
- On the _Settings_ tab, you can manage project settings such as auto-deploy and GitHub repository linking, as well as delete the project.
## 5. Create a page

To create a page using your newly uploaded custom theme:

- In your HubSpot account, navigate to **Content** > **Website Pages**.
- If you're working in a new account:

  - Click **Start from scratch**, then locate the **theme** that you just uploaded and click **Set as active theme**.
  - Next to the _Getting started with CMS React_ template, click **Select template**.
- If you're working in an existing account with a theme already selected:
  - In the upper right, click **Create**.
  - In the dialog box, enter a **name** for the page, then click **Create page**.
  - On the _Choose a template_ page, click the **Theme** dropdown menu, then select **Change theme**.
- Locate the **theme** you just uploaded, then click **Set as active theme**.
- Next to the _Getting started with CMS React_ template, click **Select template**.
You'll then be brought to the page editor where you can continue [editing the page content](https://knowledge.hubspot.com/website-pages/edit-page-content-in-a-drag-and-drop-area) before [publishing the page](https://knowledge.hubspot.com/website-and-landing-pages/create-and-customize-pages#publish-pages).

## Next steps

Having successfully uploaded the starter theme to your account, you can continue to make changes locally, then build and deploy using the `hs project upload` command. To learn more about developing on HubSpot's CMS, you may want to check out some of the following resources:

- [Managing theme settings in HubSpot](https://knowledge.hubspot.com/website-pages/edit-your-global-theme-settings)
- [Understand the various building blocks of HubSpot's CMS](/guides/cms/content/overview)
- [Install and use the HubSpot VS Code extension](/guides/cms/setup/install-and-use-hubspot-code-extension)
- [CLI commands for projects](/guides/crm/developer-projects/project-cli-commands)
- [Get started with CMS React modules](/guides/cms/content/modules/build-modules-and-partials-with-react)


# Child Themes
A **child theme** is a copy of an original **parent theme**. You can edit the child theme without altering the parent theme. You can create child themes from default HubSpot themes, Asset Marketplace themes, and custom themes.
Creating a child theme lets you customize its theme settings without impacting the parent theme settings. However, creating a child theme will not create clones of each of the theme's templates, modules, or files such as stylesheets and JavaScript. You'll need to manually clone these assets to create copies for the child theme.
## Create a child theme in the Design Manager

You can create a child theme in the Design Manager. You will be able to edit the child theme even if the original theme is not editable. The following files will be added to your child theme:

- `theme.json` - this will include an extends statement for linking to the parent theme.
- `child.css` and `child.js` - these are empty CSS and JS files. Code added to these files will only affect the child theme.
- Any files that contain the [`standard_header_includes` HubL variable](/reference/cms/hubl/variables#required-page-template-variables). This usually includes a "base" or "main" template file. You can [view an example of this on our boilerplate](https://github.com/HubSpot/cms-theme-boilerplate/tree/main/src/templates/layouts).

Learn how to create a child theme from an Asset Marketplace or default HubSpot theme in the [Knowledge Base](https://knowledge.hubspot.com/website-pages/use-themes#create-a-child-theme).

To create a child theme from a custom theme:

- In your HubSpot account, navigate to **Marketing > Files and Templates > Design Tools.**
- In the finder, click the **File** dropdown menu and select **New theme.**
- Click the **Theme Starting Point** dropdown menu and select **Blank Theme**.
- Enter a **name** in the _Theme Label_ field.
- To change where the child theme will be saved, click **Change** in the _File location_ section. Click a **folder**, then click **Select**.
- In the _theme.json_ file, enter a **comma** at the end of line 5, then add a new line below line 5.
- On line 6, add an `extends` statement, using a value of the path of the parent theme.

```json
"extends": "path/to/theme"
```

- By default, the child theme will inherit **fields.json** from the parent theme, so you do not need to create a separate **fields.json** for the child theme if you don't plan on updating it.

## Create a child theme with the CLI

You can create a child theme with the CLI. You must complete [the setup process for the CLI](/guides/cms/setup/getting-started-with-local-development) before creating a child theme. Your child theme will inherit the templates and files from the parent theme. You will be able to edit the new theme within the CLI and in the Design Manager.

- Create a new directory for your theme.

```shell
mkdir cms-project
```

- In the new directory, create a copy of the **theme.json** file of the parent theme.

```shell
cd cms-project
cp ~/src/cms-sprout-theme/src/theme.json
```

- In the copied _theme.json_ file, add a comma to the end of line 5 and a new line below line 5 (or below the final line of the `responsive_breakpoints`, if they are present).
- Add an `extends` statement to line 6 (or 14, or the appropriate line for your theme), using a value of the path of the parent theme.

```json
"extends": "@hubspot/barricade"
```

- By default, the child theme will inherit **fields.json** from the parent theme. Consequently, you don't need to create a separate **fields.json** for the child theme if you don't plan on updating it.
- Upload your new theme and files to your HubSpot account.

```shell
hs upload cms-project cms-project
```

## Limitations

The total number of child themes you can have is based on your subscription:

- _**Marketing Hub** Professional_ or _**CMS Hub** Professional_: five child themes
- _**Marketing Hub** Enterprise_ or _**CMS Hub** Enterprise:_ 10 child themes
- HubSpot's free tools and _**CMS Hub**_ _Starter_: one child theme

## FAQs

1.  **Can I copy a child theme to another HubSpot account if the parent theme is from the Asset Marketplace?** Yes, as long as the “extends” path is the same in the other HubSpot account.
2.  **What assets are inherited from the parent theme?** All files are inherited from the parent theme unless they are overwritten in the child theme.
3.  **How can I override a particular asset from the parent theme?** A file in the same relative path of a child theme will overwrite the equivalent file from the parent theme. So, for instance, to overwrite `@marketplace/parent/theme/templates/about.html` you can create `/child/theme/templates/about.html` and make your edits to the new file. The new file will take effect instead of the inherited file. This applies to your **fields.json** file as well as other files in the theme.
4.  **How can I create new pages using the child theme?** When you create a new page, your child theme will appear as an option under **Your Themes** on the theme selection screen. Learn more about creating pages using themes in the [Knowledge Base.](https://knowledge.hubspot.com/website-pages/use-themes)
5.  **How can I edit an existing page to use a child theme instead of the parent theme?** You can replace your page template with the equivalent template from the new theme. For instance, [replace the template](https://knowledge.hubspot.com/design-manager/swap-and-edit-your-blog-email-or-page-template#use-a-different-template) `landing-page.html` (in the parent theme) with the template `landing-page.html` (in the new theme).
6.  **How can I edit a template’s label on the page creation screen?** You can change the label of your template by editing the HTML file. The label is located in a comment at the top of your theme file.
7.  **Can I create a child theme from a child theme?** Currently you cannot create a child theme from a child theme.


# Default themes

HubSpot provides a set of default themes that content creators can use to build website pages without needing to modify the base theme. These themes are best suited for content creators who have less access to developer resources.

If you want to develop your own theme, it's recommended to start with the [HubSpot CMS Boilerplate](/guides/cms/content/themes/hubspot-cms-boilerplate). However, you can download and modify default themes using the [HubSpot CLI](/guides/cms/setup/getting-started-with-local-development).
The Elevate theme is currently only available for use in HubSpot. To modify the theme, you can [edit its theme settings](https://knowledge.hubspot.com/website-pages/edit-your-global-theme-settings).
With the exception of Elevate, you can access HubSpot’s default themes in the `@hubspot` folder within the design manager. This folder is read-only and the contents can only be modified by HubSpot. If you'd like to modify a theme, you'll need to use the HubSpot CLI to [fetch](/guides/cms/tools/hubspot-cli/cli-v7#fetch-files) the theme you want, then [upload](/guides/cms/tools/hubspot-cli/cli-v7#upload-files) the theme with your changes to a different folder than `@hubspot`.

## Elevate

[View a live demo](https://design-assets.hubspot.com/elevate/home)

Available in all accounts, Elevate is HubSpot's latest default theme, offering a refreshed set of modules, sections, and templates. Modules included in this theme are React-based, which improves site performance and extensibility. The theme's settings are also equipped with presets that streamline customization within the guardrails of your brand and style.

The Elevate theme includes:

- 23 templates
- 17 modules
- 22 sections
## Growth

[View a live demo](https://design-assets.hubspot.com/growth/home)

A theme designed for businesses and agencies.

The Growth theme includes:

- 18 modules
- 12 sections
- 16 templates
To download this theme using the HubSpot CLI, run the following command:

```shell
hs fetch @hubspot/growth <destination-folder>
```

## Themes in maintenance mode

The following themes have been moved to maintenance mode. They will no longer be updated with new features and will only receive updates as they relate to security, accessibility, and bug fixes.

### Barricade

[View a live demo](https://design-assets.hubspot.com/barricade/home)

A theme geared towards construction businesses.

The Barricade theme includes:

- 10 modules
- 19 templates
To download this theme using the HubSpot CLI, run the following command:

```shell
hs fetch @hubspot/barricade <destination-folder>
```

### Education

[View a live demo](https://design-assets.hubspot.com/education/home)

A theme designed for educational institutions, such as schools and colleges.

The Education theme includes:

- 13 modules
- 18 templates
To download this theme using the HubSpot CLI, run the following command:

```shell
hs fetch @hubspot/education <destination-folder>
```

### Martech

[View a live demo](https://design-assets.hubspot.com/martech/home)

A theme geared towards marketing technology businesses.

The Martech theme includes:

- 9 modules
- 19 templates
To download this theme using the HubSpot CLI, run the following command:

```shell
hs fetch @hubspot/martech <destination-folder>
```

### Rally

[View a live demo](https://design-assets.hubspot.com/rally/home)

A theme designed for businesses and agencies.

The Rally theme includes:

- 14 modules
- 19 templates
To download this theme using the HubSpot CLI, run the following command:

```shell
hs fetch @hubspot/cms-rally <destination-folder>
```

### Session

[View a live demo](https://design-assets.hubspot.com/session/home)

A theme designed for businesses.

The Session theme includes:

- 9 modules
- 18 templates
To download this theme using the HubSpot CLI, run the following command:

```shell
hs fetch @hubspot/session <destination-folder>
```

### Sprout

[View a live demo](https://design-assets.hubspot.com/sprout/home)

A theme designed for businesses and agencies.

The Sprout theme includes:

- 15 modules
- 21 templates
To download this theme using the HubSpot CLI, run the following command:

```shell
hs fetch @hubspot/sprout <destination-folder>
```

### Vitality

[View a live demo](https://design-assets.hubspot.com/vitality/home)

A theme designed for businesses and agencies.

The Vitality theme includes:

- 8 modules
- 18 templates
To download this theme using the HubSpot CLI, run the following command:

```shell
hs fetch @hubspot/vitality <destination-folder>
```


# Getting started with themes

A theme is a portable and self-contained package of developer assets designed to work together to enable a flexible content editing experience. These assets might include templates, modules, CSS files, JavaScript files, images, and more. Themes allow developers to create a set of theme fields, similar to module fields, which allow content creators to control global website styles without having to edit CSS.

You can use HubL to access the values of theme fields throughout the theme's CSS. Content creators can then use the theme editor to modify theme fields, preview those changes against existing templates within a theme, and publish their changes.
This document walks through creating your first theme based on the [HubSpot CMS Boilerplate](/guides/cms/content/themes/hubspot-cms-boilerplate). For more on themes, see the [themes reference documentation.](/guides/cms/content/themes/overview)
If this is your first experience with CMS Hub development it's recommended you go through:
Before you begin, you'll need to install the [HubSpot CLI](/guides/cms/setup/getting-started-with-local-development).
## 1. Start a boilerplate theme project

Run `hs create website-theme my-website-theme` to create a `my-website-theme` directory populated with files from the [CMS theme boilerplate](https://github.com/HubSpot/cms-theme-boilerplate).

## 2. Upload the CMS Boilerplate to your HubSpot account

Run `hs upload my-website-theme my-website-theme`. This will upload the boilerplate to your account’s design manager, in a folder titled _my-website-theme_.

## 3. Create a page

To create a page from the uploaded theme:

- In your HubSpot account, navigate to **Marketing** > **Website** > **Website Pages.**
- In the upper right, click **Create**, then select **Website page**.
- In the dialog box, select the **domain** that the page will be published to, then enter a **page name**. Then, click **Create page**.
- On the template selection screen, templates from your [active theme](https://knowledge.hubspot.com/website-pages/use-themes#use-an-active-theme) will appear at the top of the page.
  - If you haven’t selected an active theme, hover over **CMS theme boilerplate** and click **Set as active theme**.
  - If you've already set an active theme, select your new theme by clicking the **theme selector** dropdown menu, then selecting **Change theme**. Then, hover over **CMS theme boilerplate** and click **Set as active theme**. On the next screen, select a **template**.
You'll then be brought to the page editor where you can edit the theme's fields.

## 4. Edit theme fields

- In the left sidebar of the page editor, click the **Themes** tab.
- On the _Themes_ tab, click **Edit theme settings**. This is where you can modify your existing theme settings. Publishing changes to theme settings will update the styles across your pages using this theme that was updated.
## 5. Prepare to make local changes

Return to your terminal, then run `hs watch my-website-theme my-website-theme`. This [command](/guides/cms/setup/getting-started-with-local-development) watches your local directory and automatically uploads the following changes to your HubSpot account on file saves.

## 6. Add a theme field

Now that we're listening for local changes, add a new theme field:

- Open `fields.json` file in your editor. This file controls the available fields in the theme editor sidebar. We’ll be adding a new [field](/guides/cms/content/themes/overview#fields-json) to specify the height of the footer.
- Near the bottom of the file, locate the `footer` group.
- Copy the code below and paste the JSON into the file above the first item in the children array for the footer group.

```json
// fields.json
      {
      "id" : "",
      "name" : "height",
      "label" : "Footer height",
      "required" : false,
      "locked" : false,
      "display" : "text",
      "step" : 1,
      "type" : "number",
      "min" : 10,
      "max" : 900,
      "help_text":"This footer will expand in height to accomodate any content added to the footer. You are setting the minimum height in px",
      "default" : 100
      },
```

- Save `fields.json`, and refresh the theme previewer in HubSpot. Your new field should display in the left-hand sidebar.

## 7. Reference the field in your CSS

- In your code editor, open the `theme-overrides.css` file. Then locate the css selector for `.footer`. We're now going to add a `min-height` to this selector.
- To access a value in a theme, use the `theme` object. For example, you would use `{{ theme.footer.height }}` to access the height value set in our height field.
- Replace the `.footer` declaration in theme-overrides.css with the following:

```css
.footer {
  background-color: {{ footer_bg_color }};
  min-height: {{theme.footer.height}}px;
}
```

- Save `theme-overrides.css` to upload it to your HubSpot account.

## 8. Test changes

Return to the theme editor, and refresh the page to see your new field appear under footer. Update the height value to have it reflected immediately in the preview. It might be helpful to set a background color for the footer so you can see the change more easily.

## Next Steps

Now that you've created and updated your theme, you can now create more theme fields and customize them for your projects. For more customization options, check out the [themes overview](/guides/cms/content/themes/overview). While building out your theme, it might be helpful to view the best practices for [optimizing websites on the HubSpot CMS](/guides/cms/content/performance/speed).

HubSpot has several [default themes](/guides/cms/content/themes/default-themes) that come with CMS Hub. These themes are available for you to view, clone, and update, to learn how you might use a theme in a real-world scenario.

Once you've got a handle on themes, [learn how to build your first custom module](/guides/cms/content/modules/quickstart).


# The HubSpot CMS Boilerplate
## Why should developers use the HubSpot CMS Boilerplate?

The HubSpot CMS Boilerplate is built and actively maintained by HubSpot. When building the boilerplate, we incorporated best practices that were influenced by how developers created the best website building experience and then applied those to building a website on the HubSpot CMS. This provides developers with a clean, [performant](/guides/cms/content/performance/speed), and ready to modify website that saves developers a significant amount of developmental time. The boilerplate also provides comprehensive CSS for HubSpot related assets such as forms, menu modules, base classes and more. You can view a live demo of the boilerplate in action by visiting [https://boilerplate.hubspotcms.com/](https://boilerplate.hubspotcms.com/)

## How to get started using the HubSpot CMS Boilerplate

To get started with using the boilerplate in your local development environment, simply follow our [Getting Started Developing Websites on the HubSpot CMS guide.](/guides/cms/quickstart)

## HubSpot CMS Boilerplate structure
The HubSpot CMS Boilerplate’s underlying template structure revolves around a [common base layout](https://github.com/HubSpot/cms-theme-boilerplate/blob/master/src/templates/layouts/base.html), located in the **templates > layouts** folder, that is then `{% extends %}` tag and referencing the `{% block body %}` block for its main content. A sample of how the extend tag and blocks are being used can be seen in [any of the html files inside of the templates directory.](https://github.com/HubSpot/cms-theme-boilerplate/tree/master/src/templates) Learn more about [blocks and extends](/reference/cms/hubl/overview#blocks-and-extends).

This is a common method of developing on CMS systems where you have a base (sometimes called a main/parent) template that contains all the main common structural pieces of content on your site. These are often items that are inside of the `<head>` element on your site such as common meta properties (ex: Title and Meta Description), Favicon links, CSS links, and 3rd party scripts

```hubl
<!doctype html>
<html lang="{{ html_lang }}" {{ html_lang_dir }}>
 <head>
   <meta charset="utf-8">
   <title>{{ page_meta.html_title }}</title>
   {% if site_settings.favicon_src %}<link rel="shortcut icon" href="{{ site_settings.favicon_src }}" />{% endif %}
   <meta name="description" content="{{ page_meta.meta_description }}">
   {{ require_css(get_asset_url("../../css/layout.css")) }}
   {{ require_css(get_asset_url("../../css/main.css")) }}
   {{ require_css("https://fonts.googleapis.com/css?family=Merriweather:400,700|Lato:400,700&display=swap") }}
   {{ require_js(get_asset_url("../../js/main.js")) }}
   {{ standard_header_includes }}
 </head>
 <body>

   {{ standard_footer_includes }}
 </body>
</html>
```

Inside of this base layout, there are also calls to our global header and footer partials. This allows us to be able to keep the code for these partials in their own separate files for modularity and, because they are global partials, can then be easily edited using our [Global Content Editor](/guides/cms/content/global-content) by your content creators.

For more depth into the assets included in the boilerplate [check out the boilerplate's wiki on GitHub](https://github.com/HubSpot/cms-theme-boilerplate/wiki).

## jQuery

**The HubSpot Theme boilerplate doesn't require jQuery in order to function**. For older HubSpot accounts jQuery is loaded by default. Newer HubSpot accounts have jQuery disabled by default.

Historically HubSpot scripts required jQuery to function properly, so the domain-wide setting was there to help ensure compatibility. HubSpot scripts no longer use jQuery. Because JQuery is not required, and there are better ways for developers to include libraries that also work with source control. It is advised to disable the jQuery settings for new websites.

Be aware if disabling jQuery on a domain that has an existing website - any landing pages or existing web pages you may have could break if they depend on jQuery.

**If you wish to use jQuery on your new website it is recommended that you use the latest version of jQuery.** There are two easy ways to do that:

- Upload the latest version of jQuery to your developer file system and use [`require_js`](/reference/cms/hubl/functions#require-js) to load it where and when you need it.
- Use a CDN you trust, and use [`require_js`](/reference/cms/hubl/functions#require-js) to load jQuery where and when you need it.

## Related resources

- [Getting started with themes](/guides/cms/content/themes/getting-started)
- [How to optimize your CMS Hub site for performance](/guides/cms/content/performance/speed)
- [Getting started with accessibility](/guides/cms/content/accessibility)


# Themes overview

A theme is a portable and contained collection of developer assets designed to enable a flexible content editing experience. You can [build themes locally using the HubSpot CLI](/guides/cms/setup/getting-started-with-local-development) using the tools, technologies, and workflows that you prefer. Themes and all of their files are also portable between environments and accounts. For a video walkthrough of getting started developing themes, view the HubSpot Academy video below:
## Themes as a package

Themes being a package cascades throughout the HubSpot app in various places to enable an efficient content creation experience. Developers can use themes to build a design system for content creators to work within. Whatever amount of flexibility, or guardrails can be built into a theme to meet the needs of your business.

### Page creation

When content creators start building new pages, they are prompted to start by selecting which theme they are building a page from, followed by selecting which template within the theme to use.

.

### Theme fields

Themes allow developers to create a set of [Theme Fields](#fields-json), similar to Module Fields, which allow content creators to tweak various knobs and dials designed by a developer to allow global stylistic control over a website without having to edit CSS. Developers [use HubL to access the values of Theme Fields](#using-theme-field-values) throughout their CSS. Content creators use the Theme Editor to modify Theme Fields, preview those changes against existing templates within a Theme, and publish their changes.
Theme fields are dictated by the [fields.json file of a theme](#fields-json).

When editing a theme in [test mode](/guides/cms/content/themes/overview#test-mode), you can also copy the theme's settings JSON. This allows you to paste any updates into the theme's local `fields.json` file.

### Theme modules

The modules within a theme should be designed specifically for use within templates in that theme. The content editor will emphasize these theme modules, making it quick and easy for content creators to add modules to the pages they are building that are designed to work well in the context of that page. Default modules and the rest of the modules in your HubSpot account will still be available.
When developing a theme, you can [hide modules and sections from the page editor](/guides/cms/content/modules/hide-modules-and-sections) to create a more streamlined content creation experience.

## Theme file structure

A theme is a single directory of files. You can include HTML, CSS, and Javascript files, modules and additional files that can be organized in any manner within subdirectories of the parent theme folder. Two JSON files are necessary to build a theme: `theme.json` and `fields.json`. These files should be included at the root theme folder.

To start from an example, see the [HubSpot CMS Boilerplate](https://github.com/HubSpot/cms-theme-boilerplate).
At this time .json files can only be created and uploaded to a HubSpot account through the [local development tools](/guides/cms/setup/getting-started-with-local-development).
### theme.json

The `theme.json` file contains the meta-information for your theme directory, such as the themes readable label, its preview screenshot and various configurations for how the theme should behave. Your `theme.json` file will look similar to the following:

```json
// theme.json
{
  "label": "Cool Theme",
  "preview_path": "./templates/home-page.html",
  "screenshot_path": "./images/templates/homepage.jpg",
  "enable_domain_stylesheets": false,
  "version": "1.0",
  "author": {
    "name": "Jon McLaren",
    "email": "noreply@hubspot.com",
    "url": "https://theme-provider.com/"
  },
  "documentation_url": "https://theme-provider.com/cool-theme/documentation",
  "license": "./license.txt",
  "example_url": "https://theme-provider.com/cool-theme/demo",
  "is_available_for_new_content": true
}
```

| Parameter | Type | Description |
| --- | --- | --- |
| `label` | String | The readable label of the theme, used in various places the theme is shown throughout the HubSpot app, like the template selection screen and the theme editor. |
| `preview_path` | String | A relative path to a template file in the theme which should be the default template, used when previewing a theme in the theme editor. |
| `screenshot_path` | String | A relative path to an image file that is used to give a snapshot of what the theme looks like in various places theme selection occurs, such as in the template selection screen. |
| `enable_domain_stylesheets` | Boolean | Enabling or disabling stylesheets attached to domains in Website Settings getting included on templates within the theme. The default value is `false`. |
| `version` | String | Integer version number supporting `.` versions. |
| `Author` | object | Object to provide information about yourself as the theme provider.`name` <br />The provider's name. <br />`email` <br />The provider's support email address. <br />`url` <br />The provider's website. <br /> |
| `documentation_url` | String | The theme documentation link. |
| `example_url` | String | The theme live example link. |
| `license` | String | A valid [SPDX Identifier](https://spdx.org/licenses/) or the relative path to the license within your theme.This license dictates what use and modification is permitted by the creator of the theme. Useful when submitting to the marketplace. |
| `is_available_for_new_content` | Boolean | Boolean that determines if a theme shows up in the content creator page for selection. The default value is `true`. |

### fields.json

The `fields.json` file controls the available fields and field groups in the theme editor, including [style fields](/guides/cms/content/fields/overview#style-fields). The fields you include will depend on how much control you want content creators to have in the page editor. The number of fields available for themes is more limited than for modules, as theme fields are best for styling options, while [global content](/guides/cms/content/global-content) is better for theme content.

For example, rather than adding a text field to the theme's `field.json` for your site's tagline, it should add it as a global module so that content creators can update the tagline from the page editor rather than the theme editor.

The fields that are available for use in themes are:

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

For comprehensive documentation on the possible options for theme fields, see the [module and theme fields documentation](/reference/cms/fields/module-theme-fields).

#### Using theme field values

To access field values, use dot notation and prefix the path to the value in `fields.json` with theme. You can use a themes fields value in your stylesheets using syntax like `{{ theme.path.to.value }}`. For example, the font field outlined below:

```json
// fields.json
[
  {
    "type": "group",
    "name": "typography",
    "label": "Typography",
    "children": [
      {
        "type": "font",
        "name": "h1_font",
        "label": "Heading 1",
        "load_external_fonts": true,
        "default": {
          "color": "#000",
          "font": "Merriweather",
          "font_set": "GOOGLE",
          "variant": "700",
          "size": "48"
        }
      }
    ]
  }
]
```

Would be referenced in CSS with:

```css
h1 {
 font-size: {{ theme.typography.h1_font.size }}px;
 font-family: {{ theme.typography.h1_font.font }};
 color: {{ theme.typography.h1_font.color }};
 text-decoration: {{ theme.typography.h1_font.styles.text-decoration }};
 font-style: {{ theme.typography.h1_font.styles.font-style }};
 font-weight: {{ theme.typography.h1_font.styles.font-weight }};
}
```

## Previewing themes

For developers sometimes you need to be able to test out that your theme fields are working properly but don't want to impact real pages. That's where theme test mode comes in.

### Test mode
There are two ways to enable test mode:

- To activate test mode from the design manager:

  - In the design manager, select your **theme** in the finder.
  - At the top of the left sidebar, click the **Preview** button.

    

- To activate test mode from the page editor:

  - In the page editor, click the **Design** tab in the left sidebar, then click **Edit theme settings**.

    

  - Add `?testmode=true` to the URL, then hit **enter**. You'll then be in test mode.

Another method is to open your theme settings from within the page editor. Then once inside add the query parameter `?testmode=true` to the URL in your address bar.

## Related resources

- [Getting started with themes](/guides/cms/content/themes/getting-started)
- [How to add theme capabilities to existing sites](/guides/cms/improve-older-sites/add-theme-features-to-existing-sites)
- [HubSpot theme boilerplate](/guides/cms/content/themes/hubspot-cms-boilerplate)


# Getting started from the CMS quotes theme

With a CMS quotes theme, you can create a custom quote theme for sales reps to use during the buying process. This guide will walk you through cloning the default CMS quotes theme locally using the CLI, uploading the clone to your account, then making adjustments as needed. You'll then create a quote using the template to view your work.

## Prerequisites

- You should feel confident writing HTML and CSS.
- You should have the latest version of the [HubSpot CLI installed and configured for your portal](/guides/cms/setup/getting-started-with-local-development).
While this tutorial uses the HubSpot CLI, you can do all of this in HubSpot using the design manager if preferred. To complete this process in HubSpot, you'll just need to clone the `cms-quotes-theme` in the `@hubspot` folder instead of running the `fetch` command. shown in step 1.
## 1. Fetching the theme to your local directory

Open your terminal, navigate to the directory you want to download the files to. This will be your main working directory for the remainder of this tutorial.

To download the default quotes theme, run the following in your terminal:

```shell
hs fetch @hubspot/cms-quotes-theme "my-quotes-theme"
```

You should now see a folder named `my-quotes-theme` in your local file system. This folder contains all of the assets needed for the quote theme, including mock data and module defaults within the `imports` folder.

## 2. Upload and watch changes

With the folder downloaded, upload it to HubSpot. While you can use the [hs upload](/guides/cms/tools/local-development-cli#upload) command to perform a single upload, you can instead use the `watch` command to trigger automatic uploads on each file save:

```shell
hs watch "my-quotes-theme" "my-quotes-theme" --initial-upload
```

After upload, you can now view the `my-quotes-theme` folder in the [design manager](https://app.hubspot.com/login?loginRedirectUrl=https%3A%2F%2Fapp.hubspot.com%2Fshortlink%2Fdesign-manager%2F). To open the design manager from the terminal, open a new terminal tab or window and run the `hs open dm` command.
A new terminal tab or window is needed because you cannot run commands while the `hs watch` process is running. You can also press `q` to end the watch, run another command, then rerun `hs watch`.
## 3. Open a template preview

To preview the quote template:

- In the design manager, navigate to **my-quotes-theme > templates > bold.html.**
- In the top right of the code editor, click **Preview**, then select **Live preview with display options**.

With the template preview open, you'll then make a change locally, which will be automatically uploaded on save due to `hs watch` running.

## 4. Make a change locally

- In your local code editor, open **my-quotes-theme > css > bold.css.**
- Add the code below to **bold.css**, then save your changes:

```css
.line-items__table tbody tr:nth-child(odd) {
  background-color: #d6d6d6;
}
```

- Refresh the template preview in your browser to view your CSS changes. You should now see that every odd row in the table body has a gray background.

As you build your custom quote theme, you can repeat this workflow to quickly confirm the changes that you're making locally.
Because of the complexity of the signature system, signatures will not display in previews.
## 5. Change the template label

As you prepare a custom quotes theme for real life use, you should be mindful of the template label so that sales reps can easily find it among HubSpot's default quote options.

To change a quote template's label:

- In your code editor, open **my-quotes-theme > templates > bold.html**.
- At the top of the file, view the template annotation:

```html
<!--
 templateType: quote
 isAvailableForNewContent: true
 label: Bold
-->
```

- Update the `label` parameter from `Bold` to a name of your choosing, such as **`My custom quote template`**.
- Save the file to upload it to HubSpot.

## 6. Customize the quote template in HubSpot

Before a sales rep can use your quote template, it must be customized in HubSpot. This would typically be done by a sales manager so that they can [create ready-made quotes for their sales team](https://knowledge.hubspot.com/quotes/create-custom-quote-templates). However, in this tutorial, you'll walk through this process yourself so that you can understand what the content creation experience is like.

To customize the quote template and make it available for sales reps:

- In your HubSpot account, click the settings **settings icon** in the main navigation bar.
- In the left sidebar menu, navigate to **Objects > Quotes**.
- Click the **Quote templates** tab.
- In the upper right, click **Customize quote template.**
- Hover over your new **template**, then select **Choose**.
- Using the left panel, you can edit the modules included in the template. For example, you can click a **module** to edit its properties or toggle visibility.
- In the upper right, click **Save** when you're done making changes.

## 7. Create a quote using your new template

With your changes saved, you can now create a quote with the template, simulating the sales rep experience.

- In your HubSpot account, navigate to **Sales > Quotes**.
- In the upper right, click **Create quote**. You'll then be redirected to a quote creation wizard.
- On the first screen, click the **Associate with a deal** dropdown menu, then either select an existing deal or select **Create a new deal** if you want to use a test deal instead.
- In the bottom right, click **Next**.
- On the next screen, click the **Quote** dropdown menu, then select your custom quote template.
- Proceed through the rest of the quote wizard to create your quote.
- After publishing the quote, a dialog box will appear with a link to view the quote. Click **Copy** to copy the URL, then paste it into your browser to view the completed quote.
## Next steps

With your quote template created, uploaded, and customized, you should now have a better understanding of the quote template editing process and the content creator experience.

As you create quote templates to fit your business needs, you may want to try adding your own custom modules to the quote along with HubSpot's default modules.
It's recommended to <u>not</u> edit the JavaScript of the payment, signature, and download modules, as this could lead to breaking the modules. If broken, the end-user might not be able to sign it, download it, or even make a payment.
## Related Resources

- [Custom quote templates](/guides/cms/content/templates/types/quotes)
- [Custom quote variable reference](/reference/cms/hubl/quote-variables)
- [Create and use custom quote templates (from the sales, sales ops/manager perspective)](https://knowledge.hubspot.com/quotes/create-custom-quote-templates-beta)


# Set responsive breakpoints for themes

While developing a theme, you can define responsive breakpoints to optimize styling for mobile and desktop. Below, learn more about how to set breakpoints and define responsive styles in [dnd_area tags](/reference/cms/hubl/tags/dnd-areas) using HubL.

You can use these breakpoints to define the following styling for specific asset types:

- `dnd_section`, `dnd_column`, and `dnd_row` support margin and padding across breakpoints.
- Default modules in a `dnd_area` support margin, padding, and visibility.
- Default modules outside a `dnd_area` support only margin and padding.
- Custom modules in a `dnd_area` support visibility.
- Custom modules outside a `dnd_area` don't support breakpoint based customizations.

In addition, only two breakpoints are possible at this time: `mobile` and `default` (optional for desktop).

## Define breakpoints in a theme

You can define a set of breakpoints in your theme by adding the `responsive_breakpoints` object to your `themes.json` file. Inside of this object is a set of key/value pairs that will contain information about your breakpoint.

```json
// themes.json
{
  "label": "My Theme",
  "preview_path": "./path/to/preview.html",
  "screenshot_path": "./images/template-previews/home.png",
  "responsive_breakpoints": [
    {
      "name": "mobile",
      "mediaQuery": "@media (max-width: 767px)",
      "previewWidth": {
        "value": 520
      }
    }
  ]
}
```

Below are the properties you can include within `responsive_breakpoints`:

| Key | Type | Description |
| --- | --- | --- |
| `name` | String | The name of the breakpoint. At this time only `"mobile"` is available for use. |
| `mediaQuery` | String | A media query string for the renderer/editors to use when generating responsive CSS. e.g. `"@media (max-width: 767px)"` |
| `previewWidth` | Key/Value Pair | To give clues to the editor as to what size we should show our preview iframe at.e.g. `{"value": 520}` |

## Define responsive styles in dnd_area tags

While building [drag and drop areas](/reference/cms/hubl/tags/dnd-areas), you can including responsive styling using HubL. This functionality currently works in the `dnd_section`, `dnd_row`, and `dnd_column` tags.

Take the following example of a [dnd_section](/reference/cms/hubl/tags/dnd-areas#drag-and-drop-section-code-dnd-section-code-):

```hubl
{% dnd_section padding={ 'top': 100, 'bottom': 100 } %}
{% end_dnd_section %}
```

To change the padding for the mobile breakpoint, you can include values for both the `default` (desktop view) and `mobile` breakpoints as illustrated below.

```hubl
{% dnd_section padding={
    'default': { 'top': 100, 'bottom': 100 },
    'mobile': { 'top': 20, 'bottom': 20 }
  }
%}
{% end_dnd_section %}
```


# Website Settings

Website settings is a single place where various global and system-level content settings can be configured for your website's blog, navigation, pages, and themes. Navigate to [Settings > Website](https://app.hubspot.com/l/settings/website/blogs/) and choose which content area you want to access your Content Settings for.

## Blog settings

In this area you will control the content settings for your sites blog(s). If you have multiple blogs, you can switch between them using the dropdown under the "Select a blog to modify" heading.
### General Tab

#### Blog name

The HubL variables `{{ content.blog }}` and `{{ group.public_title }}` will render the name set here.

#### Blog header

The HubL variable `{{ group.header }}` will render the header set here.

#### Page Title

The HubL variable `{{ group.html_title }}` will render the title set here.

#### Meta description

The HubL variable `{{ group.description }}` will render the description set here. This meta description will be used on blog listing pages.

#### Blog root URL

The blog root URL will precede individual blog post slugs. The HubL variable `{{ group.absolute_url }}` will render the URL set here.

#### Control audience access

You can control audience access to an entire blog via this setting. More on that [here](https://knowledge.hubspot.com/cms-pages-editor/control-audience-access-to-pages).

### Templates Tab

#### Current template

This is the template used for all blog posts in a particular blog. The same template will be used for blog listing pages as well by default. Varying content for listing pages versus posts can be written within the Post Content module.

#### Template for listing pages (optional)

This setting enables a different template for blog listing pages other than the template used for blog posts.

#### Number of posts per listing page

This determines the number of post items that appear on a blog listing page by default.

#### Header HTML

Any code added to the blog listing header HTML will be added to all listing pages via the `{{ standard_header_includes }}` variable. Any code added to the blog post header HTML will be added to all blog posts via the `{{ standard_header_includes }}` variable.

#### Footer HTML

Any code added to the blog listing footer HTML will be added to all listing pages via the `{{ standard_footer_includes }}` variable. Any code added to the blog post footer HTML will be added to all blog posts via the `{{ standard_footer_includes }}` variable.

### Subscriptions Tab

#### Blog subscriber notification emails

Instant, Daily, Weekly, and Monthly blog notification emails can be enabled and edited via this setting. These emails go out automatically if new blog posts were published in the given timeframe. Read more [here](https://knowledge.hubspot.com/cos-blog/set-up-your-blog-subscription-features-in-hubspot).

#### RSS feed

The number of post items in the blog RSS feed can be determined via this setting. There is a maximum of 50 posts.

### Date Formats Tab

#### Language for dates

This setting determines the language of months and days that appear in blog dates.

#### Publish date format

The format set here determines the order and pattern of publish dates in blogs. Using Local Data Markup Language, it is possible to specify a custom date format.

#### Posts by month format

The format set here determines the order and pattern of posts by month. Using Local Data Markup Language, it is possible to specify a custom date format.

### Comments Tab

#### Enable or disable blog comments

It is possible to enable or disable blog comments via this setting. Comments can require moderation, or have an established timeframe after which comments are automatically closed.

#### Blog comment notifications

Blog comments can trigger email notifications to specified users via this setting.

### Social Sharing Tab

#### Default Twitter account

The Twitter account specified here will be used for Twitter Cards when content is shared on Twitter.

#### Social sharing buttons

Social sharing buttons for Twitter, LinkedIn, and Facebook can be automatically added to blog posts via this setting.

### Google AMP Tab

Accelerated Mobile Pages (AMP) load your content instantly. Read more [here](https://knowledge.hubspot.com/cos-general/how-to-use-accelerated-mobile-pages-amp-in-hubspot). In order to load content so quickly, a simplified page experience is required. AMP content has limited styling control for this reason.

#### Enable or disable Google AMP

Google AMP formatted pages can be enabled via this setting. AMP logo, font, and color settings

#### AMP-specific settings for a logo, header formatting

In order to deliver AMP content, simplified styling is required. Determine AMP-specific styles for the logo, header formatting, fonts, and colors via these settings.

## Navigation settings

You can manage your menu links and labels in this area. You can switch between which menus you want to configure by choosing the dropdown and selecting your desired menu. [Learn more about setting up your sites navigation menus here.](https://knowledge.hubspot.com/cos-general/set-up-your-site-s-navigation-menus)
## Page settings

Settings are broken down by domains, and default values for all domains can be set. The “_Default for all domains_” settings will be displayed when navigating to Pages Settings. There is a toggle at the top of the screen to view and modify settings for specific subdomains. Settings applied to specific subdomains will override the default for all domains settings.

Only users with the “[Edit website settings” Marketing permission](https://knowledge.hubspot.com/settings/hubspot-user-permissions-guide) can access and edit Content Settings.
### Templates Tab

#### Site header HTML

Any code added into the site header HTML field in Pages Settings will be included in the `{{ standard_header_includes }}` variable.

#### Site footer HTML

Any code added into the site footer HTML field in Pages Settings will be included in the `{{ standard_footer_includes }}` variable. Typically this is a good place for adding tracking codes and other scripts that are "non-essential" to your site functioning or looking good. That way it will not negatively impact any of your templates or pages.

#### jQuery

You can modify the jQuery version loading on your page via Pages Settings.

You can also opt to load jQuery from your footer via this setting. Appending `?hsMoveJQueryToFooter=True` to your page URL will allow you to test this change and ensure it does not impact your site’s appearance negatively.

The option to disable jQuery is also located within Pages Settings.

### Branding Tab

The logo image set for each domain here will automatically be used in the default “Logo” module.

#### Favicon

#### Logo (alt text, dimensions, link)

Your favicon image source URL can be pulled from the `site_settings` dictionary and used in your coded files:

```hubl
{% if site_settings.favicon_src %}<link rel="icon" href="{{ site_settings.favicon_src }}" />{% endif %}
```

### Personalization Tab

#### Contact & Company defaults

These are the default values used for personalization tokens used on pages when the visitor is unknown.

### Integrations Tab

#### JS Integrations

Enable JS Integrations (like Google Analytics or AdRoll) for all domains or select domains here. If using other tracking scripts or Google Tag Manager instead of Google Analytics that code should be added to the [site footer HTML](#site-footer-html).

### SEO & Crawlers Tab

#### Canonical URLs

Select your default canonicalization setting for individual pages and posts, as well as listing pages, here.

#### Default File Hosting Domain

This controls the domain that assets from the file manager appear to be hosted at.

#### Robots.txt

Modify your robots.txt file for each domain here.

### System Pages Tab

For several system pages, you can select your templates in Pages Settings. No page editor exists for these pages, only templates in the Design Manager that are created with the “System” template type. **_Please note_**_: Email preferences and subscription templates are located in Email Settings, not Pages Settings._

## 404 and 500 error pages

These are the pages that are returned for 404 or 500 status codes. When creating a new template, select the [“Error page” template type](/guides/cms/content/templates/overview#error-pages) in the Design Manager to make a template available for these system pages.

## Password Prompt Page

This is the page that is returned for [password-protected pages](https://knowledge.hubspot.com/cos-pages-editor/how-can-i-password-protect-my-pages) when prompting a visitor to input a password. When creating a new template, select the [“Password prompt page” template type](/guides/cms/content/templates/overview#password-prompt) in the Design Manager to make a template available for this kind of system page.

#### Search results page and URL

This is the page that lists search results for queries input into HubSpot’s [Site Search](https://knowledge.hubspot.com/cos-general/how-do-i-set-up-a-results-page-for-my-search-field-in-hubspot) module. Read more on how to customize your search [here](/reference/api/cms/site-search/v2#search-your-site). When creating a new template, select the [“Search results page” template type](/guides/cms/content/templates/overview#search-results-page) in the Design Manager to make a template for this kind of system page. You can also determine your search results page URL in Pages Settings.

## Themes settings

Here you can find all the themes added to your site. You can go into the themes editor by clicking on one of the themes available on your site.


# Code alerts
Code alerts provide a centralized location for developers and IT managers to see an overview listing of issues that are identified inside of your HubSpot CMS. By fixing the issues that are identified in Code Alerts it can help to optimize your website by helping to improve your customers experience and your sites performance as a whole.
To see other ways HubSpot helps to maximize your site's potential, check out [our reference page on CDN, Security, and Performance.](/guides/cms/content/performance/overview)
## How to view code alerts

View your code alerts for your entire portal by clicking on the Sprocket Menu from any published CMS page you are authenticated to and choosing **View Code Alerts** or go directly to [Code Alerts](https://app.hubspot.com/l/code-alerts).
## Asset types and issues

### Asset types

There are multiple types of assets that code alerts can scan within your HubSpot CMS website. These are listed below.

| Asset type | Description |
| --- | --- |
| `Blog Post` | A blog post from one of your HubSpot blogs. |
| `Site Page` | A website page on the HubSpot CMS. |
| `Landing Page` | A website page with a specific purpose — the objective of a landing page is to convert visitors into leads. |
| `Blog` | Your HubSpot blog listing page. |
| `Module` | Reusable components that can be used in templates or added to pages. |
| `Template` | Templates are reusable page or email wrappers that generally place modules and partials into a layout. |
| `CSS` | A Cascading Style Sheet file. |
| `Knowledge Article` | An article from your HubSpot Knowledge Base. |
| `Unknown` | When an asset type is unknown. |

### Issues

There are multiple types of issues that your assets can have. If your asset has more than one issue it will be listed multiple times inside of your code alert dashboard.

| Issue | Example | Description |
| --- | --- | --- |
| `HubL limit` | The HubL function **blog_recent_tag_posts** is used **11** times on this page. **blog_recent_tag_posts** has a limit of **5** uses per page. | [Certain HubL functions have limits](/not-found) to their usage. If your function exceeds its limits, you will be shown this issue. |
| `CSS combining` | There is a syntax error in a line of code that is preventing the CSS files from being combined. | Identifies CSS files that contain issues that would prevent the minification/combining of the file. There is also an option to **Show syntax errors** that are related to this asset. |
| `Output too big` | This page is **10446 Kilobytes**. The size limit is **9765 Kilobytes**. | The generated HTML for the page has reached more than the limit. This may result in seeing a blank or partial page. |
| `Template error` | There is an error in the code that is preventing this template from being rendered. | Identifies errors in your template code that prevent the template from being rendered. |
| `Unknown` | Unknown issue | When the system has a hard time identifying the error in the asset, an unknown issue will be assigned. |

## How to view the issues identified for your assets

Code alerts include deep linking to assets where alerts are detected. When hovering over a row, you will be provided with the following options per asset type.

### Template, page, and module assets

The actions button will provide you with a link to open the corresponding template or module in the design manager.
### CSS assets

The actions button will provide you with a link to open the corresponding stylesheet in the design manager, view your file with debugging info, or show you the syntax errors.
When choosing “Show syntax error” you will be shown a modal window with additional details about the syntax error along with the line number where the error exists.


# Debugging methods and error types

Debugging code and understanding where and how to view errors is an important part of development on the HubSpot CMS. There are a number of tools you can use to increase efficiency when building and debugging and to make sure your website is optimized as you continue to build it out.

## Errors

The HubSpot CMS [developer file system](/guides/cms/overview#developer-file-system) has many forms of validation to ensure your templates and modules render correctly on pages.

### Fatal errors

Fatal errors are errors that would prevent a page from successfully rendering. To ensure live content renders correctly, the HubSpot CMS prevents publishing templates that have fatal errors. An example of a fatal error would be missing required HubL variables, such as `standard_header_includes`. This will cause errors when developing in the Design Manager or when uploading files through the CMS CLI. [The VS Code Extension](https://marketplace.visualstudio.com/items) supports HubL linting, and can display the fatal errors in-context ahead of uploading the file.
Fatal errors must be resolved in order to publish files.

### Warnings

Warnings are errors or issues which do not prevent the publishing of files. Warnings are often suggestions in syntax or potential issues a developer might be missing. [The VS Code Extension](https://marketplace.visualstudio.com/items) supports HubL linting, and can display the warnings in-context ahead of uploading the file. For example, if you try to include a file which does not exist, this throws a warning to alert the developer.
Warnings will never prevent the publishing of files, however, it is recommended to investigate warnings.

## Debug mode on live pages

You can enable debug mode on a live page by loading the page with a `?hsDebug=true` query string in the URL.
Debug mode isn't supported on [system pages](/guides/cms/content/templates/overview#system-pages), such as 404 and password pages.
When loading a live page with this query string, the page will be rendered:

- with non-minified files.
- with non-combined CSS files (individual CSS files served).
- without serving cached files.

In addition, when you load a page with `?hsDebug=true`, debugging information will be added to the bottom of the page source code, including:

- Whether the page can be [prerendered](/guides/cms/content/performance/prerendering), and the reasons why if it cannot be prerendered.
- A breakdown of rendering request timing, which can be helpful for knowing which page components take longer to render. This breakdown will also be added to the _Timing_ tab in your browser's developer console under _Doc_ requests.
- Errors and warnings, such as HubL function limits or missing files.
## Developer mode in the page editor

You can also load the page editor in HubSpot with the query string to enable developer features, such as [copy sections as HubL](/guides/cms/content/templates/drag-and-drop/sections#copy-section-hubl) and the ability to open specific modules in the design manager from the page editor.

- In the page editor, add the following parameter to the URL, then press **Enter**: `?developerMode=true`
- With the page reloaded, you'll now be in developer mode. You can exit developer mode at any time by clicking **Exit developer mode** in the upper right.

While you're in developer mode, you can navigate to the code for a specific module by clicking the associated module, then clicking **Open in design manager** in the sidebar editor.
You can also reset any unpublished changes back to the default content of the template:

- Click the **Contents** tab.
- To the right of template name, click **Reset content**.
- In the dialog box, click **Yes, reset**.

## View HubL output

Within the Design Manager, coded files have a “Show output” toggle which open up a second code editor panel, with a transpiled code of the file you are looking at. This is helpful to see how your HubL code will transpile into CSS, HTML or JavaScript, rather than reloading live pages the file is included on. It is also a helpful tool to use when exploring new features of HubL, or learning the basics of HubL as you can easily see what your HubL input will output to.
## `|pprint` HubL filter

The `|pprint` HubL filter can be used on HubL variables to print valuable debugging information. It will print the type of HubL variable, which can be useful in understanding what expressions, filters, operators or functions it can be used with.

For example, `{{ local_dt }}` will print `2020-02-21 12:52:20`. If we pretty print this variable, we can see the value is a date `(PyishDate: 2020-02-21 12:52:20)`. This means we can use HubL filters that operate or format date objects, such as the `|datetimeformat` HubL filter.
```hubl
{{ local_dt }}
{{ local_dt|pprint }}
{{ local_dt|datetimeformat('%B %e, %Y') }}
```
```html
2020-02-21 12:55:13 (PyishDate: 2020-02-21 12:55:13) February 21, 2020
```
## Developer info
Much of the data found in developer info is used internally, and is subject to change if not otherwise documented.
The developer info for a page is the context of all data available when a page is being rendered. This rendering context is all accessible via HubL. To access the developer info for a page, select the **HubSpot sprocket icon in the top right hand corner of live pages > Developer Info.**
This will open up a new tab that returns the rendering context for a given page in the form of JSON. It is recommended to have a JSON formatter installed in your browser to make the developer info easier to read, such as this [JSON formatter Chrome extension](https://chromewebstore.google.com/detail/json-formatter/bcjindcccaagfpapjjmafapmmgkkhgoa). While much of the information contained in the page's context is for internal purposes, this tool can be valuable to see what data is available via HubL when templating.

For example, the following image is of the developer info for [https://desigers.hubspot.com/docs/developer-reference/cdn](/guides/cms/content/performance/overview).
The values of this data are being set through the Settings tab of the Content Editor:
The values are then accessible to render on pages through HubL. To print the title and meta description in a [base template](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/layouts/base.html), you would use the following HubL.

```hubl
<title>{{ page_meta.html_title }}</title>
<meta name="description" content="{{ page_meta.meta_description }}">
```

The data in the rendering context is available through HubL, and the JSON tree can be traversed using dot notation. Data in the developer info that developers frequently print out include module field values and tags that have been [exported to template context](/reference/cms/modules/export-to-template-context).

## Review website performance and broken links

It's important to verify that your site's visitors are not going to broken links. There are two tools you can use to help ensure your site visitors are getting to the correct place. You can use the [website performance API](/reference/api/deprecated/overview) to get HTTP Statuses like 404s and see your sites uptime.

If you're seeing 404 errors it's a good idea to redirect the visitor to a relevant URL.

You can also use the [SEO Recommendations tool](https://knowledge.hubspot.com/seo/view-seo-recommendations-in-hubspot) to identify broken links within your page content and quickly fix them.

## Improving website speed

There are a lot of factors that go into optimizing and testing website speed. For tools and tips to optimizing your site's speed see our guide.


# How to add theme capabilities to an existing HubSpot CMS website

One of the benefits of HubSpot is that you don't need to perpetually update it. CMS Hub is always using the latest version. The new features introduced with CMS Hub are additive. So the templates, modules, etc. for existing HubSpot CMS websites work the same as you built them. They're better even, as behind the scenes the CMS is always getting better, faster, and easier to edit.

There are some specific features of CMS Hub that a developer needs to add. You may implement some of these features different than you would for a brand new website. This tutorial will guide you in adding these features into your existing website(s).

Before you continue:

- Review the [key concepts of CMS Hub](/guides/cms/overview), even if you've been building on HubSpot for a long time. You will gain a better understanding of how all the pieces fit.
- You will also need to use the [CMS CLI](/guides/cms/tools/local-development-cli), so if you don't have it installed, [go set it up](/guides/cms/setup/getting-started-with-local-development).

You can add and use these features independently. Start at the heading for specific features you need. You do not need to follow this tutorial completely linearly.

## Theme

CMS Hub [themes](/guides/cms/content/themes/overview) are a package of templates, modules, CSS, JSON, and JS files. Themes being a package has meaning throughout the content editor experience.

When creating a new website or landing page the content creator is presented with a grid of the themes in their account. The select the theme they're using and then are shown a grid of just the templates in that theme.

When editing a page using a template from a theme, the modules in that theme get special treatment making them stand out in the add module panel.

### 1. Place existing files in a containing folder

You make your templates, modules, CSS and JS files a theme if they are within the same containing folder. It does not matter if they are organized into sub-folders, it only matters that those assets are contained within a folder. If they are already stored that way great, if not create this folder and move your assets into it. Name that folder what you want to name your theme. The name is fully up to you, but perhaps naming it after the company's brand that the site reflects, and the year of the redesign or update.

### 2. Make containing folder a theme

Themes require two JSON files, and only one of them needs actual content initially.

- **Theme.json** - contains your theme's settings.
- **Fields.json** - contains fields that can be used to make design changes across an entire site.

JSON files are special and can't currently be created in the design manager. These files CAN be uploaded through the [CMS CLI](/guides/cms/tools/local-development-cli).

1.  Fetch the containing folder you created in step "place existing files in a containing folder" to your computer.
2.  Inside of the folder on your computer, create a new file name it `theme.json`.
3.  Copy the [example `theme.json` file on the themes doc](/guides/cms/content/themes/overview#theme-json).
4.  Paste the example code into your `theme.json` file.
5.  Change the value of `"label"` to be the name of the theme, as you want content creators to see it.
6.  Change `"preview_path"` to the path of either your homepage template or your most frequently used template.
7.  Take a screenshot of the website's homepage. Name the image `thumb.jpg`. Place the image inside of the same folder as your `theme.json` file.
8.  Change `"screenshot_path"` in theme.json to the path of your `thumb.png` image.
9.  Save the file.
10. Inside of the same folder as your `theme.json` create a new file, name it fields.json. Inside of this file enter just `[]` and save the file.

To see your changes in the design manager run the `hs upload command`. You now have a basic theme. Modules and templates within that theme will display associated to the theme.

### 3. Add theme fields

Theme fields are controls you can provide to a content creator to enable them to make theme-wide styling changes.

During the initial build out of a website these theme controls enable content creators to be involved helping nail down the site wide branding. Teams may find this frees the developer up to focus on the more technical aspects of the site development.

For existing websites, theme fields may be unnecessary. After-all if the website was already custom built, there likely is no purpose to adding site wide color, and typography controls. If a site's branding is changing significantly then it may be more appropriate to do a redesign than add fields retroactively. Ultimately though this is a decision you should make mutually with the other stakeholders involved in the website.

To add fields to the theme, add their JSON to your fields.json file. The fields follow the same structure as module fields.

Ensure that the users that should have access to change the theme field values, have the "Edit global content and themes" permission. Users who you do not want to be able to edit these settings you should make sure they do NOT have this enabled.
## Clone design manager drag and drop as HTML

[Drag and drop areas](/guides/cms/content/templates/drag-and-drop/overview), and [global partials](/guides/cms/content/global-content) require the use of [coded HTML + HubL](/guides/cms/content/templates/types/html-hubl-templates) files. If your existing website may be built using the old drag and drop design manager system, and you want to use those new features, those templates may need to be cloned as HTML.

To clone these templates as HTML + HubL template:

1.  Open the design manager, and find the template in the finder.
2.  Right click the template.
3.  In the context menu that appears, choose "Clone as HTML"

You're now ready to work on adding the drag and drop areas and global partials.

## Drag and drop areas

[Drag and drop areas](/guides/cms/content/templates/drag-and-drop/overview) can be added to templates to provide content creators a way to place modules both horizontally and vertically on a page. Drag and drop areas also provide additional styling control for the content creator. Because a template with a drag and drop area can be used to create many different layouts, this frees up the developer to focus on the technical aspects of site creation and updating.
Drag and drop areas are a new feature and are not the same as [Design Manager drag and drop templates](/guides/cms/content/templates/types/drag-and-drop-templates). They are also not supported in Design Manager drag and drop templates. To add them to templates previously built using the drag and drop template builder see clone design manager drag and drop as HTML.
#### Does it make sense to convert every existing page to use drag and drop areas?

That completely depends on if the page is meeting your business goals. In other words the old adage of "If it isn't broken, don't fix it" applies. If the layout of the page needs changing, then yes it's probably wise to convert the page to use drag and drop areas. If the page is serving its purpose and doesn't need layout changes - it's probably fine as is.

### Converting existing templates

It's relatively easy to create a clone of an old template, and change the flexible column to a drag and drop area. That is a good idea, as that opens up a lot of possibilities for the content creators. That gives content creators a lot of creative control for new pages. If your template is a design manager drag and drop template see [clone design manager drag and drop as HTML](#clone-design-manager-drag-and-drop-as-html).

1.  The easiest solution is to find the instances of flexible columns, which use the HubL tag [`{% widget_container "my_unique_column_name" %}`](/reference/cms/hubl/tags/standard-tags#flexible-column).
2.  Replace each instance as necessary with [`{% dnd_area "my_unique_area_name" %}`](/reference/cms/hubl/tags/dnd-areas).
3.  If you do not care to set the default modules, leave the `dnd_area` empty. If you want to carry over the default modules for the region or set new useful defaults, within the `dnd_area` add a `dnd_section`, and inside of that a `dnd_column`.
4.  For each module tag within the old `widget_container` you will want to create a `dnd_row`, containing a `dnd_module` with a matching path to the modules you want to display by default. This will create the same vertical layout the flexible column had.

### How do I migrate existing pages?
If you are changing templates built with flexible columns to now use drag and drop areas there's a few things you should understand. **Flexible columns are not the same as drag and drop areas, you can't swap from a template that only has a flex column to one that only has a drag and drop area**. We don't allow this as a safety precaution. The content would not map from the flex column to the drag and drop area. To illustrate why this is, suppose you built your new template so you have a sidebar and a main content area. Your sidebar is a flexible column, your main content is a drag and drop area. The swapping tool would map the flexible column to the flexible column.
If you want to convert a page to using drag and drop areas the safest way to do it, is to use [content staging](/guides/cms/content/content-staging).

1.  [Open the content staging tool](https://app.hubspot.com/l/content/staging/domain), find the existing page, stage it. Choose "Stage blank page".
2.  Select your new template that uses the drag and drop area.
3.  Copy and paste the information from the original version of the page, creating the layout you want.
4.  When done publish to staging.
5.  In the content staging tool, navigate to the "Publish" tab. Select your page, and publish from staging.

## Global partials

[Global Partials](/guides/cms/content/global-content) are pieces of HTML & HubL content that can be reused across your entire website. The most common types of global partials are website headers, sidebars, and footers. For content creators global partials manifest themselves in the content editor as regions they can select to edit in the global content editor. The global content editor looks just like the page editor but is used to edit global content.

To illustrate, you might implement the header as a global partial with modules for the navigation and other content. To the content creator if they need to add a link to the navigation, they could click on the header in the page editor, then click on the menu module and update the menu.

Ensure that the users that _should_ have access to edit global content have the "Edit global content and themes" permission.
## Code Alerts

For accounts on CMS Hub Enterprise, [Code Alerts](/guides/cms/debugging/code-alerts) provides a centralized location for developers and IT managers to see an overview of issues that affect performance and rendering of pages and assets on the account.

Since you are actively optimizing the site to take advantage of the new features. It's a great idea to review the code alerts for your account and resolve any that exist.


# Convert a WordPress theme to a HubSpot CMS theme

If you're familiar with developing on WordPress and want to create websites on the HubSpot CMS, review the below guide for recreating WordPress themes as HubSpot CMS themes.

For a video walkthrough on developing on both WordPress and HubSpot, check out the YouTube series [Getting Started With HubSpot CMS Hub for WordPress Devs](https://www.youtube.com):
For support while developing on HubSpot, check out the [CMS developer community](https://community.hubspot.com/t5/CMS-Development/bd-p/designers_support) or join the [HubSpot developer Slack](/getting-started/slack/developer-slack). And if you need further assistance in recreating your website or migrating your content to HubSpot, you can [request help from a HubSpot partner](https://ecosystem.hubspot.com/marketplace/solutions).
This guide does not discuss recreating WordPress themes that rely on complex plugins. Depending on the plugins you're using, HubSpot may provide those functionalities by default. You may also be able to find apps and assets already developed on [HubSpot's marketplace](https://ecosystem.hubspot.com/marketplace/apps) to provide a similar functionality. Alternatively, you may need to develop [custom modules](/reference/cms/modules/files) or [apps](/guides/apps/public-apps/overview).
## Converting a theme

To get started, you'll need to build a theme in HubSpot, then rebuild your WordPress functionality with HubSpot elements:

- [Install the HubSpot CLI](/guides/cms/setup/getting-started-with-local-development) so that you can develop your theme locally.
- Download the HubSpot CMS theme boilerplate by following the steps in the [CMS theme boilerplate walkthrough](/guides/cms/content/themes/getting-started#start-a-boilerplate-theme-project). This walkthrough will teach you the basics of accessing your theme locally, making changes, and sending those changes to HubSpot. You can learn more about HubSpot themes by viewing the

  [themes overview](/guides/cms/content/themes/overview).

- Rebuild your WordPress theme in HubSpot using the reference sections below to learn about the differences between developing on WordPress and HubSpot.
- Once you've created your HubSpot theme, [import your content into HubSpot](https://knowledge.hubspot.com/blog/import-your-content-into-hubspot).

Below, learn more about the various differences between WordPress and HubSpot, including example code.

## Building blocks

Both WordPress and HubSpot use a similar framework for architecting themes, templates, and template partials. Below, learn about the differences between each platform's building blocks, along with example code. You can learn more about HubSpot's building blocks in the [CMS developer documentation](/guides/cms/content/overview).

### Languages

While HubSpot and WordPress use many of the same coding languages, the main difference is that WordPress sites are built primarily with PHP, while HubSpot themes rely heavily on HubSpot's templating language [HubL](/reference/cms/hubl/overview). In addition, in Wordpress you might use PHP tags and hooks, while in HubSpot you'll use HubL [filters](/reference/cms/hubl/filters), [functions](/reference/cms/hubl/functions), and [tags](/reference/cms/hubl/tags/standard-tags).

Below, learn more about some of the difference between WordPress and HubSpot languages.

| WordPress | HubSpot |
| --- | --- |
| Stylesheets and JavaScript files can be included in a theme by using `wp_enqueue_style()` and `wp_enqueue_script()`. | Stylesheets and JavaScript files can be included in a theme by using [require_css()](/reference/cms/hubl/functions#require-css) and [require_js()](/reference/cms/hubl/functions#require-js).In addition, you can [include CSS and JavaScript in individual modules](/reference/cms/modules/files) to keep the code exclusive to that module. |
| Theme styling can be coded using CSS, SCSS, SASS, LESS, and other supersets of CSS. | [CSS + HubL](/reference/cms/hubl/overview#variables-and-macros) can be used to enable variables and macros in your HubSpot theme CSS. |
| Returns relative file paths for a theme folder in WordPress with `get_template_directory_uri()` | Return the public URL of a specific template or coded file with [get_asset_url()](/reference/cms/hubl/functions#get-asset-url). |
| Create custom functionality with a `functions.php` file. | The HubSpot CMS doesn't have an equivalent file, as there is no concept of [hooks](https://developer.wordpress.org/plugins/hooks/). However, you can extend functionality with [HubL macros](/reference/cms/hubl/variables-macros-syntax#macros), [serverless functions](/guides/cms/content/data-driven-content/serverless-functions/overview), and [custom apps](/guides/apps/public-apps/overview). |

### Themes

In WordPress, themes often control how the content management system works. For example, a theme can define the custom post types available or which content fields are editable in page templates. In HubSpot, a theme is a collection of developer assets. One HubSpot website might use multiple themes, and updating a theme will only affect the pages that use that theme's templates. The templates and modules within a HubSpot theme are portable, meaning they can be used in multiple themes, moved between themes, or cloned to create a variation of the asset.

Both WordPress and HubSpot share the concept of a single file being used as the starting point for a theme when calling in [global partials](/guides/cms/content/templates/types/html-hubl-templates#global-partials). In WordPress, `index.php` is generally the main file that's used to call in partials such as `header.php` and `footer.php`. In HubSpot, the main file is called `base.html`.

Below, compare the standard Wordpress `index.php` file to HubSpot's `base.html` file.

#### WordPress index.php

```shell
<!DOCTYPE html>
<html>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>">
<title><?php wp_title( '|', true, 'right' ); ?></title>
<link rel="stylesheet" href="<?php echo esc_url( get_stylesheet_uri() ); ?>" type="text/css" />
<?php wp_head(); ?>
</head>
<body>
<h1><?php bloginfo( 'name' ); ?></h1>
<h2><?php bloginfo( 'description' ); ?></h2>

<?php if ( have_posts() ) : while ( have_posts() ) : the_post(); ?>

<h3><?php the_title(); ?></h3>

<?php the_content(); ?>
<?php wp_link_pages(); ?>
<?php edit_post_link(); ?>

<?php endwhile; ?>

<?php
if ( get_next_posts_link() ) {
next_posts_link();
}
?>
<?php
if ( get_previous_posts_link() ) {
previous_posts_link();
}
?>

<?php else: ?>

<p>No posts found. :(</p>

<?php endif; ?>
<?php wp_footer(); ?>
</body>
</html>
```

#### HubSpot base.html

```hubl
<!--
  templateType: none
-->
<!doctype html>
<html lang="{{ html_lang }}" {{ html_lang_dir }}>
  <head>
    <meta charset="utf-8">
    {% if page_meta.html_title || pageTitle %}<title>{{ page_meta.html_title || pageTitle }}</title>{% endif %}
    {% if site_settings.favicon_src %}<link rel="shortcut icon" href="{{ site_settings.favicon_src }}" />{% endif %}
    <meta name="description" content="{{ page_meta.meta_description }}">
    {{ require_css(get_asset_url("../../css/main.css")) }}
    {# This is intended to be used if a template requires template specific style sheets #}
    {% if template_css %}
      {{ require_css(get_asset_url(template_css)) }}
    {% endif %}
    {{ require_css(get_asset_url("../../css/theme-overrides.css")) }}
    {# To see a full list of what is included via standard_header_includes please reference this article: https://developers.hubspot.com/docs/cms/hubl/variables#required-page-template-variables #}
    {{ standard_header_includes }}
  </head>
  <body>

    {{ require_js(get_asset_url('../../js/main.js')) }}
    {# To see a full list of what is included via standard_footer_includes please reference this article: https://developers.hubspot.com/docs/cms/hubl/variables#required-page-template-variables #}
    {{ standard_footer_includes }}
  </body>
</html>
```
Note that HubSpot's base.html file includes the following variables:

- `{{ standard_header_includes }}`
- `{{ standard_footer_includes }}`

These variables are required for a standalone template, and are used to pull in information such as HubSpot's tracking code and any code included in your website settings header and footer HTML. [Learn more about these variables](/reference/cms/hubl/variables#required-page-template-variables).
### Templates

In both WordPress and HubSpot, a template is a subset of a theme which determines the page's layout.

In WordPress, the template that's used to display a page's content is programmatically determined by the [template hierarchy](https://developer.wordpress.org/themes/basics/template-hierarchy/) where template file names are named based on their usage. For example, a general post template would be named `single.php`. You could then create a new post type with a name that's relative to that template, such as `single-products.php`, where _products_ is the post type.

In HubSpot, the template selection process depends on the type of content you're creating:

- All posts on a blog will use a single blog template, which you can [select in your blog settings](https://knowledge.hubspot.com/blog/manage-your-blog-template-and-settings). You can use the same template to host your blog listing page, which provides a listing of your blog posts, or you can create a separate template for your blog listing page. Learn more about [HubSpot's blog template markup](/guides/cms/content/templates/types/blog).
- When [building website pages](https://knowledge.hubspot.com/website-and-landing-pages/create-and-customize-pages), content creators select which theme template they want to use before creating content. Templates can be swapped out at any time within the page editor.

Similar to WordPress, when creating a HubSpot template you need to include an annotation to [set which type of content the template will be available for](/guides/cms/content/templates/types/html-hubl-templates#template-annotations).

### Template partials

In both WordPress and HubSpot, template partials are individual templates that can be used as components in a theme. In HubSpot, you can create two types of [partials](/guides/cms/content/templates/types/html-hubl-templates):

- **Standard partials:** partials that can be edited at the page-level without affecting other instances of the partial. For example, you might build a standard partial when adding a form if you want the form to be different across landing pages.
- **Global partials:** partials that update across all pages and templates when their content is updated. For example, you might build a header or footer global partial so that they're consistent across all pages.

Both types of partials can include modules, custom HTML, and [drag and drop areas](/guides/cms/content/templates/drag-and-drop/overview).

HubSpot doesn't have specially recognized partials for headers and footers. Instead, you can create [global partials](/guides/cms/content/templates/types/html-hubl-templates#global-partials). When recreating your WordPress `header.php` and `footer.php` in HubSpot, it's recommended to view your header and footer in two ways:

- Locate your `header.php` and `footer.php` files in WordPress. You can usually find these by navigating to **Site** > **WordPress-includes** > **header.php** and **footer.php**. This may depend on your install, however.
- View the rendered header and footer by viewing the page's source code on a live page, then locating everything between `<head>` and `</head>` as well as `<footer>` and `</footer>`.

For recreating your header or footer navigation, learn more about how to [manage menus on the HubSpot CMS](/guides/cms/content/menus-and-navigation). You can also view the [HubSpot CMS boilerplate's menu module](https://github.com/HubSpot/cms-theme-boilerplate/tree/main/src/modules/menu.module) for reference.
- To include third-party tracking codes, it's recommended to re-export them from the vendor, rather than copying directly from WordPress.
- WordPress-specific code is not necessary to copy to HubSpot, including any script from _/WordPress-includes/_ or _/WordPress-content/_.
#### WordPress header.php

```html
<?php
/**
 * The template for displaying the header
 */
?><!DOCTYPE html>
<html>
<head>
	<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
  <header class="header">

  </header>

</header>
```

**WordPress footer.php**

```hubl
<?php
/**
 * The template for displaying the footer
 */
?>

  </div><!-- .site-content -->

  <footer class="site-footer">

  </footer><!-- .site-footer -->

<?php wp_footer(); ?>

</body>
</html>
```

**HubSpot footer**

```hubl
<!--
  templateType: global_partial
  label: Footer
-->
<footer class="footer">

</footer>
```

## Content types

WordPress and HubSpot have different ways of referring to content types. The WordPress content model centers around the post, with different types of content, such as pages and blog posts, categorized as [post types](https://wordpress.org/documentation/article/what-is-post-type/). WordPress also allows for custom post types.

HubSpot's content model has a similar hierarchy, but content types are self-described rather than categorized as post types. The [main content types](/guides/cms/overview) that you can build on HubSpot are website pages, landing pages, and blog posts. Outside of website development, you might also [build email templates using the CMS](/guides/cms/content/templates/types/email-template-markup). While HubSpot doesn't have custom content types, you can [create custom objects](/guides/api/crm/objects/custom-objects) to store data that doesn't fit into one of the standard content or object categories. Custom objects can be used both within the CRM and for [CMS website data](/guides/cms/content/data-driven-content/dynamic-pages/crm-objects).

Below, learn about how content types compare across each platform, along with code examples.

### Pages

In HubSpot, there are two types of pages:

- **Website pages:** the standard page type used for general pages across a HubSpot website.
- **Landing pages:** the page type best suited for marketing-driven pages, often containing forms. These pages focus on the customer conversion path as a part of the [inbound marketing methodology](https://www.hubspot.com/inbound-marketing).

The differences between the two are more conceptual than practical, and when building a page template, you'll refer to them both with the [template type](/guides/cms/content/templates/overview) of `page`. Below, view how WordPress's standard page markup compares with HubSpot's.

**WordPress page markup**

```hubl
<?php
/*
 * The main template file
 */
get_header(); ?>

  <main>
  <?php
  if ( have_posts() ) :

	while ( have_posts() ) : the_post();
      the_content();
	endwhile;

  else :
    echo '<p>No content was found :(</p>'
  endif;
  ?>
  </main>

<?php get_footer(); ?>
```

**HubSpot page markup**

```hubl
<!--
  templateType: page
  isAvailableForNewContent: true
  label: Page
-->
{% extends "./path-to/base.html" %}

{% block body %}
<main>
  {% widget_container "page_content" %}
  {% end_widget_container %}
</main>
{% endblock body %}
```
Note how HubSpot's page markup includes `{% extends "./path-to/base.html"%}`. This allows the page's `{% block body %}` block to populate the `base.html` file with its contents. It also means that you don't need to add the standard includes to the page, as `base.html` includes them. [Learn more about blocks and extends](/reference/cms/hubl/variables#required-page-template-variables).
### Posts

In WordPress, the posts post type refers to content typically used by blogs, and can be used to create RSS feeds.

In HubSpot, a post is an individual blog post created with the blog tool. Similar to WordPress, HubSpot blog posts are created using one template, which you can [select in your blog settings](https://knowledge.hubspot.com/blog/manage-your-blog-template-and-settings). Blog posts are displayed on a blog listing page, and can also be [accessed through an RSS feed](https://knowledge.hubspot.com/blog/generate-rss-feed-urls-for-blogs-authors-and-tags). Learn more about [HubSpot's blog template markup](/guides/cms/content/templates/types/blog).

In both WordPress and HubSpot, you can call in blog post content using loops. In WordPress, you call in blog content using [The Loop](https://codex.wordpress.org/The_Loop). For example:

```html
<?php
if ( have_posts() ) :
    while ( have_posts() ) : the_post();
        // Display post content
    endwhile;
endif;
?>
```

In HubSpot, you call in blog content using a [for loop](/reference/cms/hubl/loops). For example:

```hubl
{% for content in contents %}

{% endfor %}
```

You can either create one template for your posts and blog listing page, or create a separate listing page template. Learn more about [blog templates](https://knowledge.hubspot.com/blog/generate-rss-feed-urls-for-blogs-authors-and-tags).

## Content management

WordPress and HubSpot offer various features for managing website content, media, settings, and users. Below, learn about some of the content management differences between WordPress and HubSpot.

### Editors

In WordPress, content creators generally use the default Gutenberg editor or use drag and drop editor plugins such as Elementor and Themify.

In HubSpot, content creators use HubSpot's default drag and drop editor for creating many types of content, including [pages](https://knowledge.hubspot.com/website-pages/edit-page-content-in-a-drag-and-drop-area) and [emails](https://knowledge.hubspot.com/marketing-email/create-marketing-emails-in-the-drag-and-drop-email-editor). Developers can build [drag and drop areas](/guides/cms/content/templates/drag-and-drop/overview) into a template to allow drag and drop editing, along with [sections](/guides/cms/content/templates/drag-and-drop/sections) to contain those drag and drop areas.

In HubSpot, content creators use HubSpot's drag and drop editor for creating different types of content, including [pages](https://knowledge.hubspot.com/website-pages/edit-page-content-in-a-drag-and-drop-area) and [emails](https://knowledge.hubspot.com/marketing-email/create-marketing-emails-in-the-drag-and-drop-email-editor). You can add [drag and drop areas](/guides/cms/content/templates/drag-and-drop/overview) to a template to allow content creators to add [modules](/guides/cms/content/modules/overview) to a page.

### Databases and HubDB

While WordPress database management is often accomplished through plugins, HubSpot offers [HubDB](/guides/cms/storage/hubdb/overview). HubDB is a relational data store that represents data in a table, and can be managed both within HubSpot and through the [HubDB API.](/guides/api/cms/hubdb)

In WordPress, you can register custom post types to extend your content taxonomy. In HubSpot, the equivalent would be creating [custom objects](/guides/api/crm/objects/custom-objects), which allow you to store data as CRM records alongside other CRM objects, such as contacts and companies. Custom object data can be used not just by the [CMS for website data](/guides/cms/content/data-driven-content/dynamic-pages/crm-objects), but by other parts of the business too. You can access custom objects from within HubSpot and through the API.

### AJAX and serverless functions

In WordPress, you can create AJAX functions to extend functionality with APIs and custom code. In HubSpot, you can use [serverless functions](/guides/cms/content/data-driven-content/serverless-functions/overview) to write server-side code that interacts with APIs while keeping your credentials secret.

### Media and file storage

WordPress offers a media library for storing images, videos, and other types of files, which also allows for image editing.

Similarly, HubSpot offers the [files tool](/guides/cms/storage/file-manager), which can be used to [upload](https://knowledge.hubspot.com/files/upload-files-to-use-in-your-hubspot-content) and [organize files into folders](https://knowledge.hubspot.com/files/organize-edit-and-delete-files). You can access the files tool directly or from within content editors. In both locations, you can edit files as needed. The files tool also provides access to [royalty-free stock images](https://knowledge.hubspot.com/files/how-can-i-use-stock-images-in-my-content-with-the-shutterstock-integration) through Shutterstock.

### Settings

In Wordpress, settings are often modified by plugins, and can have site-wide impact. In HubSpot, you can [manage website settings](/guides/cms/content/website-settings) within your HubSpot account, and can be applied globally or to individual domains and blogs. This includes settings for website pages, blogs, navigation, domains and URLs, and themes.

### User management

In both WordPress and HubSpot, you can [set permissions](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide) per user to control which users can view, edit, and publish website content and more.

In WordPress, permissions can also be used to set content access for end-users. In HubSpot, you can use the [memberships feature](/guides/cms/content/memberships/overview) to provide content access to specific contacts, and you can also [password protect pages](https://knowledge.hubspot.com/website-pages/password-protect-a-page).


# How to upgrade to the latest version of jQuery
Historically HubSpot JavaScript libraries required jQuery to function. jQuery is a JavaScript library that makes it easier to write JavaScript compatible with many web browsers.

Modern JavaScript and web browsers have made writing JavaScript much easier. Most of the functionality of jQuery, has vanilla JavaScript equivalents. Those equivalents run faster, they don't require a large JavaScript library like jQuery. The result is they're also more secure.

Because going jQuery free is generally better for performance and security - **it is recommended to either not use jQuery, or use the latest version available.**

For older websites, it's not as simple. Removing jQuery can break existing old code on the website. If there's a lot of JavaScript on a website it may be hard to remove jQuery all together. In that situation it's often much easier to upgrade to the latest version of jQuery.

Upgrading to the latest version of jQuery makes your website more secure, and potentially a little faster in terms of script execution time and loading time. **If you are using a jQuery version provided through the HubSpot website page settings UI, we recommend upgrading jQuery or updating your site to no longer need jQuery.**

This guide will walk you through the steps to upgrade to a newer version of jQuery. You will open the developer tools in your web browser

## 1. Identify where jQuery is being used

Before we switch versions, it's important to be aware of what areas of the site currently use jQuery. Once we know that, we know what functionality to test to ensure the new version is working.

**Open the homepage of your website.**

Now in your address bar, add `?hsNoJQuery=true&hsDebug=true` to the end of the URL, and hit enter.

`https://www.my-website.com/?hsNoJQuery=true&hsDebug=true`

You added query parameters. This specific one tells HubSpot to disable jQuery when loading the page. This only affects what **you** see and doesn't break the site for other visitors.

Now right-click anywhere on the page and choose **"inspect"**. _The exact wording may differ slightly depending on which web browser you're using._ This is the developer tools for your web browser.
You should see a tab in the panel that opened, which says "console". Open that tab.

On pages that require jQuery you will see this error message:

`"Uncaught ReferenceError: $ is not defined"`

That error should also state a file name that's producing that error.
_In the example image above, the two files are vast-main.js and the module js file._

Copy and paste or enter that file name somewhere you can reference later. You do not need to worry about exact spelling. Taking a screenshot you can refer to later is an alternative. Repeat this for all instances of this error you see.

Note the page you saw those errors on.

Now we're going to do this same thing for a few other pages on your site. The best pages to do this on are ones with the most interactivity. Things like forms, dropdown menus, buttons that do something aside from bringing you to a new page. If you don't see this error on a page, great news, it doesn't use jQuery.

Once you've noted those file names somewhere let's move on to the main test.

## 2. Add new version of jQuery in a testing mode

In this step we're going to add code to test if migrating to the newest version of jQuery will break anything for your site.

We're going to open the HubSpot website settings. Copy and paste a snippet of code, and disable jQuery.

In a new tab, open HubSpot, navigate to Settings (gear icon). Then **Website > Pages**.

Scroll to down until you see **jQuery.**

First check that the **"Include jQuery"** checkbox is enabled.
If it is not, and you know jQuery is used, it was likely added by a developer. In that instance see [Advanced troubleshooting](#advanced-troubleshooting) . **If it's enabled however, proceed.**

**Check the jQuery version number** that's currently selected.

**Copy the code below and paste it into your footer HTML, do not save yet.**

```hubl
{# jQuery upgrade #}
{# These are the variables you update #}

{% set oldjQueryVersion = 1.7 %}
{% set oldjQueryLocation = "footer" %}

{# You don't need to edit any of the code below. #}
{# The if statement below tests for a query parameter being added to the URL "?latest_jquery=true" #}
{% if request.query_dict.latest_jquery %}
{# embed the latest jQuery version #}
  {{ require_js("https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js",oldjQueryLocation) }}
  {{ require_js("https://cdnjs.cloudflare.com/ajax/libs/jquery-migrate/3.3.2/jquery-migrate.min.js",oldjQueryLocation) }}

{% else %}
  {# when the latest_jquery parameter is not found, display the old version of jQuery #}
  {% if oldjQueryVersion == 1.7 %}
      {{ require_js("https://static.hsappstatic.net/jquery-libs/static-1.1/jquery/jquery-1.7.1.js",oldjQueryLocation) }}
  {% elif oldjQueryVersion == 1.11 %}
      {{ require_js("https://static.hsappstatic.net/jquery-libs/static-1.4/jquery/jquery-1.11.2.js",oldjQueryLocation) }}
  {% endif %}
{% endif %}
```

**Scroll back down** until you see the settings for jQuery.

**Check the jQuery version number** that's currently selected.

If you have version 1.7 skip to the next step. If you have version 1.1 Where you see `{% set oldjQueryVersion = 1.7 %}` change the number 1.7 to 1.11. This ensures the correct version of jQuery will load on your site during testing.

Now check if the checkbox for **"Load HubSpot's included jQuery library from your footer"** is enabled.

**If it's disabled,** where you see`{% set oldjQueryLocation = "footer" %}`, change the **"footer"** text to say **"head"**.

**Find the jQuery checkbox and disable it.**

**Save the settings.**

## 3. Test areas of your site that you know use jQuery for issues

In the tab with your website, click into the address bar again, add `?latest_jquery=true` to the end of the URL, hit enter.

You are viewing what the page looks and functions like with the latest version of jQuery.

If you closed the developer tools before, reopen them by right clicking and inspecting anywhere on the page.

Look in the console for those `Uncaught ReferenceError: $ is not defined` errors. **If you don't see one, congratulations you followed step 2 correctly.** If you see any errors or warnings that were not there prior see [Advanced troubleshooting](#advanced-troubleshooting).

Now review your list of pages and files you logged earlier. Go to the pages that had those errors and add the query parameter `?latest_jquery=true` to the URL and hit enter.

If everything went smoothly you shouldn't see any new errors and you shouldn't see any `Uncaught ReferenceError: $ is not defined` errors.

Click around the pages on anything that's intended to be interactive on the page, dropdowns, forms, search fields etc. If everything works as expected you should be able to transition safely.

### Optional performance opportunity

Loading jQuery from the footer is better for performance than loading in the `head`. If you previously needed to set `oldjQueryLocation` to `"head"` you can improve page load times by making an update to the code. If you did not need to do that, skip to step 4, you're already loading from the footer.

Go back to your site settings where your the code is. Immediately below `{# embed the latest jQuery version #}` there are two `require_js` functions. Remove the `,oldjQueryLocation` from those two functions. Do not remove it from the `require_js` functions within the `else` statement -- that code is being used on your site while you are testing.

```hubl
{# embed the latest jQuery version #}
{{ require_js("https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js") }}
{{ require_js("https://cdnjs.cloudflare.com/ajax/libs/jquery-migrate/3.3.2/jquery-migrate.min.js") }}
```

Now repeat the same testing you did with the `?latest_jquery=true` parameter. Again check all of the same pages, looking for `Uncaught ReferenceError: $ is not defined.`

What you're testing for this time is whether moving this script to your footer will break any scripts on your site.

If you test all of the pages with the query parameter and **don't find any of those errors** then congratulations, you will be able to load jQuery in the footer. Carry on to the next step.

If you did find those errors, you have two options.

- Continue loading jQuery from the head, this will result in slower web page performance but you'll be done upgrading to the latest jQuery.
- Go through the modules and templates in your site to identify where there is JavaScript that is not waiting for jQuery to be loaded before executing. Fix those issues. If you are not a developer you'll want to find a developer to assist.

## 4. Go live with the updated version of jQuery

Now that you know upgrading will go smoothly, go back to your website page settings tab.

Remove the code you added previously to your footer HTML and replace it with the code below. Do not save yet.

```hubl
{# jQuery #}
{% set newLocation = "footer" %}
{{ require_js("https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js", newLocation) }}
{{ require_js("https://cdnjs.cloudflare.com/ajax/libs/jquery-migrate/3.3.2/jquery-migrate.min.js", newLocation) }}
```

This code will place the code in your site footer. If your old jQuery code was in the footer, or you were successful in the optional performance opportunity step above **You are ready to save the settings.**

**If you were unsuccessful in moving jQuery to the footer** in the optional performance opportunity step, change `newLocation = "footer"` to `newLocation = "head"`

**You are now ready to save the settings.**

Congratulations you've successfully upgraded to the latest version of jQuery.

## Advanced troubleshooting

If you're not a developer, and you've followed the other steps and have a problem, it is recommended you find a developer to assist. If you do not have a go-to developer, we recommend you reach out to one of our [Solutions Partners](https://ecosystem.hubspot.com/marketplace/solutions/website-development).

For developers we recommend reviewing the official [jQuery upgrade guide](https://jquery.com/upgrade-guide/3.0/#jquery-migrate-plugin). You may need to update old JavaScript to no longer use deprecated functionality. Use the migrate plugin in development mode to identify the issues and fix them one by one.

If you hit a snag, it may be helpful to ask for help in the [official jQuery forums](https://forum.jquery.com/).


# Listing page requirements

In addition to complying with the [Theme Provider Branding Guidelines](https://www.hubspot.com/partners/themes/branding-guidelines), template providers must adhere to the following requirements set by HubSpot when submitting templates and their listing pages to the HubSpot Template Marketplace. These standards apply to all templates on the Marketplace.

HubSpot reserves the right to change the information on your template listing submission to comply with the following requirements. Learn more about the [best practices](https://developers.hubspot.com/blog/best-practices-for-listing-your-theme-or-module-on-hubspots-asset-marketplace) for listing a theme or module on the HubSpot Template Marketplace.

## List a template on the HubSpot Template Marketplace

- In your Template Marketplace provider account, navigate to **Template Marketplace** \> **Listings**.
- In the upper right, click **Create listing**.
- Select either **Create theme listing** or **Create module listing**.
- Review the information on the _Provider Information_ page, then click **Next**.
- Review your payment information on the _Payment Information_ page, then click **Next**.
- Select the **theme or module** you want to list, then click **Save and continue**. You'll be directed to the listing editor.
### Listing Info

Provide the following information:

- **Listing name:** the name that will appear on your template listing. Use a unique and descriptive name for your template. Please avoid the following:
  - The word _theme_, _module_, or _section_ to identify the template type. Listings will already be categorized by type. (e.g. _Example Theme_ or _Easy Accordion Module_).
  - Your company name (e.g. _Growth by HubSpot_ or _Slider | HubSpot_).
  - Version numbers (e.g. _Growth - 1.21_ or _Growth v2_) as the HubSpot Template Marketplace uses its own versioning system for published templates.
  - The word _Hub_ (e.g. _Hub Growth_).
  - Using the term _Pro_ when naming a free template. The marketplace can contain both free and paid versions of the same template, and naming a free template _Pro_ can create confusion. The terms _Pro_, _Plus_, _Advanced_, and other similar terms should only be used for the paid version of templates that have both free and paid versions.
- **Price:** select **Free** or **Paid**. If you’re listing a paid template, enter a **price**. After publishing, you cannot change between _Free_ and _Paid_ types.
- **Live example link:** a link to a live version of your template so users can test it. Do not link to an image.
- **Thumbnail image:** the image that appears in search results and on the storefront card.

  - This image must showcase your theme or module as the primary focus of the image. At least 75% of the image's content should be your template.
  - Images will be removed at the HubSpot Ecosystem Team's discretion if they violate the guidelines below. Images <u>must not</u> contain any of the following:

    - Excessive marketing verbiage, slogans, text, or branding (e.g. _Over 100 features plus support!!!!_ or superimposing text/branding over the image).
    - Imagery of individuals (real or fictitious) posing, pointing, o holding items (e.g. an image of a person holding a computer with your template on the screen).
    - Imagery that shows personal or private information.
    - Badges or banners containing information related to reviews and installs of your template, or website performance grading. These types of data can fluctuate and have the potential to mislead others. If you’re a HubSpot Partner and would like to display your partner tier or badge, please ensure it’s the most current version.

**Examples of good theme thumbnail images:**
**Examples of good module thumbnail images:**
### Theme/Module Details

Provide the following information:

- **Demo video (optional):** the demo video should showcase your template in action and convey proper use cases. While optional, a video is recommended as buyers find them valuable when evaluating a purchase.
  - Videos must be in mp4 format.
  - It is recommended to use a 4:3 or 16:9 ratio for optimal viewing.
- **Theme description/module overview:** a summary of your template's key selling points. Descriptions should include the following:

  - A summary
  - Key selling points
  - How your template can help buyers achieve their goals
  - For theme descriptions, you do not need to include a listing of the modules or templates included in the theme as these are already listed on the template listing page.
  - For individual modules, it must be noted in the description if the module requires the use of a 3rd party account. For example, if your module makes use of the Google Maps Platform, include the note: _The use of this module requires a Google Cloud (Google Maps Platform) account_.

- **Features:** feature titles are displayed in the Template Marketplace list view and are one of the first things your buyers see.

  - You must add a minimum of two and up to five features for a template.
  - Use the features section to highlight the following:
    - Key features of the template
    - Use cases and how it can solve your buyers' problems
    - You can also add marketing-based imagery to convey feature sets.

- **Client Examples (Themes only):** examples (i.e. functional websites of your clients) are important for customers to see how other companies are using your template. It allows potential buyers to get an idea of how to use your template and also see how customizable it can be. It's strongly recommended to ensure that you have written permission from your clients to link to their websites. If requested, please make your client’s written permission available for verification.
- **Module Images (Modules only):** up to 20 images of your module to demonstrate how it integrates into a template. Modules must contain, at minimum, the following screenshots:
  - How the module will appear on the page to front-end users.
  - Screenshots of both content and style settings.
- The page-level editor settings of the module. If [repeater content](/guides/cms/content/fields/overview#repeaters) is used in the module, you must provide a screenshot of one instance of the repeating item settings.
### Category

Provide the following information:

- **Category and attributes:** add categories and attributes for your themes or modules.
  - **Themes:** your business types, pages types, and page features must be relevant to the theme you are submitting.
  - **Modules:** your template types and categories must be relevant to your module.
- **Search tags:** add up to 15 keywords for your top features and style attributes.
  - A set of default tags are assigned to your theme. If a particular tag isn't available, you can add a new tag.
  - Tags must be between 2–3 words long.
  - Use tags that accurately show the theme or module listed.
  - You may not use tags that contain the following:
    - Company names such as _HubSpot_ or _Example Company_.
    - Subjective terms such as _top-rated_, _best_, or _number one_.
    - Tags that describe the template type such as _theme_, _module_, or _email._ These are implied based on the template type being viewed.
    - Profanity or derogatory language.
  - If you have more than two business types for your theme, you can add the extra types as tags in your listing.

### Support

Provide the following information:

- **Free support:** select whether you offer free support to buyers of your template, and what languages you support.
- **Support channels:** all support email addresses must be working email addresses. If you offer Live Chat, Facebook Messenger or Phone support, make sure these are in working order.
  - Avoid using _hubspot@\[companyname\].com_ as your support email address.
- **Self-support support resources:** the setup documentation URL must link to a live website page with documentation on how to set up and use your template. You cannot use a contact page or a homepage. You can have downloadable documentation as a supplemental option, but live documentation must be present.
- **Terms of Service and Privacy Policy:** all URLs entered must be linked to your website and refer to your company's terms and privacy policies. Do not use HubSpot's terms and privacy links.

### Review

On the _Review_ tab, validate your template's code. To preview how your template will look like on the Marketplace, click **Preview listing**.

## General imagery requirements

Images and screenshots will be removed at HubSpot’s discretion if they violate the guidelines above. Templates must <u>not</u> include images from the [HubSpot-Shutterstock integration](https://knowledge.hubspot.com/files/how-can-i-use-stock-images-in-my-content-with-the-shutterstock-integration).

Use full-size images of your template to showcase appearance and functionality. The following image dimensions are strongly recommended for the best viewing experience of your template listing:

- **Storefront Card thumbnail image:**
  - **Size:** minimum 480px x 360px (4:3 ratio)
  - **Formats:** JPG, JPEG, PNG
- **Theme or Module images:** you must have a minimum of two and up to 20 full-size images of your template.

  - **Minimum width:** 1160px
  - **Formats:** JPG, JPEG, PNG

- **Feature images:**
  - **Size:** minimum 480px × 360px (4:3 ratio)
  - **Formats:** JPG, JPEG, PNG
- **Client examples**
  - **Size:** minimum 480px x 360px (4:3 ratio)
  - **Formats:** JPG, JPEG, PNG

## Other requirements

All verbiage, images, media, and default content used in the template must match what is advertised on the template listing page and demo site. For example, if your template listing is geared towards real estate with a working demo, the end user must receive that theme.

If upon purchase the end user is delivered a theme centered around fitness instead, this would be considered a bait-and-switch, which is unacceptable.


# List and update templates in the HubSpot Template Marketplace

Once you’ve created a [Template Marketplace provider account](https://app.hubspot.com/signup-hubspot/asset-provider), you can use it to create template listings and submit them for approval to be listed on the [HubSpot Template Marketplace](https://ecosystem.hubspot.com/marketplace/templates).
You can only have **one** new submission of each type (module or template) in review at a time. A submission is considered “new” if it’s not currently published or approved.
Before listing your template on the Template Marketplace, fill out your Provider profile:

- In your Template Marketplace provider account, navigate to **Template Marketplace** \> **Provider profile**.
- In the _Company Info_ section, fill out all the fields. These fields will appear on your template listings. You will not be able to edit your company name once it's been chosen and will need to contact HubSpot Support to change it.
- In the _Contact Info_ section, fill out all the fields for both the _Main contact_ and _Developer contact_ sections. This information will only be used if HubSpot needs to contact you about a listing and <u>will not</u> appear on any listing pages.
- In the _Payment Info_ section, add your supported payment methods. You can connect either PayPal or Stripe as a payment gateway. If your payment information causes the payment gateway to stop working, your templates will be temporarily delisted to avoid a negative customer impact.

## Create a template listing

To create and submit a theme or module listing:

- In your Template Marketplace provider account, navigate to **Template Marketplace**.
- In the upper right, click **Create listing**.
- Select either **Create theme listing** or **Create module listing**.
- Review the information on the _Provider Information_ page, then click **Next**.
- Review your payment information on the _Payment Information_ page, then click **Next**.
- Select the **theme or module** you want to list, then click **Save and continue**. You'll then be brought to the listing editor
The listing editor is broken up into five tabs: _Listing Info_, _Theme/Module Details_, _Category_, _Support_, and _Review_. All information is required unless otherwise indicated. Review all listing page requirements [here](https://developers.hubspot.com/docs/guides/cms/marketplace/general-requirements).

- On the _Review_ tab, validate your template’s code and preview your listing. If everything looks good, click **Submit for review.**
Do not change the template folder name once submitted. The template listing is directly linked to this folder upon submission. You will not be able to resubmit or update the template after changing the folder name.
## Update a template listing

After creating a template listing, you can update it in your Template Marketplace provider account. If you've made changes to a template, you'll need to revalidate it before those changes are pushed out. If you do not revalidate, only the content on the Marketplace listing page will be updated.

To update a template listing:

1.  In your Template Marketplace provider account, navigate to **Template Marketplace** \> **Listings**.
2.  Click the **name** of the listing you want to update. You'll be redirected to the listing editor.
3.  Make any needed changes to the listing information, template details, categories, and support options, then click the **Review** tab to review your changes.
4.  On the _Review_ tab, check whether your template's code has changed since its last validation:

    - If you've made no changes to the template, HubSpot will display a confirmation message in the _Theme source code_ tile or _Module source code_ tile showing that your validation is up to date.

    

    - If you've made changes to the template, HubSpot will display an alert in the source code tiles. To send these changes to the Marketplace, you'll first need to click **Run validation**.

    

    **5.** On the upper right, click **Submit for review**.


# HubSpot Template Marketplace module requirements

Learn about the requirements to submit a module to the Template Marketplace. These requirements apply to both modules in a theme and independent modules.

## Module restrictions

Modules must not contain [HubDB](/guides/cms/storage/hubdb/overview), calls to [serverless functions](/guides/cms/content/data-driven-content/serverless-functions/overview), or the [CRM object field](/reference/cms/fields/module-theme-fields#crm-object).

The following module types should not be built as independent modules

- HTML
- Full-width modules
- Forms and multi-step forms
- Spacer modules or modules that create non-UI page structure
- Modules that duplicate default module functionality
- Commerce-specific modules
- Email-specific modules

## Module content

Learn about the requirements for module labels and help text, fields, and default content.

### Module labels & help text

- Modules must have descriptive labels that convey the purpose of the module. The label _Hero Banner with Parallax Scrolling_ is descriptive, whereas the labels _Hero Banner_ and _Gallery_ are not.
- Module labels must not contain numbers, such as _Hero Banner 01_.
- Module labels must not contain underscores.
- Module labels must not contain abbreviations, such as _Col_ instead of _Column_.
- Modules must contain [inline help text](/reference/cms/modules/configuration#meta-json) where applicable to further convey how to use the module.
- Modules should not be named the same as a [default module](/reference/cms/modules/default-modules).
- For independent modules, the module label should match the name on the template listing. For example, if your template listing is _SuperAwesome Banner with Scrolling_, your module label should be the same.
### Default content

- Default field cannot include _Lorem ipsum_ text.
- Default field content should represent the field’s purpose:
  - When including menu fields, modules must use _Select a menu_ as the default content option.
  - When including form fields, modules must use _Select a form_ as the default content option.
  - When including blog selector fields, modules must use _Select a blog_ as the default content option.
- If adding default content to a module doesn't make sense, use a [module placeholder](/reference/cms/hubl/tags/standard-tags#editor-placeholders) instead to help the content creator visualize the space that they'll fill with content.

### Module icons

Modules must include a custom icon assigned to the module (replacing the default icon). Do not use company logos as icons, such as Apple or Amazon logos. For modules included in a theme, each module should have a unique and relevant icon.

Learn more about [module icons](/reference/cms/modules/configuration#adding-an-icon).

### Modules that require 3rd party accounts

For individual modules, if the module requires a 3rd party account, it must be noted in the template description. For example, if your module makes use of the Google Maps Platform, you need to include a note, _"The use of this module requires a Google Cloud (Google Maps Platform) account."_

## Module fields

Review specific requirements for modules in a theme and independent modules below:

- For modules in a theme:
  - Must contain inline help text and specific default content for certain fields.
  - A part of the theme's [color](/reference/cms/fields/module-theme-fields#color) and [logo](/reference/cms/fields/module-theme-fields#logo) must inherit from the account's [brand settings](/guides/cms/content/fields/brand-and-settings-inheritance).
    - At a minimum, three color fields must inherit colors from the account's brand settings. Extra color fields can default to other colors, including black and white.
    - At least one logo field must inherit from the account's brand settings. If using an image field to render a logo, the image field does not have to inherit from the brand settings.
- For both modules in a theme and independent modules:
  - Module field names should describe the field’s intent. For example, if a text field is meant to include a person’s job title, _Job Title_ would be a proper description whereas _Title_ would not.
  - All of HubSpot's default modules must be styled and must display properly on all templates submitted.

### fields.json and module.html configuration

To ensure compatible functionality between themes and independent modules, modules must [inherit](/guides/cms/content/fields/overview#inherited-fields) the color and font fields either by defining `default_value_path` or `property_value_paths`, or both in their `fields.json` file and add a reference to the theme fields in the `module.html` file. [Learn more about these requirements.](/guides/cms/marketplace/template-policies#module-compatibility-with-themes)

## Module code quality

### Modules must be self-contained

#### Theme modules

Any files needed for your theme module, such as CSS or JavaScript, must be contained in the theme folder and included in the theme directory. You can use the Linked Files feature in the Design Manager. Or, include the files using the [require_js()](/reference/cms/hubl/functions#require-js) or [require_css()](/reference/cms/hubl/functions#require-css) functions with a relative path to the file.

For common libraries, such as slick.js, you can include them using the `require_js()` or `require_css()` functions with an absolute URL to the CDN where it's hosted.
Do not use absolute URLs to assets contained within your development portal as cross-portal references will not resolve.
#### Independent modules

For independent modules, all CSS and Javascript files should be contained in either the `module.css` or `module.js`. Alternatively, include the files using the `require_js()` or `require_css()` functions with an absolute URL to the CDN where it’s hosted. It is not possible to use the Linked Files feature in the Design Manager as that is only available for theme modules.

Since `module.js` is included in the DOM before any `require_js` or `require_css` files, Javascript contained in the `module.js` section should be deferred using the annotation below:

```js
document.addEventListener('DOMContentLoaded', function () {
  // Put Javascript here
});
```

All scripts and files should be rendered in the head of the module's [HTML](/reference/cms/hubl/functions#require-js).

### Code restrictions for independent modules

The following restrictions apply to only independent modules:

- It is recommended to use [vanilla JS](http://vanilla-js.com/) where possible. Adding a jQuery library to a site that is not using jQuery can potentially cause conflicts and slow down the website page.
- If using a jQuery library, use the [require_js()](/reference/cms/hubl/functions#require-js) function to include the library in the event that jQuery is turned off with the checkbox (Boolean) in account settings to avoid conflicts from multiple jQuery libraries.

```js
{% if not site_settings.include_jquery %}
{{ require_js("https://code.jquery.com/jquery-3.7.0.min.js", "footer") }}
{% endif %}
```

### Categories

- All independent modules must have at least one category. Modules submitted as part of a theme are not required to have categories, but it's best practice to include at least one. Learn more about [adding categories to modules](/reference/cms/modules/configuration).

### Class name selectors

- Any class name selectors must be prefixed with the module name, replacing spaces with hyphens. For example, below is the `module.html` file for a button named `example-button`, with each class name and CSS selector reflecting its name.

```hubl
<style>
{% scope_css %}
{# Button wrapper #}
 {% if module.styles.group_alignment.alignment %}
  .example-button-wrapper {
   text-align: {{ module.styles.group_alignment.alignment.horizontal_align }};
   }
 {% endif %}

{# Button #}

.example-button {
 {% if module.styles.group_background.color.color %}
  background-color: rgba({{ module.styles.group_background.color.color|convert_rgb }}, {{ module.styles.group_background.color.opacity / 100 }});
 {% endif %}
 }
 {% end_scope_css %}
</style>
{% end_require_css %}

{##### Module HTML #####}
```

### Styles and Javascript

- Styles:
  - Modules must have a non-empty style group.
  - Hardcoding inline styles within modules is not recommended. Instead, use dynamic inline styles by enabling fields to control styling.
- JavaScript:
  - JavaScript must be able to represent multiple instances of a module. JavaScript in the JS Pane will only load once per page, regardless of the number of module occurrences.
  - JavaScript should reference DOM elements by module-specific class names to ensure elements outside of the module are not unintentionally affected.

When creating modules, you can use a built-in variable called `{{name}}`. This variable pulls in the module's instance ID (which can be used in the HTML+HubL panel only) to help in CSS and JS markup for complex modules. [Learn more about this in our developer documentation.](/reference/cms/modules/files#require-css-block)

## Field organization

The following field organization and grouping requirements must be met.

### Content tab

- Where there is at least one control within a field group, all controls must be grouped into categories labeled by their function.
- Module fields added to the _Content_ tab must give ways to customize the content of a module. For example, controls for image, icon, alt text, and link controls.

### Styles tab

Module style field groups must be consistent and follow a pattern. Below is a recommended order for your style field groups. These groups can either be at the top level or [nested one group deep](#multi-level-grouping). Empty groups may also be removed:

- [Presets](#presets)
- Text
- Background
- Border
- Hover
- Corner
- Spacing
- Alignment
- Custom style groups that don't fit the above
- Advanced

The following field types must be contained in the _Styles_ tab if present:

- [Alignment](/reference/cms/fields/module-theme-fields#alignment)
- [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)
When moving fields from the _Content_ tab to the _Styles_ tab, learn how to [use alias mapping](/guides/cms/content/fields/alias-mapping) to preserve styling for modules that are already in use on live pages.
- Animation options should always be positioned near the bottom of the field group list.
- Options that allow content creators to add code snippets or CSS should be grouped at the end of the field group list under a field labeled _Advanced_.
- Controls should be standardized across all modules. For example, all elements that can have a border radius control should offer that control. Avoid offering controls on some modules that are absent on others.

- Module fields added to the _Style_ tab must provide ways to style the module. For example:
  - Style options such as color, text styling, alignment, spacing, border, and corner radius.
  - Animations such as hover and slide-in effects.
  - Presets such as dark and light themes that are meant to change many styles at the same time.

### Examples of field organization

#### Presets

Presets can be used when wanting to give content creators a limited set of options, often tying back to theme settings. For example, the _Icon_ module included in the Growth theme contains presents for _Dark_ and _Light_ colors, which enables consistency when used across the website.

#### Multi-level grouping

When deciding whether to keep style fields at the top level or nest them, consider the following example.
#### Grouping individual fields

The button module below contains groupings for _Presets_, _Text_, _Background_, and more. Although the _Corner_ field group contains only the corner radius control, it’s still grouped to create a uniform content creation experience.


# HubSpot Template Marketplace Guidelines

The [HubSpot Template Marketplace](https://ecosystem.hubspot.com/marketplace/templates) helps thousands of customers without direct access to a developer grow their online presence. It is powered by developers who create easy to use templates for customers.

You can sell two types of templates on the HubSpot Template Marketplace: [themes](https://ecosystem.hubspot.com/marketplace/website) and [modules](https://ecosystem.hubspot.com/marketplace/modules).

## Why should I sell templates on the marketplace?

The HubSpot Marketplace has a lot to offer developers or agencies who are looking to create a new revenue stream.

- **Non-exclusive:** You’ve worked hard at creating your templates. You shouldn’t be tied down to only having your template on our marketplace and nowhere else. You’re free to use your templates outside of the marketplace for your own projects as well.
- **Build once, sell multiple times:** Once your template is listed on the marketplace, it becomes a 1:many opportunity. You simply build your template once and it can be resold to multiple customers.
- **Grow your business:** Many of the developers who sell their templates (both free and paid) on our marketplace have seen their businesses grow both from marketplace revenue and from additional services requested by buyers.
- **Join a community of creators and showcase your skills:** Connect with a growing and supportive ecosystem to help shape the future of the HubSpot Template Marketplace while bringing more visibility to your offerings and services.

## Overview and requirements

To get started selling on the Template Marketplace, [create a Template Marketplace provider account](https://app.hubspot.com/signup-hubspot/asset-provider).

Any template submission must adhere to the compliance, design, and code quality requirements. Each template type also has its own requirements that must be met in order to be listed in the marketplace. You can view these requirements below:
Before you begin creating templates for the marketplace, HubSpot strongly recommends reviewing the [Building for the HubSpot Template Marketplace](https://www.hubspot.com/template-providers), [Branding](https://www.hubspot.com/providers/template/branding-guidelines), [Trademark Usage](https://legal.hubspot.com/tm-usage-guidelines), and [Content Usage](https://www.hubspot.com/content-usage-guidelines) guidelines. Templates that violate these guidelines will immediately be removed from the marketplace as described in HubSpot’s additional terms.
<ContentCardsRow>
<ContentCard>

### Theme Requirements

A portable and contained set of templates, modules, global content, and style settings designed to work together to enable a marketer-friendly content editing experience.

[View theme requirements](/guides/cms/marketplace/theme-requirements)

</ContentCard>
<ContentCard>

### Module requirements

Submitted themes will contain modules, which are reusable components that can be used in templates or added to pages through drag and drop areas and flexible columns. You can also submit standalone modules. These are modules that can be added to any theme in HubSpot's content editor.

[View module requirements](/guides/cms/marketplace/module-requirements)

</ContentCard>
<ContentCard>

### Listing Page Requirements

Requirements one must adhere to when submitting templates and their listing pages to the HubSpot Template Marketplace. These standards apply to all templates on the Marketplace.

[View listing page requirements](/guides/cms/marketplace/general-requirements)

</ContentCard>
<ContentCard>

### Template Marketplace Policies

Requirements and compliance details for listing templates in the Template Marketplace.

[View Template Marketplace policies](/guides/cms/marketplace/template-policies)

</ContentCard>

</ContentCardsRow>

## List and update themes on the Template Marketplace

Once you've created your template, submit it for approval to be listed on the HubSpot Template Marketplace. Once submitted, templates will undergo a review and approval process by the HubSpot Template Marketplace team. [Learn how to list and update your templates in the Template Marketplace](/guides/cms/marketplace/listing-templates).

## Supported payment gateways

HubSpot currently offers the ability for marketplace providers to connect the following payment gateways to receive payments:

- PayPal ([supported countries](https://www.paypal.com/us/webapps/mpp/country-worldwide) | [cross border payments](https://www.paypal.com/us/cshelp/article/what-are-the-cross-border-fees-when-selling-internationally-help550))
- Stripe ([supported countries](https://stripe.com/global) | [cross border payments](https://docs.stripe.com/connect/cross-border-payouts))

Payments from customers are received through the payment connected in your Marketplace Provider account:

- In your HubSpot account, navigate to **Marketplace** \> **Provider Info**.
- In the _Payment Method_ section, view or update your payment information.

HubSpot is not responsible for any payment gateway issues that may occur during the purchase process. HubSpot does not take a cut of any transactions, and all transactions are handled independently of the HubSpot platform. If your payment method stops working for any reason, your assets may be delisted temporarily to avoid a negative customer experience.
HubSpot has customers worldwide. When setting up your payment gateways, we strongly recommend accepting payments from international currencies (cross border payments).

HubSpot strongly encourages Template Marketplace Providers to use the Stripe payment gateway if it is available in their country. If Stripe is not [available in your country](https://stripe.com/global), then we recommend using the PayPal payment gateway.
## Refund requests

You may run into the occasional request from a purchaser for a refund. HubSpot recommends clearly stating your refund policy in a listing's description and providing a link to documentation about your refund policy. HubSpot encourages providers to honor valid refund requests. Learn more about general [guidelines for refund requests](https://knowledge.hubspot.com/resources/marketplace-refund-guidelines).

## Manage your transactions

A deal record is created in your Template Marketplace provider account for every template that a customer downloads. All deals will populate in a deal pipeline called _Marketplace Transactions_ with the _Transactions_ deal stage. When a refund is issued for a transaction, the _Refunded at_ property will be set on the deal record.

Providers are able to [customize the deal pipeline and deal stages,](https://knowledge.hubspot.com/object-settings/set-up-and-customize-pipelines) and [use the deal properties in workflows](https://knowledge.hubspot.com/workflows/choose-your-workflow-actions#property-management)[.](https://knowledge.hubspot.com/object-settings/set-up-and-customize-pipelines)

## Removal of template listings

HubSpot reserves the right to remove Marketplace listings for reasons including, but not limited to:

- Bugs present in the template.
- Third-party templates in the submission break.
- The template is incompatible with [HubSpot-supported browsers.](https://knowledge.hubspot.com/account-security/browsers-supported-by-hubspot)
- Use of copyrighted material such as images, icons, fonts, audio, and video that are not licensed for distribution.
- Templates that track users without their informed, explicit opt-in consent.
- Templates (including linking to content from the Template Marketplace or from your template demo) that include:
  - Promoting or engaging in illegal activities.
  - Derogatory, disparaging, and/or malicious content.
  - Dishonest or false claims.
  - Offensive content about race, religion, age, gender, or sexual orientation.
- Modules will be removed if the module affects other elements on the page/email that it is added to.

## Frequently Asked Questions
No, the most recent European Union P2B regulation that took effect on July 12, 2020, does not apply to the HubSpot App and Template Marketplaces. [Learn More](https://wayback.archive-it.org/12090/*/https://ec.europa.eu/digital-single-market/en/business-business-trading-practices).
A [Marketplace provider account](https://www.hubspot.com/template-providers) is needed to submit to the Template Marketplace.

Some partners manage their Marketplace listings in their Partner account to use the templates in the same account. Other partners manage their listings in a separate Marketplace account to keep the templates separate from other templates in their design manager tool.
Products submitted to or published in the Marketplace live in the HubSpot Marketplace system. This ensures that providers can continue to edit them in their own account without the changes affecting customers’ purchases. To update a Marketplace listing, a provider must resubmit the listing so it can be reviewed and republished.

Customers who have purchased previous versions of a product in a listing can [update to the latest version](https://knowledge.hubspot.com/resources/marketplace-customers-frequently-asked-questions#update) free of charge.
Once you have listed a template in the marketplace, you will need to contact [HubSpot Support](https://help.hubspot.com/)  to change your company name.
Possible reasons you may not see a product when trying to create a listing include:

- The template is currently listed in the Marketplace or contains elements from a currently listed template.
- The template isn't supported in the Marketplace (transactional emails, ticket emails, etc.)
- The template is a clone of a template from the Marketplace.
When creating preview URLs you must use your own domain name if available. If you don't have a domain available to use, HubSpot provides each account with a system domain in the following URL structure: `[AccountID].hs-sites.com`

**Please note**: HubSpot does not support template previews that use the `preview.hs-sites.com` subdomain.
Yes, a single template can be used in multiple themes. A common example of when a template may be used in multiple themes is if a provider offers a Starter and Premium version of the same theme.
Yes. All changes to a theme or module must be re-submitted to the Marketplace for pre-validation and approval by the quality team. Once the changes are approved, the listing and template will be updated in the Marketplace for customers.
To adjust the price of a submitted product, you must resubmit it to the Marketplace with updated information. Prices cannot be changed from free to paid, or vice versa once a template is published.
Payments are received through the payment method that you've connected in your Marketplace Provider Profile. In your HubSpot account, navigate to **Marketplace** > **Provider Info**. In the _Payment Method_ section, view or update your payment information.

HubSpot doesn’t take a cut of any transactions, and all transactions are handled independently of the HubSpot platform.
Join the #marketplace channel in HubSpot's Developer Slack by signing up at [https://developers.hubspot.com/slack](/getting-started/slack/developer-slack).
 <Accordion title="When a customer requests a refund, do their downloaded files remain in their HubSpot account?" defaultOpen={false}>
All files associated with the template as well as any dependencies (e.g. pages created with the template, cloned templates, and modules) will be removed once the refund is initiated.
To stay informed of the latest changes, visit and subscribe to the [HubSpot Developer Changelog](https://developers.hubspot.com/changelog).


# HubSpot Template Marketplace policies

Thank you for your interest in building and listing templates on the [HubSpot Template Marketplace](https://ecosystem.hubspot.com/marketplace/templates). At HubSpot, we value our developer community and focus on delighting our customers with amazing solutions. We strive to collaborate with our developers to bring new solutions to solve customer needs.

Below, you'll find details about HubSpot's Template Marketplace Policies, which outline requirements and compliance for listing templates in the Template Marketplace.

To learn more, please review additional resources for further details:

- [Theme](/guides/cms/marketplace/theme-requirements) and [module](/guides/cms/marketplace/module-requirements) technical requirements
- Theme and module [listing page requirements](/guides/cms/marketplace/general-requirements)
- [Template Branding Guidelines](https://www.hubspot.com/providers/template/branding-guidelines)
- [HubSpot Marketplace Terms of Use](https://legal.hubspot.com/marketplace-tou) for terms and conditions that apply to you when you list your template(s) on the HubSpot Template Marketplace.
Capitalized terms used but not defined in these policies have the meanings set forth in the HubSpot Marketplace Terms of Use. HubSpot uses the terms Service Offering and templates interchangeably.
## Compliance with Template Marketplace Policies

You agree to comply with the terms and conditions of the [Marketplace Terms of Use](https://legal.hubspot.com/marketplace-tou), [Developer Terms](https://legal.hubspot.com/hs-developer-terms) and [Developer Policy](https://legal.hubspot.com/hubspot-developer-policy), the Template Marketplace Policies, [Template Marketplace Guidelines](/guides/cms/marketplace/template-guidelines), and the Template Branding Guidelines at all times, which are incorporated herein by reference. The Template Marketplace Policies and Template Marketplace Guidelines may include requirements that you must complete in order to be listed in the Template Marketplace.

If you use HubSpot Products or services, you agree to the [HubSpot Customer Terms of Service](https://legal.hubspot.com/terms-of-service).

## Template Marketplace compliance

### Acceptable Template Marketplace submissions

You can submit only themes and modules to the Template Marketplace.

#### Free template limits

Template providers have the following limits for free templates:

- Twenty (20) free themes
- Twenty (20) free standalone modules

### Marketplace provider information

Marketplace providers must keep their provider information up to date at all times. Provider information must be kept accurate in the Provider Profile and Support Info sections. HubSpot reserves the right to remove or unapprove any template listing where any provider information (including email or website) is inaccurate, missing, or no longer functioning.

### Template ownership

Your template must be built or owned by you/your company.

Templates must not use elements from an existing Template Marketplace listing created by another provider. Any template that was purchased or downloaded, or is a cloned version of a template purchased or downloaded from the Template Marketplace will be rejected or removed. Please refer to the [Marketplace Terms of Use](https://legal.hubspot.com/marketplace-tou), specifically those terms addressing the Template Marketplace for more details on copyright infringement.

Templates cannot be purchased or downloaded from other marketplaces and submitted to the HubSpot Template Marketplace.

### Template variants as separate submissions are not permitted

Examples of <u>unacceptable variations</u> include, but are not limited to:

- **Color variations:** identical templates with different color schemes will not be counted as unique templates. For example, you cannot submit a template with a blue color theme, then submit a red version of the same template.
- **Template layouts:** submitting a template such as “Landing page with Right Sidebar” and “Landing Page with Left Sidebar” where the sidebar is simply swapped does not equate to being unique separate instances of a template.
- **Content variation:** submitting a template with content that is focused on Education and submitting the same template with content focused on Real Estate does not equate to being a unique separate instance of a template.

## Design and code requirements

The following requirements must be adhered to when designing templates for the HubSpot Template Marketplace.

### Mobile/desktop resolution and browsers

Your templates should display properly at common mobile and desktop resolutions/widths. This means templates should display properly when being viewed on both mobile and desktop screens. Your template should not require users to have to scroll off-page (unless this is the intended behavior of the template) or cause unexpected results across different browsers.

It's recommended to test using physical devices and different browsers. You can also use third-party services, such as:

- [BrowserStack](https://www.browserstack.com/)
- [Responsinator](https://www.responsinator.com/)
- [Sauce Labs](https://saucelabs.com/resources/blog/getting-started-with-manual-live-testing)

All templates must be supported by [HubSpot’s supported browsers](https://knowledge.hubspot.com/account-security/browsers-supported-by-hubspot).

### Design aesthetics

Designs must display high aesthetic quality and be visually appealing. Below are some examples of poor design aesthetics that would invalidate a submission to the HubSpot Template Marketplace:

- **Designs are too similar to existing items:** your design closely resembles an existing listing and could cause confusion in differentiating them.
- **Inconsistent spacing, padding, margin, or line-heights on elements:** your design has inconsistent spacing among the elements causing users to be unable to visibly discern between sections or groups of texts.
- **Use of inappropriate, watermarked, pixelated, or unlicensed imagery:** your design must use imagery that is appropriate. Images that are found through a search engine “image” search are not licensed for public use. If you are looking for free images, we recommend reading [HubSpot’s blog post about free image sites](https://blog.hubspot.com/marketing).
- **Use of colors that are not complementary:** your design should contain a color scheme that is aesthetically pleasing. When choosing color schemes, we strongly encourage you to think of accessibility standards in order to create an inclusive design.
- **Misaligned or inconsistently-placed elements:** your design should have a logical visual flow and not cause visual clutter. An example of this would be having floating text boxes unintentionally overlapping in areas where text wouldn't be expected to be placed.

### Stylesheets and scripts

When including stylesheets or JavaScript files in your themes and modules, it's recommended to:

- Use the [require_css function](/reference/cms/hubl/functions#require-css) and [require_js function](/reference/cms/hubl/functions#require-js) to include these files.
- Load render-blocking JavaScript in the footer to improve performance.

When including style sheets or JavaScript files in a theme's module, it is strongly recommend to [link these file dependencies](/reference/cms/modules/configuration#adding-css-and-javascript-dependencies) to the module.

Learn more about [module code quality requirements](/guides/cms/marketplace/module-requirements#module-code-quality).

### Classes

#### ID and class naming conventions

When applying IDs and classes, you must use appropriate names and follow a consistent naming convention. There are many different naming convention methodologies on the web. Below are a few examples:

- BEM: [https://en.bem.info/methodology/naming-convention/](https://en.bem.info/methodology/naming-convention/)
- ABEM: [https://css-tricks.com/abem-useful-adaptation-bem/](https://css-tricks.com/abem-useful-adaptation-bem/)
- ITCSS: [https://www.xfive.co/blog/itcss-scalable-maintainable-css-architecture/](https://www.xfive.co/blog/itcss-scalable-maintainable-css-architecture/)
- [https://www.creativebloq.com/web-design/manage-large-css-projects-itcss-101517528](https://www.creativebloq.com/web-design/manage-large-css-projects-itcss-101517528)

#### Default classes

Do not style your content based on default generated classes that are a part of your site’s structural framework. This includes but is not limited to, the following list of classes and IDs that are generated by HubSpot on templates and modules:

```css
/* ids */
#hs_cos_wrapper*, #hs_form_target_dnd*

/* classes */
.heading-container-wrapper, .heading-container, .body-container-wrapper,
.body-container, .footer-container-wrapper, .footer-container,
.container-fluid, .row-fluid, .row-fluid-wrapper,
.row-depth-*, .row-number-*, .span*, .hs-cos-wrapper,  .hs-cos-wrapper-widget,
.dnd-section, .dnd-column, .dnd-row, .dnd-module, .dnd_area*
```

HubSpot default generated classes are always subject to change. Instead, use custom classes that are assigned either through the Design Tools in the designated class fields or through local development by adding a custom class to your element tags.

#### Adding classes inside the rich text editor source code

Do not include custom classes or IDs inside of content in the rich text editor through the “Source Code” option. Classes and IDs added through this option can easily be overridden (either directly or indirectly) causing perceived issues with your template.

### Redundant and unnecessary code

Your template must not include any unnecessary code or items. This includes but is not limited to, items such as:

- Unused style sheets or scripts
- Commented out code
- Unused code

There is a difference between commented out code and comments in code. If you are providing context in your code for clarification purposes, use the [comment delimiter](/reference/cms/hubl/overview#types-of-delimiters) as this will let you comment on your code while also not having these comments show in the “View Source” or “Inspect” views of a page. See below for an example.

```html
<p>
  This is a sample. When viewing source, you should only see the HTML Comment.
</p>
<p>HTML Comment Wrapper Start</p>
<!-- This is an HTML Comment and will show in the View Source / Inspect -->
<p>HTML Comment Wrapper End</p>
<hr />
<p>HubL Delimiter Comment Wrapper Start</p>
{# This comment is using the HubL Delimiter for comments. These comments appear
here but do not render in the front end #}
<p>HubL Delimiter Comment Wrapper End</p>
```

### Templates must load over HTTPS

If using third-party files, your listing must be loaded over HTTPS to ensure proper security. Likewise, all code must render properly when being viewed over HTTPS. This is to avoid mixed content warnings in the browser console and make sure all content displays properly.

### Third-party files

Third-party files must be loaded on the HubSpot CDN unless they are from a reputable source such as JSDelivr, Google Hosted Libraries, or CDNJS. These files can be added to the stylesheet using the @import feature.
If you are including files in a module, you must use the [css_assets](/reference/cms/modules/configuration) and [js_assets](/reference/cms/modules/configuration) parameters in your meta.json file (Linked Files section in Design Tools). This only works with modules in themes, and will not work for standalone modules.
### Template errors

All templates must not display any errors in either the Design Tools or the browser console. An example of errors is shown below.
## Module compatibility with themes

The `alternate_names` attribute can be used to achieve module compatibility with themes in the Template Marketplace. It provides the bridge for a module to integrate with as many marketplace themes as possible without much effort from providers and module developers.
Theme providers define a new attribute called `alternate_names` which contains standard fields mapped to the module fields. Alternate names are supported for fonts and color fields. Module fields will [inherit](/guides/cms/content/fields/overview#inherited-fields) from the standard color and font fields. This is a new attribute introduced for theme fields. For example:
```json
{
  "label": "ButtonColor",
  "name": "button_color",
  "type": "color",
  "visibility": {
    "hidden_subfields": {
      "opacity": true
    }
  },
  "alternate_names": ["primary_color"],
  "default": {
    "color": "#516747"
  }
}
```
In the above example, developers get the ability to access the button color in two ways: `theme.button_color` and `theme.primary_color`. This way, providers can update their existing theme to meet template standards.

Modules and themes must adhere to the following requirements to ensure functionality when used across themes:

- The font and color style fields must follow these standard naming conventions: `primary_color`, `secondary_color`, `heading_font`, and `body_font`.
- If theme fields do not have `primary_color`, `secondary_color`, `heading_font`, or `body_font` fields, they can use the `alternate_names` attribute to map existing fields to standard fields. This way, when an independent module is added to the theme template, it has a similar look and feel of the themed module.
- A user can [inherit](/guides/cms/content/fields/overview#inherited-fields) either by defining `default_value_path` or `property_value_paths`, or both. Review the code snippet below for an example:
  - If you use `default_value_path`, the accepted value is `theme.primary_color`.
  - If you use `property_value_paths` you must use trailing individual properties `.color` or .`opacity` based on the property they are mapping.
```json
[
  {
    "id": "d506e41f-7206-bb8f-7fa5-d4a7de75c61e",
    "name": "color",
    "display_width": null,
    "label": "Color",
    "required": false,
    "locked": false,
    "type": "color",
    "inherited_value": {
      "default_value_path": "theme.primary_color",
      "property_value_paths": {
        "color": "theme.primary_color.color",
        "opacity": "theme.primary_color.opacity"
      }
    },
    "default": {
      "color": "#00FF03",
      "opacity": 100
    }
  }
]
```
- In the `module.html`, these fields can be referred to with the following dot notation:
```hubl
{{ theme.primary_color }} <br>
{{ theme.primary_color.color }} <br>
{{ theme.primary_color.css }}
```
- When creating a theme, the same standard naming conventions (`primary_color`, `secondary_color`, `heading_font`, and `body_font`) must be used so that the module fields can be mapped with the standard field names. Developers can either create new module fields that meet the standard naming convention, or use the `alternate_names` parameter to map existing fields to the standard fields.
- The color field with an `alternate_name` can be accessed directly using `theme.primary_color` or indirectly using `theme.colors.primary`. Below is an example:
```json
{
 "label": "Colors",
 "name": "colors",
 "type": "group",
 "children": [
  {
   "label": "Primary",
   "name": "primary",
   "type": "color",
   "visibility": {
    "hidden_subfields": {
     "opacity": true
    }
   },
 "alternate_names" : ["primary_color"]
 "default": {
  "color": "#516747"
  }
 }]
}
```
- The font field with an `alternate_name` can be accessed directly using `theme.heading_font` or indirectly using `theme.fonts.primary`. Review the snippet below for an example:
```json
{
    "label": "Fonts",
    "name": "fonts",
    "type": "group",
    "children": [
      {
        "label": "Primary",
        "name": "primary",
        "type": "font",
        "visibility": {
          "hidden_subfields": {
            "bold": true,
            "italic": true,
            "size": true,
            "underline": true
          }
        },
        "default": {
          "color": "#231f20",
          "fallback": "sans-serif",
          "font": "Montserrat",
          "font_set": "GOOGLE",
          "variant": "400"
        },
        "alternate_names":["heading_font"]
      }
```
## Marketplace listing approval review process

All template submissions and resubmissions undergo a review process and all requested modifications or corrections are required. The HubSpot Template Marketplace listing approval review process is as follows:

1.  Your submission will run through a series of automated checks to ensure that general requirements are met. If issues arise during this process, a modal will appear informing you of the issues with your template. You are then required to make the necessary changes and re-submit for approval.
2.  Once your submission passes the automated checks, it will then be added to a queue for the Ecosystem Quality Team to review. They will review the code, functionality, listing content, included assets (such as images, scripts, and more), and additional information that are related to your submission.
3.  If your submission is approved, an email will be sent to either the email address listed in your _Provider Info_ section, or associated with the submission, notifying that your listing has been approved and will be listed in the Template Marketplace. If your submission is rejected, you will receive communications from the Ecosystem Quality Team with additional information and next steps for remediation.

We may require you to make modifications or corrections to your template listing based on the results of the approval review process. Failure to make corrections or modifications may result in your termination or removal from the Template Marketplace.

Templates must be approved within three (3) reviews. Failure to make corrections or modifications may result in your termination or removal from the Template Marketplace per our [Marketplace Terms of Use](https://legal.hubspot.com/marketplace-tou), denial of the template from being listed in the future, or suspension from the Template Marketplace.

## Disclaimers

### The submission queue

Our Ecosystem Quality Team works from a queue of submissions based on the order in which they were received. Most submissions are reviewed within one week, but review turnaround times can vary depending on the volume of submissions and complexity of said submissions. Please note that the team is not able to provide information on your template's current position in the queue or the timeframe until the review is completed.

Templates that do not pass the approval review process will be moved to the bottom of the queue when they are resubmitted. Resubmitting your template, either from fixing issues from previous rejections or resubmitting while your template is still pending review, will move your template to the bottom of the queue.

Our team is dedicated to completing reviews as quickly as possible without compromising quality or thoroughness. Following the [directions and requirements](/guides/cms/marketplace/listing-templates) for submitting and listing your templates will help to ensure your template passes review.

### Support for published templates

Template providers are responsible for all code and files that are uploaded to the Template Marketplace. They must support their own work for the duration of the listing in the marketplace if there are any issues with their code or files.

You must offer email support for your template(s) and provide documentation via a publicly accessible URL. Documents like PDFs and Google Docs can be used as supplemental resources, but the publicly accessible URL <u>must</u> be a live webpage. Tickets, phone, live chat, and Facebook Messenger are all optional forms of support.

### Self-reviews for listed templates are not allowed

Template providers and their team (including others at their company who may not have worked on their template) are not allowed to publish reviews of their own listings in the Template Marketplace. Violation of this requirement may result in removal from the Template Marketplace.

### Customer requests for receipt of purchase

If requested, template providers must be able to provide a detailed receipt that includes:

- Template Listing Name
- Template Listing Description
- Date of Template Purchase
- Price of Template
- VAT Tax Information (if applicable)
- Template Marketplace Provider Company Information

### Template listing removal and rejection

HubSpot reserves the right to remove marketplace listings for reasons including, but not limited to:

- Bugs and/or defects that are present in the templates.
- Broken third party libraries, files, and/or assets in the submission
- Incompatibility with [HubSpot's supported browsers](https://knowledge.hubspot.com/account-security/browsers-supported-by-hubspot)
- Use of copyrighted material such as images, icons, fonts, audio, and video that are not licensed for distribution
- Templates that track users without their informed, explicit opt-in consent
- Templates (including linking to content from the Template Marketplace or from your template demo) that includes:
  - Promoting or engaging in illegal activities
  - Derogatory, disparaging, and/or malicious content
  - Dishonest or false claims
  - Offensive content about race, religion, age, gender or sexual orientation
- Independent module listings will be removed if the module affects other elements of the page (view Module Requirements [here](/guides/cms/marketplace/module-requirements#5-module-code-quality)).

### Template rejections

HubSpot reserves the right to reject templates for reasons not listed above or in the Template Marketplace Guidelines, Marketplace Terms of Use, Template Marketplace Policies, or Platform Policies. In all cases, if your template is rejected, a HubSpot Technical Program manager will provide feedback via email.

### For All Participants

Additionally, "inbound" is a method of attracting, engaging, and delighting people to grow a business that provides value and builds trust. This methodology extends to our Template Providers. As a Provider, your interactions with the various teams that work at HubSpot should mirror this methodology. Unsolicited outreach, whether 1:1 or through "broadcast", to HubSpot teams to promote your products and services could result in the removal of your company from the HubSpot Template Marketplace.


# HubSpot Template Marketplace theme requirements

Learn more about the requirements a theme must meet for submission to the HubSpot Template Marketplace.

If you are just starting out on your theme creation journey, we highly recommend using our free CMS theme boilerplate, which can be downloaded from our [Git Repository](https://github.com/HubSpot/cms-theme-boilerplate) or [imported within your account’s design manager UI](https://www.youtube.com).

In addition, review the [template listing page requirements](/guides/cms/marketplace/listing-templates) when submitting your template to the HubSpot Template Marketplace.

## Theme limits

Keep in mind the following limits per theme:

- Free **_CMS Hub_** accounts cannot use site search, CTA functionality, or native HubSpot video. Learn more about what's included with **_CMS Hub_** subscriptions in [HubSpot's Product & Services catalog](https://legal.hubspot.com/hubspot-product-and-services-catalog).
- Themes cannot contain more than:

  - 50 templates [(minimum pages needed)](#templates-css%2C-sections%2C-page-templates%2C-etc.)
  - 50 modules (minimum of 5)
  - 50 sections (minimum of 5)

- Themes **must not** contain:
  - Email templates
  - HubDB functionality
  - Serverless functions
  - CRM object fields
  - [Flexible columns in templates](/guides/cms/content/templates/types/drag-and-drop-templates#flexible-columns)
**Modules vs. Sections**

Sections are helpful because content creators can only drop them in full-width drop zones on the page, which helps you as a developer guarantee a great finished product.

In particular, fixed-layout sections, where the content creator cannot move elements within the section around, are a great tool to provide creative formatting and layout that couldn’t otherwise be achieved using the drag-and-drop editor.

Sections also offer extra usability benefits for the content creator because they can select individual modules inside the section, making it so that their module forms are shorter and more targeted to the element they’re editing.
## Overall theme requirements

- All submitted themes must be distinct and original. For example, the same theme with different copy or placeholder content does not count as a distinct theme. [Learn more about HubSpot Template Marketplace compliance](/guides/cms/marketplace/template-policies#template-marketplace-compliance).
- A theme must be built with HTML and HubL templates, and [dnd_area](/reference/cms/hubl/tags/dnd-areas) tags.
- Themes must respect a 12-column grid.

### Theme file structure

All themes should contain a proper folder structure and be grouped under one parent folder, which should describe your product listing. For example, if you build a theme named “SuperAwesome” for the marketplace, your structure should look similar to the image below. Learn more about [theme file structure](/guides/cms/content/themes/overview#theme-file-structure).
### Relative local file paths for templates

You <u>must</u> use relative local file paths when referring to theme assets. The best way to include these is to use the [get_asset_url function](/reference/cms/hubl/functions#get-asset-url), which returns the public URL of an asset, file, or template. You can also [generate this function](/reference/cms/hubl/functions#get-asset-url) by either right-clicking a **file** and selecting **Copy public URL**, or by clicking **Actions**, then selecting **Copy public URL**.

For example, a stylesheet referenced by `require_css` and `get_asset_url` must be formatted as follows:
```hubl
{{require_css(get_asset_url('../../css/main.css')) }}
```
```html
//cdn2.hubspot.net/hub/1234567/hub_generated/template_assets/1565970767575/custom/styles/style.min.css
```
In the video below, review the differences in file structure in your developer account versus files delivered to a marketplace customer:
## Theme performance

Using Google Lighthouse, a theme must score <u>higher</u> than the following thresholds:

- **Desktop accessibility:** 65
- **Desktop best practices:** 80
- **Desktop performance:** 70
- **Mobile performance:** 40

Learn how to [generate a Google Lighthouse report for your theme using the CLI](/guides/cms/tools/local-development-cli#evaluate-themes-and-templates-for-seo-and-accessibility).

- Theme files should be able to be minified.
- All image files should be under 1MB in size.
- All image tags should have an `alt` attribute (a value of `""` is acceptable).
- All image tags should have a `loading` attribute (a value of `""` is acceptable).

### Preview URLs for themes

You must use your own domain name when creating preview URLs. You cannot use the HubSpot-provided domain with this URL structure: `[AccountID].hs-sites.com`

A live website and not an image of the demo site must be used.

If at any point your live demo becomes inaccessible, HubSpot reserves the right, with notification to the provider, to delist/remove your theme until the live demo becomes accessible again.

## Using jQuery

jQuery is [not enabled by default](https://knowledge.hubspot.com/website-pages/include-jquery-across-your-hubspot-pages) in a customer's HubSpot account. If your theme relies on jQuery, a version of the jQuery must be included to ensure the theme works as expected.

For example, if you include a module that requires jQuery when the rest of the site doesn’t, you need to use the following code to load jQuery:

```hubl
{# this checks if the "Include jQuery" option in Settings > CMS > Pages is checked #}
{% if not site_settings.include_jquery %}
  {{ require_js("../jquery-3.4.1.js", "footer") }}
{% endif %}
```

## Theme configuration (Theme.json)

The `theme.json` file must include the following parameters:

```json
// theme.json
{
  "label": "Cool Theme",
  "preview_path": "./templates/home-page.html",
  "screenshot_path": "./images/templates/homepage.jpg",
  "enable_domain_stylesheets": false,
  "version": "1.0",
  "author": {
    "name": "Jon McLaren",
    "email": "noreply@hubspot.com",
    "url": "https://theme-provider.com/"
  },
  "documentation_url": "https://theme-provider.com/cool-theme/documentation",
  "license": "./license.txt",
  "example_url": "https://theme-provider.com/cool-theme/demo",
  "is_available_for_new_content": true
}
```

Please check your `theme.json` file and ensure the following:

- The label name matches the name in your theme listing.
- If you're using HubSpot's free CMS theme boilerplate, boilerplate values must not be present. This includes author information, documentation URL, example URL, etc.
- The documentation URL resolves and has documentation on how to use your theme.
- The preview path is a valid file in your theme.
- The screenshot path is a valid file and is related to your theme.
- The example URL resolves and leads to a demo of your theme. Do not use `preview.hs-sites.com` or `[AccountID].hs-sites.com` subdomains for the example URL.

Learn more about [theme.json parameters](/guides/cms/content/themes/overview#theme-json).

## Theme settings (Fields.json)

The `fields.json` file controls the available fields and fields groups in the theme editor, including style fields. The fields you include will depend on how much control you want content creators to have in the page editor.

- The `fields.json` file must contain at least three color fields.
- To ensure compatibility between themes and independent modules, themes must include the following font and color standard naming conventions: `primary_color`, `secondary_color`, `heading_font`, and `body_font`. Learn more about [module-theme compatibility](/guides/cms/marketplace/template-policies#module-compatibility-with-themes).

If theme fields do not have `primary_color`, `secondary_color`, `heading_font`, or `body_font` fields, they can use the `alternate_names` field.

Learn more about these [fields.json parameters](/guides/cms/content/themes/overview#fields-json) and [review an example fields.json file](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/fields.json) from the HubSpot CMS boilerplate.

Theme settings must also:

- Not conflict with your editor styles or styles set through a module. For example, do not use `!important` in your CSS stylesheet as it makes it difficult for end users to override and would cause a conflict.
- Use descriptive labels for each setting so that content creators know what they're updating.
- Apply to all templates in a theme, unless there's a specific use case for additional styles. For example, changes to the style and size of `h1` headings in theme settings must apply across all `h1` tags in the theme.

At a minimum, a theme **must** include the following theme fields:

**Typography fields:**

- Body text font fields (`p` tags)
- `h1` through `h6` font fields
- Hyperlink color (`a` tags), including hover styling

**Form fields:**

- Form background color
- Form border color
- Form label color
- Form field border color
- Form button - this includes settings for button text, background color, and hover styling.

In addition:

- Fields inside of your theme must be grouped logically where appropriate. For example, multiple fields related to typography should be grouped under a `Typography` group.
- Theme fields should have separate color and font controls for buttons and forms, as well as separate color, logo, and font controls for the header and footer.
- A portion of the theme's color and logo fields must inherit from the account's [brand settings](/guides/cms/content/fields/brand-and-settings-inheritance):
  - At a minimum, two color fields must inherit colors from the account's brand settings. Additional color fields can default to other colors, including black and white.
  - If modules within a theme are using logos, at least one logo field must inherit from the account's brand settings. If using an image field to render a logo, the image field does not have to inherit from the brand settings.

**How brand colors impact your theme's aesthetics**
## Templates (CSS, Sections, Page Templates, etc.)

### Sections

- You must use [sections](/guides/cms/content/templates/drag-and-drop/sections#reusable-sections) wherever applicable. There **must** be a minimum of five sections in a theme.
- Sections must have unique and working screenshots.
- Sections and modules should not be redundant.

### Page templates

At a minimum, a theme <u>must</u> include the following template types:

- A website page template or landing page template.

  - When including multiple-page templates, each template must have a distinct purpose. For example, a home page, an _About Us_ page, a full-width landing page, and a landing page with a right sidebar.
  - It is recommended to include at least eight page templates in a theme.

- Separate blog listing and blog post templates.

  - **Blog listing template:** the page that shows all blog posts in a listing format (known as the blogroll). The template title must reflect that it's for the listing page.
  - **Blog post template:** the blog post detail page that displays individual blog posts. The template title must reflect that it's for the blog post page.
  - In addition, blog comments and blog author boxes must be styled to match the theme.

- The following system page templates:
  - **404 error template:** shown when visitors hit a page that doesn't exist.
  - **500 error template:** shown when the site encounters an internal error.
  - **Password prompt template:** shown when a page is password protected.
  - **Subscription template:** a subscription preferences page where email recipients can manage the types of emails they're subscribed to.
  - **Subscriptions update template:** a confirmation page that appears when an email recipient updates their email subscription preferences.
  - **Backup unsubscribe template:** the page that appears for email recipients who are trying to unsubscribe if HubSpot is unable to determine their email address.
  - **Search results template:** displays search results returned when using the [site search](/guides/cms/content/content-search). Available for paid **_CMS Hub_** accounts only.

### Page template naming

- If you have templates with similar names, add descriptive words that denote the difference between them.
- Keep capitalization consistent, remove hyphens, and avoid using shorthand (e.g. spell out background instead of using bg).
- Your company name or theme name does not need to be included in the template name.

## Modules

Learn more about the requirements for theme modules and individual modules [here](/guides/cms/marketplace/module-requirements).

## Global content

### Global partials

[Global partials](/guides/cms/content/global-content) are a type of template built using HTML and HubL to can be reused across your entire website. The most common type of partials are website headers, page sidebars, and website footers. Learn how to [create global partials](/guides/cms/content/global-content#creating-a-global-content-partial-template-using-local-development-tools).

- Themes <u>must</u> include global partials.
- Global partials <u>must</u> include usable [drag and drop areas](/guides/cms/content/templates/drag-and-drop/overview). For example, you cannot hide the drag and drop area with a "hide" class.
- You <u>must</u> incorporate usable drag-and-drop areas in headers and footers.
- For menus that are used globally throughout a site, users must also be able to [select a HubSpot navigation menu](/guides/cms/content/menus-and-navigation) they've created in their account settings.
Avoid including global modules within global partials, as it can create a negative end-user experience.
## Multi-Language support

Themes <u>must</u> be able to support multiple language versions and should specify the languages that they support. This can be done by adding the [language switcher module](/reference/cms/modules/default-modules#language-switcher) in a global header, which allows customers to easily locate the language options and choose their desired language.

- You <u>must</u> only display one language at a time. For example, avoid having both English and Spanish in the UI at the same time.
- Avoid using hard-coded text. For example, rather than hard-coding a blog listing button’s text as _Read More_, set the text within a field so that the end user can update the text without having to go into the code.

## Mobile and responsive elements

Themes should be capable of adapting their content to the device it is being viewed on. They should also provide a good user experience across various devices. This includes, but is not limited to:

- Main navigation
- Sliders and tabs
- Large images
- Avoiding horizontal scrolling (unless intentional)


# HubSpot CMS overview

This section is designed to help you understand key aspects of HubSpot's _CMS_ and build great websites on it. To get the most out of this, a professional-level understanding of web development basics, including HTML, JavaScript, and CSS, is expected.

## Getting started

If you're just getting started with developing on HubSpot's CMS, it's recommended to begin with the following:

- Create a free [developer account](/getting-started/account-types#app-developer-accounts), then create a [test account](/getting-started/account-types#developer-test-accounts) within it. This will give you a testing environment to build out your CMS assets without impacting a standard HubSpot account. Because you can also build private apps in developer test accounts, along with building public apps in developer accounts, you'll have one home for both CMS and app development. Alternatively, you can create a [CMS developer sandbox account](/getting-started/account-types#cms-sandbox-accounts).
- Follow the [CMS quickstart guide](/guides/cms/quickstart) to walk through some basics, such as using the [CMS theme boilerplate](https://github.com/HubSpot/cms-theme-boilerplate), running commands using the HubSpot CLI, and the relationship between local development and content creation in HubSpot.

## Building for content creators

HubSpot's CMS is designed to help businesses grow their web presence with an emphasis on enabling marketers to create and manage web content. The website's content, lead collection, and analytics are integrated with the [HubSpot CRM](https://www.hubspot.com/products/crm), making it easy to create personalized experiences for visitors and integrate those experiences with the rest of the business.

A well-crafted website should be developed in close collaboration with your content creators to understand their needs. To that end, it's recommended that you [preview how the page building experience looks and feels for content creators](/guides/cms/setup/creating-an-efficient-development-workflow#testing) while you build. This ensures they can work independently with the site as much as possible.
HubSpot takes care of hosting and maintaining your pages, so you don’t have to worry about plugin management, updates, hosting, scaling, or security. The tradeoff is that the system puts a few more restrictions on what you can do compared to self-hosted CMS's. For example, you can’t alter or extend system fundamentals manually or via plugins, manipulate low-level rendering, or access and alter database content directly.

Developer-built content (e.g., [themes](/guides/cms/content/themes/overview), [templates](/guides/cms/content/templates/overview), [modules](/guides/cms/content/modules/overview), JavaScript, and CSS) is created in a developer file system, while page content (pages, blog posts) is laid out and built in a powerful block-based what you see is what you get (WYSIWYG) editor, and media files (content creator-built images, PDFs, etc.) are stored in a web app-based file manager.

When a page is rendered, HubSpot routes the request to one of many servers based on domain, renders the page on our servers, and caches it to a content delivery network (CDN) if possible.

## Types of content

There are many types of content that you create using HubSpot's CMS. The user interface for content creators is slightly different depending on content type, which has implications that you as a developer need to be aware of.

### Website pages and landing pages

Website and landing pages are built independent of one another, but all pages are based on templates. For content creators, the process of building a landing page or a website page is nearly identical. The distinction between them is that website pages are made to present information that’s part of your website and designed to be found organically, while a landing page is [generally associated with a specific marketing offer or campaign](https://blog.hubspot.com/marketing/landing-page-best-practices) (e.g., linked from a marketing email sent to a specific list of contacts).

In the UI for marketers, the analytics and organization of these page types are also organized separately since landing pages often have specific conversion goals.

### Blogs

HubSpot blogs have two views—one for the listing page and one for the individual post page, then each blog post is populated into each of them. You can set a blog to share the same template for blog posts and listing pages, or have separate templates for the listing page and for blog posts. Blog posts must share the same template. Learn more about [blog template markup](/guides/cms/content/templates/types/blog) and [how to create and manage blogs in HubSpot](https://knowledge.hubspot.com/blog/manage-your-blog-template-and-settings).

### Emails

Emails can be built in a few ways in HubSpot:

- **Classic email:** build email templates and modules in a similar way to website and landing pages. You can also build [coded email templates](/guides/cms/content/templates/types/email-template-markup) to have full control of the markup.
- **Drag and drop emails:** build customizable [drag and drop](https://knowledge.hubspot.com/marketing-email/create-marketing-emails-in-the-drag-and-drop-email-editor) email templates that enable content creators to build email layout and content using HubSpot's drag and drop interface.
**Please note:** building custom email modules and templates requires a _**Marketing Hub**_ _Professional_ or _Enterprise_ subscription.
## Working with data

In addition to creating page content through the in-app editors or hard-coding in templates, you can also use structured data sources to populate [dynamic page content](/guides/cms/content/data-driven-content/dynamic-pages/overview) with HubL. You can use the following data sources to populate pages:

- [HubDB](/guides/cms/content/data-driven-content/dynamic-pages/overview#hubdb-dynamic-pages): store data in cells of HubDB tables.
- [CRM records](/guides/cms/content/data-driven-content/dynamic-pages/overview#crm-object-dynamic-pages): store data in CRM records, such as contacts, companies, or custom objects.

Building dynamic pages using structured content means that you can create, edit, and remove website pages and page content by updating the data sources directly. Similar to a HubSpot blog, a set of dynamic pages will include a single listing page to display the instances of your data source, then a separate page for each individual instance. Using HubL, you can fully configure the data that the pages display.

For example, you can create a HubDB table that stores a row of information for each member of a sales team. Using that HubDB table, HubSpot can then generate a listing page to display key details from each table row (such as a name and image for each sales rep), along with a separate page per sales rep to display more information (such as their bio and phone number). Should a sales rep later be promoted to a different team, you can delete their row from the HubDB table, and HubSpot will automatically delete their detail page and remove them from the listing page.

### Serverless functions

In addition to using CRM records and HubDB data to populate pages, you can use [serverless functions](/guides/cms/content/data-driven-content/serverless-functions/overview) to write server-side code that interacts with HubSpot and third-party services through APIs. Serverless Functions are a **_Content Hub_** _Enterprise_ feature.

## Developer file system

The core assets—templates, [themes](/guides/cms/content/themes/overview), and [modules](/guides/cms/content/modules/overview), as well as the JavaScript, CSS files, and images that support them—are created in a developer file system. You can view this file system either in the left panel of the [design manager](/guides/cms/tools/design-manager) or in folders synchronized locally using the local development tools. Within the file system, assets can refer to each other with absolute or relative paths.
React-based assets, such as some HubSpot default modules and custom CMS React assets, will not appear in the design manager file system. These assets are intended to only be worked on in your local environment using the HubSpot CLI to fetch and upload.
Behind the scenes, these files are mapped to entries in a database. This is why access to the developer file system is through the HubSpot [CLI](/guides/cms/tools/local-development-cli) tools rather than direct SSH or FTP access, and some file system features you may expect, like permissions and symlinks, are not offered in the developer filesystem.

This differs from the approach of traditional CMS's, but means that broken references between file or syntax errors are caught at publish time rather than at runtime, providing you with extra insulation against accidental failures when live traffic is hitting a website.

Templates in the file system will be automatically detected and will be presented to content creators as they’re making new pages, so the structure of the file system is up to you. There’s no requirement that modules live in a `/modules/` folder or JavaScript lives in a `/js/` folder. However, it's recommended to organize your assets in a similar way to the [boilerplate example code for the CMS](https://github.com/HubSpot/cms-theme-boilerplate).
By default, HubSpot automatically minifies JavaScript and CSS included in the design manager to remove unnecessary spaces, line breaks, and comments. This also applies to JavaScript and CSS [uploaded to the design manager through the CLI](/guides/cms/setup/getting-started-with-local-development). This means that you should not add already minified code directly to the design manager.

Learn more about [JavaScript and CSS minification](/guides/cms/content/performance/overview#javascript-and-css-minification).
## Themes, templates, modules, and fields

[Themes](/guides/cms/content/themes/overview), [templates](/guides/cms/content/templates/overview), [modules](/guides/cms/content/modules/overview), and [fields](/reference/cms/fields/module-theme-fields) are the most common types of assets you'll be working with. Using each type of asset effectively gives content creators the freedom to work on websites independently while staying inside defined style and layout guardrails.

Themes are the highest-level container that you can use to package other assets to create a cohesive site. One level down, templates are the files that content creators use for building individual pages, blog posts, emails, and more. Then, modules are elements on the page, such as a pricing card or image gallery. HubSpot provides a set of [default web modules](/reference/cms/modules/default-modules) you can use out of the box for pages, and a set of [default email modules](/reference/cms/modules/default-email-modules) for building emails.
**Please note:** building custom email modules and templates requires a _**Marketing Hub**_ _Professional_ or _Enterprise_ subscription.
Themes and modules contain fields, which are settings of specific data types, such as numbers, strings, rich text, and images. You can control how these are used in rendering these objects, as well as how they should be organized and appear in the drag and drop content editor. Content creators can set values for fields in the editor, which are applied to the theme or module at render time.

Learn more in the [CMS building blocks overview](/guides/cms/content/overview).

## The HubL Language

The main language that you'll use to build website assets on HubSpot's CMS is the HubSpot Markup Language or [HubL](/reference/cms/hubl/overview) (pronounced “Hubble”). HubL is HubSpot’s extension of [Jinjava](https://github.com/HubSpot/jinjava), a templating engine based on [Jinja](https://palletsprojects.com/projects/jinja/). HubL uses a fair amount of markup that is unique to HubSpot and does not support all features of Jinja. It’s executed completely on the server-side when a page is rendered.

HubL has the features you’d expect of a simple templating language like [variables](/reference/cms/hubl/variables), [for loops](/reference/cms/hubl/loops), and [if statements](/reference/cms/hubl/if-statements), but also supports more complex rendering [macros](/reference/cms/hubl/variables-macros-syntax#macros), data fetching, and mapping with [tags](/reference/cms/hubl/tags/standard-tags), [functions](/reference/cms/hubl/functions), and [filters](/reference/cms/hubl/filters).

If you reach the limits of what's possible with HubL, HubSpot provides APIs for creating more customized solutions. **_Content Hub_** _Enterprise_ accounts can use [serverless functions](/guides/cms/content/data-driven-content/serverless-functions/overview), enabling more sophisticated server side programming.

You can refer to the [HubL language reference](/reference/cms/hubl/overview) for more details on specific language features.

## Logged in pages

Using the [Membership](/guides/cms/content/memberships/overview) feature of **_Content Hub_** _Enterprise_, you can require your CRM contacts to be logged in to view specific content of your site. Content behind membership pages can be highly personalized to the logged-in contact, and can even render Contacts, Companies, Deals and Products data from the CRM.

## Multi-language support

With HubSpot’s CMS, users can create [multi-language variations](/guides/cms/content/multi-language-content) of their content. This will allow end-users to see content in the language with which they’re most comfortable. In addition, HubSpot provides tools to help developers ensure that the right language is available to the end-user.


# HubSpot CMS quickstart guide

HubSpot's CMS is a powerful, flexible platform for creating HubSpot websites, including website pages, blogs, and lightweight apps. It features built-in security and reliability features, along with a globally distributed Content Delivery Network (CDN) that ensures fast page load times.

When developing on the HubSpot CMS, you can use your preferred tools, technologies, and workflows, such as [GitHub](/guides/cms/setup/github-integration), while developing websites. Content creators can then create pages and publish content using drag and drop editors. And because the CMS is integrated with the CRM, you can [create dynamic website experiences](/guides/cms/content/data-driven-content/dynamic-pages/overview) for your visitors based on the data you already have.

## Before you begin

Before you start, ensure you've done the following:

- Create a free test account to build without impacting a production environment. You can use either of the following types of accounts:

  - Create a [developer account](/getting-started/account-types#app-developer-accounts), then create a [test account](/getting-started/account-types#developer-test-accounts) within it. Because you can also build private apps in developer test accounts, along with building public apps in developer accounts, you'll have one home for both CMS and app development.
  - Create a [CMS developer sandbox account](/getting-started/account-types#cms-sandbox-accounts).

- Install [Node.js](https://nodejs.org/en/), which enables HubSpot's local development tools. Versions 18 or higher are supported.

## 1. Install the HubSpot CLI

Once you're ready to begin, open a terminal window and create or navigate to the directory where you want your local HubSpot files to live. This working directory is where the theme and its associated files will be placed.

Next, run `npm install -g @hubspot/cli@latest` to install the HubSpot CLI, which introduces an `hs` command that allows you to easily interact with your HubSpot account.

## 2. Configure the local development tools

Run `hs init` to connect the tools to your HubSpot account. This command will walk you through the following steps:

1.  First you’ll be guided to create a personal access key to enable authenticated access to your account via the local development tools. You’ll be prompted to press "Enter" when you’re ready to open the [Personal Access Key page](https://app.hubspot.com/l/personal-access-key) in your default browser. This page will allow you to view or generate your personal access key, if necessary. (Note: You’ll need to select at least the "Design Manager" permission in order to complete this tutorial.) Copy your access key and paste it in the terminal.
2.  Next, you’ll enter a name for the account. This name is only seen and used by you, For example, you might use "sandbox" if you're using a developer sandbox or "company.com" if you’re using a full customer account. This name will be used when running commands.

Once you've completed this simple `init` flow, you'll see a success message confirming that a configuration file, [hubspot.config.yml](/guides/cms/tools/local-development-cli#authentication), has been created in your current directory.

## 3. Create a theme

Run `hs create website-theme my-website-theme` to create a `my-website-theme` directory populated with files from the [CMS theme boilerplate](https://github.com/HubSpot/cms-theme-boilerplate).
## 4. Upload your theme to HubSpot

Run `hs upload my-website-theme my-website-theme` to upload your new theme to a `my-website-theme` folder in your HubSpot account.

Once this task has completed, you can view these files in the design manager of your HubSpot account. The design manager is an in-app code editor that displays the developer file system, and can be found by navigating to [Content > Design Manger](https://app.hubspot.com/l/design-manager/) in the left sidebar of your account.
## 5. Create a website page

To experience how content creators will use your templates and modules, create a website page using the theme you just uploaded.

- In your HubSpot account, navigate to [Content > Website Pages](https://app.hubspot.com/l/website)
- In the upper right, click **Create**, then select **Website page**.
- In the dialog box, enter a **name** for your page, then click **Create page**.
- On the next page, select the **my-website-theme** theme if it's not already selected. Then, hover over the **Homepage** template and click **Select template**.
- You'll then be brought to the website page editor where you can explore all of the options that content creators will have when working with the template. Learn more about using the editor to build and customize pages on [HubSpot's Knowledge Base](https://knowledge.hubspot.com/website-pages/create-and-customize-pages).
- Click the **Settings** tab in the editor, then select **General**. Enter a **Page title**, then set a **Content slug** to finalize the page's URL. Then, close out the dialog box by clicking **X** or pressing the **Escape key**.
- In the upper right, click **Publish** to take your page live.
## 6. Edit a CSS file

Run `hs watch my-website-theme my-website-theme`. While `watch` is running, every time you save a file, it’ll be automatically uploaded. Open your theme's `/css/components/_footer.css` file in your editor, make a change (such as updating the `.footer__copyright` selector to have `color: red;`), and save your changes. Your terminal will show that the saved file has been uploaded.

Reload your published page to see the CSS change reflected on your website.

## What's next?

You're encouraged to continue to explore and experiment with the boilerplate theme and the page-building experience. The sandbox account you created is yours to play around in and experiment with.

You can checkout [HubSpot's Inspire gallery](https://designers.hubspot.com/inspire/) to see websites, landing pages, and web apps built on HubSpot.

You might also want to check out the following documentation:

- [CMS developer tutorials](/guides/cms)
- [HubSpot CMS overview](/guides/cms/overview)

## Join the HubSpot CMS developer community

Learning is easier when you can learn from those who came before you.

HubSpot is driven by its [Culture Code](https://blog.hubspot.com/blog/tabid/6307/bid/34234/the-hubspot-culture-code-creating-a-company-we-love.aspx), embodied by the attributes in HEART: **H**umble, **E**mpathetic, **A**daptable, **R**emarkable, and **T**ransparent. This culture extends to our ever-growing developer community, with thousands of brilliant and helpful developers around the world.

### Developer Slack community

Join the [Developer Slack](/getting-started/slack/developer-slack) to collaborate with 9,000+ developers and members of the HubSpot product team.

### Developer forums

Ask questions, learn from fellow developers, and submit ideas in the [CMS developer forums](https://community.hubspot.com/t5/CMS-Development/bd-p/designers_support).


# Optimize your HubSpot development workflow

Setting up an efficient developer workflow will help you work more effectively when building websites on the HubSpot CMS. Depending on the nature of your web development team, or the nature of a specific project, your workflow may differ.

For example, a single developer building out a new site in a new HubSpot CMS account needs to worry less about testing and collaboration. On the other hand, a team of developers working on a larger website will need a clearer dev and staging process, a deployment workflow, and code living in source control in order to work efficiently.

This guide is designed to walk you through setting up an efficient developer workflow, which you can adapt to fit your needs.

This guide assumes you build websites using the [CMS CLI](/guides/cms/tools/local-development-cli), follow the [getting started with local development](/guides/cms/setup/getting-started-with-local-development) tutorial to get set up. This guide also assumes you've gone through the [quick start guide to developing on the HubSpot CMS](/guides/cms/quickstart#quick_start).

## Building with portability in mind

Before we begin setting up our developer workflow, it is important to recognize portability as a key concept in having an efficient developer workflow. The portability of your project ensures it is easy to move between environments with little friction and explanation, making it easy to test and stage changes before taking them live.

The [CMS Theme Boilerplate](https://github.com/HubSpot/cms-theme-boilerplate) is an example project that is portable, utilizing features like relative file paths, and true file format for all assets in the project using the [CMS CLI](/guides/cms/tools/local-development-cli), which allows it to live in source control and work in any HubSpot account. This project is a great starting or reference point for developers working on a new project. All of the HubSpot default Themes are built using this boilerplate, and can also be used as a portable and effective starting point.

## Setting up your development environment

For your individual development environment, each developer on your team should create a free [CMS Developer Sandbox account](https://offers.hubspot.com/free-cms-developer-sandbox). These accounts never expire and have all of the functionality of paid HubSpot CMS accounts (except being able to connect custom domains).

The CMS CLI makes it easy to interact with multiple HubSpot CMS accounts. Create a new [configuration entry](/guides/cms/tools/local-development-cli) for your CMS Developer Sandbox account. Set the name of the entry for your sandbox to be along the lines of “DEV” or “SANDBOX” so it is clear this account is a development environment. Additionally, set the `defaultPortal` to be your sandbox account, so when you run commands using the CMS CLI, it will automatically interact with your sandbox, and reduce accidental production deploys. At this point, your configuration file will look something like this:

```yaml
defaultPortal: DEV
portals:
  - name: PROD
    portalId: 123
    authType: personalaccesskey
    personalAccessKey: >-
      xxxxx-xxxxxx-xxxxxxx-xxxxxx-xxxxx-xxxxxxx-xxxxxxxx
    auth:
      tokenInfo:
        accessToken: >-
          xxxxx-xxxxxx-xxxxxxx-xxxxxx-xxxxx-xxxxxxx-xxxxxxxx
        expiresAt: '2020-01-01T00:00:00.000Z'
  - name: DEV
    portalId: 456
    authType: personalaccesskey
    personalAccessKey: >-
      xxxxx-xxxxxx-xxxxxxx-xxxxxx-xxxxx-xxxxxxx-xxxxxxxx
    auth:
      tokenInfo:
        accessToken: >-
          xxxxx-xxxxxx-xxxxxxx-xxxxxx-xxxxx-xxxxxxx-xxxxxxxx
        expiresAt: '2020-01-01T00:00:00.000Z'
```

Now, when running commands in the CMS CLI, like [`hs upload`](/guides/cms/tools/local-development-cli#upload), if you do not specify a portal, the files will be uploaded to your “DEV” account.

### Setting up your code editor

You can use your preferred code editor when building on HubSpot, whether you prefer [VS Code](#vs-code), or [other code editors and IDEs](#other-code-editors-and-ides).

#### VS Code

A significant amount of developers building on HubSpot use [Visual Studio Code](https://code.visualstudio.com/). That inspired the HubSpot VS Code Extension. The extension adds handy intellisense snippets, HubL code completion, HubL syntax highlighting HubL Linting. The project is [open source](https://github.com/HubSpot/hubspot-cms-vscode) and [contributions are welcome](https://github.com/HubSpot/hubspot-cms-vscode/blob/master/CONTRIBUTING.md). If you have feedback, please [file an issue on the repository](https://github.com/HubSpot/hubspot-cms-vscode/issues).
#### Other code editors and IDEs

While there is an official VS Code extension, there is no reason you can't use a different preferred editor. HubL is HubSpot's private fork of Jinjava, which is based on Jinja. Because of the similarities in syntax, Jinja syntax highlighting extensions tend to work well. Extensions and add-on tooling vary by editor.

## Testing

There are two main methods for testing changes:

- **Testing with watch/upload:** When working in your development environment, it is safe to use the [watch](/guides/cms/tools/local-development-cli#watch) command to automatically upload changes when you save files in your text editor to rapidly develop. If you use the Design Manager “Live preview with display options” tool for a template, as you save changes, you will automatically see them reflected in the rendered output of the template preview. To view the live preview of a template, select **Preview > Live Preview** with display options within the template editor of the Design Manager.
- **Testing locally:** to preview your changes locally without uploading to the account, you can run the `hs theme preview` command in the theme's root directory. This command will run a local proxy server at [https://hslocal.net:3000/](https://hslocal.net:3000/) which you can then use to preview the theme's templates and modules. Learn more about the [hs theme preview command](/guides/cms/tools/local-development-cli#locally-preview-theme).

#### Editor

Another critical piece of the development phase is testing your changes in the content creation tools. If you are building modules, or templates designed to be manipulated in the content editor, create pages in your development environment to ensure the content editing experience is as you intend it to be. Drag modules around into odd configurations and enter dummy content to make sure marketers can not “break” your modules when building pages. Using the content editors will help illustrate what guardrails you want to build into your templates and modules. Currently, it is not possible to move content, such as pages or blog posts, between HubSpot accounts.

#### Module Preview

When in the module editor within the Design Manager, select the “Preview” button. This will open up a preview editor for how the module and its fields will behave in the content editors. This allows you to test the fields, groups, and repeaters in your module with dummy content in a safe environment.
#### Debugging

Knowing how to debug and troubleshoot issues with your website is critical in the ongoing health and success of your website. Familiarize yourself with [debugging techniques when developing on the HubSpot CMS](/guides/cms/debugging/troubleshooting).

#### Sandboxes

As noted above in the section about setting up your development environment, you can create free [CMS Developer Sandbox](https://offers.hubspot.com/free-cms-developer-sandbox) accounts to use for testing and as a safe development environment.

## Deploying

Once you have tested your changes and are ready to take them live, it is time to deploy your changes to your production portal. Based on your local configuration, you will need to run the CMS CLI command with the `--portal` argument to interact with your production account, such as `hs upload my-theme/src my-theme --portal=PROD`. When uploading files to your production account, pay attention if there were any errors to diagnose, and make sure to briefly browse your live website to make sure there were not any unintended consequences of the deploy.

If you work as part of a web development team, it is recommended to have your entire production codebase source of truth in version control, and to deploy to your product portal when changes are merged in master. This way, your team of developers can use your favorite version control system to collaborate, track changes and easily roll-back changes.

To learn more about setting up continuous integration with git repositories, follow this guide on [utilizing GitHub actions to deploy to your production account when changes are merged into master](/guides/cms/setup/github-integration).


# Getting started with local development

The [HubSpot CLI](/guides/cms/tools/local-development-cli) (Command Line Interface) connects your local environment to HubSpot, meaning you'll have local copies of your HubSpot web assets. This allows you to use version control, your favorite text editor and various web development technologies when developing on the HubSpot CMS.

This guide is best for those who are already familiar with the CMS but want to learn about working with the CLI. If you are completely new to HubSpot CMS Hub development, we encourage you to follow the quickstart guide.
In this tutorial, you'll learn:

- How to install the CLI and connect it to your HubSpot account.
- How to fetch a module from your HubSpot account.
- How to update the module locally, then upload your changes.
- How to use the `watch` command to continue uploading saved changes.

For more commands and local file formats, see the [Local Development Tooling Reference](/guides/cms/tools/local-development-cli).

## Install dependencies

To develop on HubSpot locally, you'll need to:

1.  Install [Node.js](https://nodejs.org/en/download/package-manager), which enables HubSpot's local development tools. Versions 18 or higher are supported. It's recommended to use a package manager like [Homebrew](https://brew.sh/) or [nvm](https://github.com/nvm-sh/nvm) to install Node.
2.  Run `npm install -g @hubspot/cli` in your command line to install the HubSpot CLI globally. To install the tools in only your current directory instead, run `npm install @hubspot/cli`.

If you prefer, you can also use [Yarn](https://classic.yarnpkg.com/en/docs/install). If you are using Yarn, commands are run with the `yarn` prefix.
**Getting an EACCES error when installing?** See [NPM Resolving EACCESS permissions errors when installing packages globally](https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally).
## 1. Create a working directory

Create a folder for the work you'll be doing below. For the purposes of this tutorial, name the folder `local-dev-tutorial`.

You can do this locally by running `mkdir local-dev-tutorial` in the command line, which will create the directory. Then, run `cd local-dev-tutorial` to navigate to that directory.

## 2. Configure the local development tools

Next, run `hs init` to connect the tools to your HubSpot account. This command will walk you through the steps below.

- To connect the CLI to a HubSpot account, you'll need to copy the account's personal access key. When prompted, press **Enter** to open hubspot.com to the [personal access key page of the account](https://app.hubspot.com/l/personal-access-key). If you have multiple accounts, you'll be prompted in the browser to select an account first.
- On the personal access key page, you can generate a new personal access key or copy the existing key value if one already exists.
  - If you're creating a key for the first time, select which scopes the key has access to. You'll need to select at least the _Design Manager_ permission to interact with the account's design tools.
  - After selecting the key's permissions, click **Generate personal access key**.
- Once a key has been generated, copy its value by first clicking **Show** under the key, then clicking **Copy**.
- Paste the key into the command line, then press **Enter**.

- Next, enter a name for the account. This name is only seen and used by you when running commands. For example, you might use "sandbox" if you're using a developer sandbox or "company.com" if you’re using a standard account. This name can't contain spaces.
- Press **Enter**.

With the `init` flow complete, you'll see a success message confirming that a configuration file, [hubspot.config.yml](/guides/cms/tools/local-development-cli#authentication), has been created in your current directory.

Your `hubspot.config.yml` will look something like this:

```yaml
defaultPortal: mainProd
portals:
  - name: mainProd
    portalId: 123456
    defaultMode: publish
    authType: personalaccesskey
    auth:
      tokenInfo:
        accessToken: >-
          {accessTokenValue}
        expiresAt: '2023-06-27T19:45:58.557Z'
    personalAccessKey: >-
      {personalAccessKeyValue}
    sandboxAccountType: null
    parentAccountId: null
```

| Name | Description |
| --- | --- |
| `defaultPortal` | The account that is interacted with by default when running CLI commands.To interact with an authenticated account that is not set as the default, you can add a `--account=` flag to the command, followed by the account name or ID. For example, `--account=12345` or `--account=mainProd`. |
| `name` | Under `portals` you'll find an entry for each connected account. `name` specifies the given name for the account. You can use this name when [setting a new default account](/guides/cms/tools/local-development-cli#set-default-account) or specifying an account with the `--account` flag. |
| `portalId` | The account ID. |
| `defaultMode` | When uploading to the account, sets the default state to upload content as. Can be either `draft` or `publish`. |
| `authType` | The form of authentication used to auth the account. |
| `sandboxAccountType` | If the account is a sandbox account, indicates the ID of the parent production account. |
| `parentAccountId` | parentAccountId |
The `hubspot.config.yml` file supports multiple accounts. To authenticate more accounts, run [`hs auth`](/guides/cms/tools/local-development-cli#auth) and follow the prompts.
## 3. Create an asset to fetch in HubSpot

For the purpose of this tutorial, you'll first create a new asset in HubSpot so that you can fetch it to your local environment using the CLI.

- In your HubSpot account, navigate to **Marketing** > **Files and Templates** > **Design tools**. This will open the design manager, which is where you can access the files you upload using the CLI. This tree of files and folders is also referred to as the [developer file system](/guides/cms/overview#developer-file-system).
- In the left sidebar of the design manager, select the **@hubspot** folder to view HubSpot's default assets, such as themes and modules.

  

- In the left sidebar, scroll down to and right-click the **icon** module, then select **Clone module**. The module will be cloned to the root of the developer file system, and your new module copy will be opened on the right.

  

- At the top of the left sidebar, click **Actions**, then select **Copy path**. This will copy the relative path to the module in the developer file system, which you'll use in the next step to fetch the module locally.

  

## 4. Fetch the module to your local environment

With the module cloned, you'll now use the [`fetch`](/guides/cms/tools/local-development-cli#fetch) command to bring it into your local environment.

In the terminal, run `hs fetch '/icon copy.module'`.

```shell
hs fetch '/icon copy.module'
```

The module will be downloaded as a directory containing five files:

- `fields.json`: contains the code for the module's various fields. In this case, this includes the icon field, two accessibility option fields, and a set of style fields. You can see these fields in the right sidebar of the module editor in HubSpot.
- `meta.json`: contains the module's basic information, such as its label, ID, and the types of templates it can be used in. This information will be displayed in the right sidebar of the module editor.
- `module.css`: contains the module's CSS, which you can also see in the CSS pane of the module editor in HubSpot.
- `module.html`: contains the module's HTML, which you can also see in the HubL + HTML pane of the module editor in HubSpot.
- `module.js`: contains the module's JavaScript, which you can also see in the JS pane of the module editor in HubSpot.

  

Next, you'll make an update to the module's `meta.json` file, then upload it to your account and view the change in HubSpot.

## 5. Make changes and upload

First, make a change to the module:

- In your preferred code editor, open the module's `meta.json` file.

  

- Update the `label` field from `"Icon"` to `"CMS tutorial module"`. Then, save the file.

  

Before running the next command, `hs upload`, let's break down the command and the arguments it takes. This command takes two arguments: `hs upload <src> <dest>`

- `src`: the relative path of the source folder that you're uploading from your local environment.
- `dest`: the path of the destination directory in HubSpot, local to the root of the developer file system. You can create a new directory in the account by specifying the directory name, such as `hs upload <src> /new-directory`.
- With that in mind, because you're uploading to the root of the developer file system, run the following command:

```hubl
hs upload 'icon copy.module' 'icon copy.module'
```

- After the CLI confirms that the module has been successfully uploaded, refresh the design manager to view your change in HubSpot. You should now see that the _Label_ field shows your new value.
## 6. Run a watch to automatically upload changes

Now that you've used the `upload` command to run a one-time upload of your local files, you'll now use the `watch` command to continuously upload saved changes. This command takes the same arguments as `upload`, so you can specify the same `<src>` and `<dest>` as above.

- Run `hs watch 'icon copy.module' 'icon copy.module'`

```hubl
hs watch 'icon copy.module' 'icon copy.module'
```

With the watch now running, saved changes will automatically upload to the HubSpot account. As a demonstration, make the following local change to the module:

- In the `meta.json` file, update the `host_template_types` field to remove `"BLOG_LISTING"` and `"BLOG_POST"` so that the module is only available for pages: `"host_template_types"``:["PAGE"]`
- Save the file. This should prompt the CLI to automatically upload the file to HubSpot.
- With the file uploaded, refresh the design manager in HubSpot to view your change. The _Template types_ section of the right sidebar should now only include _Page_.
- To end the watch, press `Control + C`. It's important to note that you won't be able to run other commands in the same terminal window that the watch command is running in. To run other commands while running a watch, you should instead open another terminal window and execute your commands there.

## Next steps

Now that you've walked through how to use the `fetch`, `upload`, and `watch` commands, you may want to check out the full [CLI command reference guide](/guides/cms/tools/local-development-cli) to learn what else you can do with the CLI.

It's also recommended to check out the following tutorials:

- [Creating an efficient development workflow](/guides/cms/setup/creating-an-efficient-development-workflow)
- [How to set up continuous integration with GitHub](/guides/cms/setup/github-integration)
- [Getting started with custom modules](/guides/cms/content/modules/quickstart)
- [Getting started with themes](/guides/cms/content/themes/getting-started)
- [Getting started with drag and drop areas](/guides/cms/content/templates/drag-and-drop/tutorial)


# Set up continuous integration with a GitHub repository using GitHub Actions

As a part of your [development workflow](/guides/cms/setup/creating-an-efficient-development-workflow), you might prefer to keep your production codebase source of truth in version control. This would be especially helpful if you work as a part of a development team so that you can track changes and quickly roll them back if needed.

Using [GitHub Actions](https://github.com/features/actions), you can set up a continuous integration with a GitHub repository. This guide walks through the integration process, and assumes that you're familiar with:

- [Using Git](https://docs.github.com/en/get-started/using-git) and GitHub
- Building websites using the [HubSpot CLI](/guides/cms/setup/getting-started-with-local-development)

Below, learn how to set up the integration using the HubSpot CMS Deploy GitHub Action (recommended) or manually.

## Send local files to GitHub

Before you can integrate with GitHub, you'll first need to gather your files locally.

- If you have an existing CMS asset that lives in HubSpot, such as a theme or set of templates, you can fetch it by running the [fetch](/guides/cms/tools/local-development-cli#fetch) command as follows: `hs fetch <HubSpot_src> <local_dest>`. Alternatively, you can download all files in the account's [developer file system](/guides/cms/overview#developer-file-system) by running `hs fetch /`.
- To create a new local project, it's recommended to start with the [CMS theme boilerplate](/guides/cms/content/themes/hubspot-cms-boilerplate). If you haven't worked with the CMS theme boilerplate before, check out the [quickstart guide](/guides/cms/quickstart). If you've already installed the HubSpot CLI and configured your local environment, you can create a new local theme from the boilerplate by running `hs create website-theme <new-theme-name>`. You'll then need to upload your files to HubSpot with the [hs upload](/guides/cms/tools/local-development-cli#upload) command.

With your code available locally, you'll then [add it to a GitHub repository](https://docs.github.com/en/get-started/importing-your-projects-to-github/importing-source-code-to-github/adding-locally-hosted-code-to-github). After adding your files to GitHub, proceed to the next step to either install HubSpot's pre-made GitHub Action (recommended) or [configure the Action manually](#manually-configure-the-action).

## Use the HubSpot CMS Deploy GitHub Action (recommended)

To streamline the process, HubSpot created a GitHub Action that you can install to your GitHub project to handle automatically deploying changes from a branch to your production HubSpot account.
## Create and merge a pull request in main

- With your secrets, workflows, and scripts in your GitHub repository, create a pull request and merge it into main.
- After merging the pull request, navigate to **Actions**. You should see your deploy Action run, which will then deploy your code to your HubSpot account.

## Lock your asset in the design manager

Now that your source of truth lives in GitHub, you should lock your asset in HubSpot to prevent edits from being made there. This ensures that changes only come through the deploy action.

To lock assets in the design manager:

- In your HubSpot account, navigate to **Marketing** > **Files and Templates** > **Design Tools**.
- Locate your asset's folder in the left sidebar, then **right-click** and select **Lock folder**.


# Install and use the HubSpot Visual Studio Code extension

The HubSpot Visual Studio Code extension provides a set of tools to streamline local HubSpot development. Using the extension, you can install and manage the HubSpot CLI, authenticate and manage connected accounts, as well as quickly develop using HubL with syntax highlighting, autocomplete, boilerplate content, and more.

Below, learn how to install and use the extension.

## Install the extension

To install the HubSpot VS Code extension:

- In VS Code, navigate to **Preferences** \> **Extensions**.
- In the search bar, search for **HubSpot**, then click **Install**.

A HubSpot panel will then be added to the left sidebar, which you can click to access HubSpot-specific actions, resources, and more. 

## Manage authenticated HubSpot accounts

Using the extension, you can manage the accounts that you want connected to the CLI, similar to using the [authentication CLI commands](/guides/cms/tools/local-development-cli#authentication).

### Authenticate a new account

To authenticate a HubSpot account using the extension:

- In the extension panel, under _Authentication_, click **Authenticate HubSpot Account**.
- In the dialog box, click **Open** to open HubSpot in a browser window.
- In your browser, select the HubSpot account you want to authenticate, then click **Continue with this account**.
- If you haven't generated an access key for your account yet, select the **permissions** you need, then click **Generate personal access key**.
- In the dialog box, click **Open Visual Studio Code** to navigate back to VS Code.
- You'll then be prompted to select whether this account should be set as the default account. Click **Yes** if you'd like CLI commands to interact with this account by default. You can change this at any time in the extension panel after authentication.
- To authenticate more accounts, click **Authenticate additional HubSpot account**, then repeat the above process.

  

### Manage connected accounts

Under _Accounts_, the extension panel will display all currently authenticated HubSpot accounts, with the current default account marked by a **star** icon.

 You can right-click the **account** to view account management options:

- **Open design manager:** navigate to the account's [design manager](/guides/cms/tools/design-manager).
- **Show personal access key info:** navigate to the account's personal access key page. This can be useful if you need to copy the key, see its scopes, or deactivate it.
- **Set as default account:** instruct the CLI to interact with the account by default when running commands.
- **Rename account:** change the account's name in the CLI. This will only change the label that the CLI uses.
- **Delete account:** remove the account from your list of authenticated accounts. This will not [delete the account](https://knowledge.hubspot.com/account/how-do-i-cancel-my-hubspot-account#crm-free-only).
## File management

In the _Remote file system_ _(default account)_ section of the extension, you can view, upload, and manage files stored in the default account's [developer file system](/guides/cms/overview#developer-file-system), which will reflect the files in the account's design manager. The files displayed in this section are read-only. Below, learn how to view, fetch, upload, watch, delete, and create new files.

### View files

To view the default account's files, click to expand the **Remote File System** section in the left sidebar. You can refresh this view by clicking the **... options icon** and selecting **Refresh**.
### Upload files

To upload a new file or folder to the account:

- Click the **... options icon**, then select **Upload**.
- Select the **file** or **folder** that you want to upload to the account.
- Enter the **destination path** where the file or folder will live in HubSpot, including any file extensions (e.g. `/my-theme/images/examplefile.jpg`). You can nest files within another folder by including those folders in the path.
You can find the path of an existing folder in the account's design manager. To open the design manager, right-click the account in the VS Code extension, then select **Open design manager**. Then, locate the folder in the left sidebar of the design manager, then right-click it and select **Copy path**.
- Press **Enter** to upload the file. The file system viewer will then update after upload. If an error occurs during upload, it will display in the bottom right of VS Code.

### Watch folders

You can set a [watch](/guides/cms/tools/local-development-cli#watch) on a folder to automatically upload changes within the folder on save. To watch a folder:

- Click the **... options icon**, then select **Watch**.
- Select the **folder** that you want to watch.
- Enter the destination path where the folder will live in HubSpot, then press **Enter**.
- The extension will display a **sync icon** next to the folder in the left sidebar to indicate that it's being watched.

  

- To end a watch, right-click the folder being watched, then select **End Watch**
Only one file or folder can be watched at a time. To watch multiple files or folders simultaneously, you can instead watch the root directory or other parent directory.
### Delete and fetch files

To delete a file or folder from the account:

- Right-click the **file** or **folder** that you want to delete, then select **Delete**.
- You'll then be prompted to confirm whether you want to delete it. Click **Okay** to confirm the deletion.
To download a file or folder from the account to your local environment:

- Right-click the **file** or **folder** that you want to fetch, then select **Fetch**.
- You'll then be prompted to select the folder that you want to download it to.
### Create new files

To generate scaffolded files for new CMS assets in VS Code:

- In the left sidebar, click the **Explorer icon** to view your working directory.

  

- Right-click the **File explorer panel** and select one of the **New** options:
  - **New template:** generates a new global partial, page, partial, or section template with boilerplate content.
  - **New module:** generates a new module with boilerplate content.
  - **New Serverless Function folder:** generates a `.functions` folder that contains boilerplate `serverless.js` and `serverless.json` files. You can add another serverless function to the folder by right-clicking the **folder**, then selecting **New serverless function**.
## HubL Language support

To enable HubL language support in your files, configure your VS Code file association settings:

- In VS Code, open the command prompt by pressing `cmd` + `shift` + `p`.
- Search for and select **Preferences: Open User Settings**.
- Choose the scope of your settings by clicking either the **User** or **Workspace** tab.
- In the settings search bar, search for **files.associations**.
- Click **Add item**, then add the following item-value pairs:
  - `*html`: `html-hubl`
  - `*css`: `css-hubl`
### Supported HubL syntax

When developing with HubL, the extension offers the following [HubL syntax](/reference/cms/hubl/overview) support:

- **Inline HubL linting:** checks your HTML, CSS, HTML + HubL, and CSS + HubL files for HubL-related errors and displays them inline.
- **HubL syntax highlighting:** supports HubL syntax highlighting in the files associated through your VS Code language mode settings. Learn how to [set your file associations](#add-hubl-file-associations) below.
- **Statement wrapping:** supports `{% %}`, `{# #}`, and `{{ undefined.undefined }}` [delimiters](/reference/cms/hubl/overview#types-of-delimiters).
- **Block comment toggling:** create HubL comments by pressing `cmd` + `/`.
- **Autocomplete:** offers autocomplete suggestions for supported HubL tags, filters, expression tests, and functions. [Learn more about autocomplete](#autocomplete-reference) below.
- **Emmet** **for HTML + HubL files:** allows [Emmet](https://code.visualstudio.com/docs/editor/emmet) abbreviation and snippet expansions for HTML + HubL files. Learn how to [enable Emmet support](#enable-emmet-for-html-hubl-files) below.
- **IntelliSense suggestions:** enables [IntelliSense](https://code.visualstudio.com/docs/editor/intellisense#:~:text=You%20can%20trigger%20IntelliSense%20in,name%20to%20limit%20the%20suggestions.) suggestions when in snippet placeholders. Learn how to [enable IntelliSense Suggestions](#enable-intellisense-suggestions) below.

### Enable Emmet for HTML + HubL files

To enable [Emmet](https://code.visualstudio.com/docs/editor/emmet) support for abbreviations and snippet expansion in `html-hubl` files, map `html-hubl` to `html` in your Emmet settings:

- In VS Code, open the command prompt by pressing `cmd` + `shift` + `p`.
- Search for and select **Preferences: Open User Settings**.
- Choose the scope of your settings by clicking either the **User** or **Workspace** tab.
- In the settings search bar, search for **Emmet: Include Languages**.
- Click **Add item**, then add the following item-value pair: `html-hubl`: `html`.
### Enable IntelliSense suggestions

To enable [IntelliSense](https://code.visualstudio.com/docs/editor/intellisense#:~:text=You%20can%20trigger%20IntelliSense%20in,name%20to%20limit%20the%20suggestions.) suggestions when in snippet placeholders:

- In VS Code, open the command prompt by pressing `cmd` + `shift` + `p`.
- Search for and select **Preferences: Open User Settings**.
- Choose the scope of your settings by clicking either the **User** or **Workspace** tab.
- In the settings search bar, search for **Editor** **Suggest: Snippets Prevent Quick Suggestions** and ensure that the checkbox is **cleared**.

  

- Then, in the settings search bar, search for **Editor Parameter Hints Enabled** and ensure that the checkbox is **selected**.

  

### Autocomplete reference

The HubSpot extension enables quick access to snippets that map to support HubL tags, filters, expression tests, and functions. To access autocomplete suggestions, you'll need to use the following prefixes:

- [Expression tests](/reference/cms/hubl/operators-and-expression-tests): type the **test name** alone, then pres **Enter**. For example, typing `div` produces `divisibleby`.
- [Filters](/reference/cms/hubl/filters): type `|` followed by the **filter**, then press **Enter**. For example, typing `|se` produces `|selectattr('attr', exp_test)`.
- [Functions](/reference/cms/hubl/functions) and [tags](/reference/cms/hubl/tags/standard-tags): type `~` followed by the **function** or **tag**, then press **Enter**. For example, typing `~hub` produces `hubdb_table_rows(${table_id}`.
- [HubL supported variables](/reference/cms/hubl/variables): type the **variable name** alone, then press **Enter**. For example, `content.ab` produces `{{ content.absolute_url` `}}`.
- [Module fields](/guides/cms/content/fields/overview): in JSON files, type the **field type**, then press **Enter**. For example: `ri` produces:

```json
// typing ri > Enter produces:
{
  "name": "richtext_field",
  "label": "Rich text field",
  "required": false,
  "locked": false,
  "type": "richtext",
  "inline_help_text": "",
  "help_text": "",
  "default": null
}
```

### Other snippets

Other snippets you can generate with the VS Code extension include:

| Snippet prefix | Description | Example |
| --- | --- | --- |
| `otrue` | Generates `overrideable=True` for HubL tags. | `overrideable=True` |
| `ofalse` | Generates `overrideable=False` for [HubL tags](/reference/cms/hubl/tags/standard-tags). | `overrideable=False` |
| `for` | Returns a basic [for loop](/reference/cms/hubl/loops). | `&#123;% for iterable in dict %&#125;``&#123;&#123; iterable &#125;&#125;``&#123;% endfor %&#125;` |
| `if` | Returns a basic [if statement](/reference/cms/hubl/if-statements). | `&#123;% if &#123;condition&#125; %&#125; do_something &#123;% endif %&#125;` |
| `elif` | Returns an [else if](/reference/cms/hubl/if-statements#using-elif-and-else) statement to be used within an if statement. | `&#123;% elif &#123;condition&#125; %&#125;` |
| `else` | Returns an [else](/reference/cms/hubl/if-statements#using-elif-and-else) statement to be used within an if statement. | `&#123;% else %&#125;` |
| `hubldoc` | Returns a boilerplate HTML + HubL document. |  |
| `hublblog` | Returns boilerplate blog markup. |  |

The extension also offers autocomplete for file paths when used with the HubL tags and parameters below. To access file path autocompletions, start typing the **HubL tag** or **parameter** followed by a **quotation mark**.

- `{% include "... %}`
- `{% import "... %}`
- `{% tag_name path="... %}`
- `{% extend "... %}`
Autocomplete will not pull in [HubSpot default module](/reference/cms/modules/default-modules) file paths.
## Enable extension beta features

To enable HubSpot VS Code extension beta features:

- In VS Code, open the command prompt by pressing `cmd` + `shift` + `p`.
- Search for and select **Preferences: Open User Settings**.
- Choose the scope of your settings by clicking either the **User** or **Workspace** tab.
- In the settings search bar, search for **HubSpot: Beta** and select the **checkbox**.
## Telemetry

HubSpot for VS Code collects user data in order to improve the extension’s experience. You can [review HubSpot’s privacy policy here](https://legal.hubspot.com/privacy-policy). Additionally, you may opt out of data collection by changing the setting for global telemetry in VS Code. To read more about VS Code and telemetry, including disabling telemetry reporting, [please read the official VS Code documentation](https://code.visualstudio.com/docs/editor/telemetry).

## Contribute

This extension is open source and HubSpot welcomes contributions as well as issues for feature requests and bug reports. For more information about contributing, see the [contributing docs](https://github.com/HubSpot/hubspot-cms-vscode/blob/master/CONTRIBUTING.md) to get started.


# How to use JavaScript frameworks and libraries on HubSpot

Using the HubSpot CMS, you can create JavaScript-based web applications.

## What tier of HubSpot CMS is needed?

If your website requires server-side code or a content membership mechanism, you can take advantage of HubSpot's [serverless functions](/guides/cms/content/data-driven-content/serverless-functions/overview) and the [content membership](/guides/cms/content/memberships/overview) feature if you have an _Enterprise_ subscription. However, you could alternatively build your own system using third-party providers such as AWS Lambda combined with an API gateway to run server-side code.

If you're building a web application that needs to hit API endpoints that require authentication such a [private app access token](/guides/apps/private-apps/overview), you shouldn't run that code in the browser. You would be exposing your credentials to anyone who views the page. The right approach is to create a layer of abstraction between the browser and the authenticated API: a custom API endpoint that does not require exposing your credentials and is served from the same domain as the website calling it.

Hitting the custom API endpoint will run server-side code that can make the authenticated request. Then you can do any formatting of the data or business logic you want to keep secret, and send the result to the browser.

Commonly, serverless functions are used to do this because they have incredible scalability, and they don't require managing and maintaining your own server. You can use providers like AWS Lambda combined with an API gateway, or you can use HubSpot's first-party [serverless functions](/guides/cms/content/data-driven-content/serverless-functions/overview). The advantage of HubSpot serverless functions is that you don't need to manage multiple separate services. The experience is simplified and directly integrated with the same developer file system which themes, templates and modules all exist in.

If you don't need to make authenticated API calls, then you don't need enterprise for your app. React and Vue are front end frameworks that don't need serverless functions to work, it is what you do with them that matters.

## Frameworks and libraries

For web applications, developers commonly use JavaScript frameworks that help manage state and User Interface (UI).

CMS Hub was not purpose-built to work with a specific framework in mind, but many common JavaScript frameworks work on HubSpot CMS. Building on HubSpot, you may need to think about how you work with those frameworks differently. But the core things needed to work with these frameworks are available: the ability to write [custom templates](/guides/cms/content/templates/overview), [modules](/guides/cms/content/modules/overview), and JavaScript. We also enable you to do your [coding locally](/guides/cms/tools/local-development-cli), so that you can use a build step.

## What you should know

We are collaborating with our developer community to establish the best practices for building with common JavaScript frameworks on HubSpot. While it is possible to do it, there are aspects of how the HubSpot CMS works that may require you to consciously set up your project differently than you might on a simple HTML page.

There may also be some parts of your development workflow that aren't what you're used to. We ask that you let us know your feedback so we can improve the experience for all developers. Currently the best place to do that is our [Developer forum](https://community.hubspot.com/t5/HubSpot-Developers/ct-p/developers). As we experiment and learn, we will continue to update our documentation accordingly.

### Things to consider when building

The HubSpot CMS has a powerful [module system](/guides/cms/content/modules/overview), enabling you to create re-usable chunks of CSS, JavaScript and HTML with access to [HubL, the HubSpot templating language](/reference/cms/hubl/overview). HubSpot modules provide a way for you to give a lot of control and power to content creators. Modern JavaScript frameworks often have their own module systems. These systems are all built independent from each other and as a result often have different solutions for issues you might encounter.

#### Server-side rendering and client side rendering

Server-side rendering is when the HTML for a page is generated from templating logic on the server before sending any HTML to a browser.

Client-side rendering is when a lighter or "incomplete" version of the HTML is sent from the server, and JavaScript is used to generate the HTML. This transfers the processing of logic from the server to the web browser (the client).

Hydration is the act of combining both techniques. First, on the server, as much HTML as possible is generated. Then JavaScript evaluates the HTML provided and makes smaller changes to it as needed when the user interacts with the page or data is received. This reduces the load on the client and potentially reduces the time it takes for the user to see the loaded content.

On HubSpot CMS, HubL is processed server-side and then cached at the CDN level. You can then use JavaScript to hydrate or client-side render the HTML the browser serves to the site visitor.

#### Single Page App (SPA) analytics

Analytics is important for your company's ability to grow and adapt to solve for your customers and prospects. When building a single page app that contains multiple views, you may want to track the visitor seeing different views, as pages.

Most analytics platforms provide a way to do this with JavaScript, HubSpot is no different. [Push the page-view](/reference/api/analytics-and-events/tracking-code#tracking-in-single-page-apps) when your app changes views.

#### Building your app utilizing HubSpot modules

HubSpot's module system is a server-side module system, generating an HTML document from HubL + HTML partials and generating minified CSS and JavaScript for each module within a page.

If you build using HubSpot modules, there are several benefits that come along with it:

- Content creators can add your module to pages that have drag and drop areas or flexible columns. They can also move and remove the module themselves.
- You can provide fields to the content creator that let them configure settings for your app.
- Your code is only rendered to the page only if the module is actually used.
- `Module.css` and `module.js` is automatically minified.

The cost of using the HubSpot module system is that it requires modules to be made up of specific files and in different places than you might normally place your code.

#### Building a full template instead

You could also build your application as a template rather than within the module framework. This gives you more flexibility with your file structure. But you do not get the benefits that modules provide; content creators will not be able to add this application to pages within drag and drop areas and flexible columns.

#### Delimiters

Some JavaScript frameworks use curly braces `{ }` to delimit their code. The HubL language uses these braces, as well. There are three strategies you can use to ensure you don't have conflicts between your framework and HubL: You can use the raw HubL tag to wrap around your JSX, set the framework to use a different delimiter, or use a build step that compiles the JavaScript beforehand.

## VueJS

The popular [Vue.js](https://vuejs.org/) framework can be used with and without a build step. See [Vue's own documentation](https://vuejs.org/v2/guide/installation.html) for a more detailed breakdown of the pros and cons of each method. On HubSpot there are specific pros and cons you should also be keeping in mind.

### Without a build step

Integrating Vue.js without a build step into a module is easy.

#### Add the vue library to your module

In your `module.html` file, use [`require_js`](/reference/cms/hubl/functions#require-js) to add the [Vue library](https://vuejs.org/v2/guide/installation.html#Direct-lt-script-gt-Include) ensuring it will only load once when your module is added to a page.
While developing, use the dev build to get useful information for debugging. Once in production, it is recommended to use either the CDN URL for the specific Vue version, or download that file and host it as a JavaScript file in the HubSpot [developer file system](/guides/cms/overview#developer-file-system).
#### Add the HTML code

Copy the HTML code from the [Vue.js introduction](https://vuejs.org/v2/guide/index.html#Declarative-Rendering), and paste it into your `module.html` file. Wrap this code in a HubL raw tag to prevent it from being evaluated as HubL.

```hubl
{# raw prevents code within it from being evaluated as HubL #}
```

#### Add your JavaScript code

Copy the JavaScript from the [Vue.js introduction](https://vuejs.org/v2/guide/index.html#Declarative-Rendering), and paste it into your `module.js`. Wrap this code in an [event listener to ensure it's executed once the DOM content has finished loading](https://developer.mozilla.org/en-US/docs/Web/API/Window/DOMContentLoaded_event). Publish your module, and preview it. **You should now see your basic Vue app working.**

```js
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!',
  },
});
```

### With a build step

We've built a [boilerplate](https://github.com/HubSpot/cms-vue-boilerplate) \[BETA\] to help you get up and running with the HubSpot module approach to building a VueJS application. The easiest way to take advantage of it is to run the `hs create vue-app` command from the [CMS CLI](/guides/cms/tools/local-development-cli). Directions can be found in the [repository](https://github.com/HubSpot/cms-vue-boilerplate/).

This boilerplate is new and we would love to hear your feedback! Let us know what could be improved and any issues you encounter. The best way to provide feedback is by [submitting issues to the GitHub repository](https://github.com/HubSpot/cms-vue-boilerplate/issues).

### Working with HubSpot forms and CTAs within Vue components

HubSpot CTAs and forms have their own script tags, and manage their own HTML themselves. To ensure your vue component doesn't modify the form or CTA, create an HTML element around the CTA/form embed code. [Apply `v-once` to that element.](https://vuejs.org/v2/api/#v-once) This ensures the code will be rendered once and then ignored by your Vue component.

## ReactJS

Rather than using HubL to build modules and partials, you can use [JavaScript and React](https://developers.hubspot.com/docs/cms/building-blocks/modules/build-modules-and-partials-with-react). In addition to stitching server-rendered React components into the HTML generated by HubL, JavaScript modules and partials support both server-side and client-side interactivity. Learn more in HubSpot's [Introduction to JS Building Blocks](https://github.hubspot.com/cms-js-building-block-examples/).

You can also check out the [React boilerplate](https://github.com/HubSpot/cms-react-boilerplate) to get up and running quickly with a [React](https://reactjs.org/) app inside of a HubSpot module. The easiest way to take advantage of it is to run the `hs create react-app` command from the [CMS CLI](/guides/cms/tools/local-development-cli). From there follow the instructions in the [repository](https://github.com/HubSpot/cms-react-boilerplate).

This boilerplate is new and we would love to hear your feedback! Let us know what could be improved and any issues you run into. The best way to provide feedback is by [submitting issues to the GitHub repository](https://github.com/HubSpot/cms-react-boilerplate/issues).

## Other JavaScript libraries

There are a lot of JavaScript libraries out there and it is impossible for us to document all of them individually. There are some core best practices to know and understand when using JavaScript libraries on HubSpot.

### Use require_js instead of script tags

You can have dozens of modules, and templates that use shared JavaScript libraries, and not worry about loading those libraries multiple times. To do this you need to use the [`require_js`](/reference/cms/hubl/functions#require-js) HubL function. Scripts loaded using this function will only load once per page regardless of how many modules, partials, and the template, requires them.

```hubl
{{ require_js(get_asset_url('/js/jquery-latest.js')) }}

{{ require_js("https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js") }}
```
Use `get_asset_url()` to require files stored within the developer file system. The advantage aside from just co-locating your development files and consolidating security of these files, is that it will result in fewer DNS lookups.
Using require can be amazing for performance, because not only will you only load the file once. If assets on a page page don't need that library, it won't be loaded at all. You can even use requires with HubL logic to load resources only when you truly need it.

## Recommended tutorials and guides

- [Optimizing for performance](/guides/cms/content/performance/speed)
- [Accessibility is not a feature](/guides/cms/content/accessibility)
- [How to use web components on HubSpot](https://developers.hubspot.com/blog/use-web-components-in-hubspot-cms-development)
- [Getting started with modules](/guides/cms/content/modules/quickstart)
- [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)
- [Building dynamic pages with HubDB](/guides/cms/content/data-driven-content/dynamic-pages/hubdb)
- [Build modules and partials with JavaScript](https://developers.hubspot.com/docs/cms/building-blocks/modules/build-modules-and-partials-with-react)


# File Manager

In addition to the developer file system, HubSpot's file manager can be used to store and serve files. Files in the file manager are served over HubSpot's global content delivery network (CDN).

By default, all files uploaded to the file manager are publicly accessible and may be indexed in search engines. After uploading your files, you can [manage your file's visibility settings](https://knowledge.hubspot.com/files/organize-edit-and-delete-files#edit-your-file-s-visibility-settings) to prevent files from being indexed or accessed.

The File Manager can be found at [**Marketing > File and Templates > Files**](https://app.hubspot.com/l/files/) in the top navigation menu.
## When to use the File Manager

Generally, the file manager should be used for files intended to be utilized in file pickers throughout HubSpot. For example, when selecting an image in an image or rich text module.

Before uploading files for use with the file manager:

- Certain [file size and type limits](https://knowledge.hubspot.com/files/supported-file-types#files-tool) will apply. Learn more about [what to consider before uploading files](https://knowledge.hubspot.com/files/upload-files-to-use-in-your-hubspot-content#before-you-get-started) to the file manager.
- Files uploaded to the file manager cannot be edited within the HubSpot app, other than minor image file editing.
- If you intend to edit text-based files, they must be stored in the design manager.
- Text-based files uploaded to the file manager will not be minified or modified in any way. To take advantage of HubSpot’s [JavaScript minification](/guides/cms/content/performance/overview#javascript-minification) and [CSS minification and combination](/guides/cms/content/performance/overview), store these files in the design manager.

## Uploading files to the File Manager

Files can be uploaded to the file manager via the following options:

- To upload files directly in HubSpot, learn how to [upload files to the file manager](https://knowledge.hubspot.com/files/upload-files-to-use-in-your-hubspot-content).
- To upload files through the [CMS CLI,](/guides/cms/tools/local-development-cli) use the `hs upload filemanager` command.
- To upload files using an API, learn more about [HubSpot's _Upload a new file_ API](/reference/api/library/files/v3#post-%2Ffiles%2Fv3%2Ffiles).

After uploading your files to the file manager, learn how to [organize and manage your files and file details](https://knowledge.hubspot.com/files/organize-edit-and-delete-files).

## Using File Manager files

Files uploaded to the file manager can be accessed via the following options:

- Files uploaded to the file manager are accessible in the various file pickers throughout HubSpot and HubSpot's CMS, such as in rich text or image modules on pages.
- Files uploaded to the file manager can be accessed through a direct download link. Learn how to [retrieve a file's direct download link](https://knowledge.hubspot.com/files/provide-a-direct-download-link-to-a-file-hosted-on-the-files-tool).

## Optimizations

File Manager files are automatically [cached](/guides/cms/content/performance/overview#browser-and-server-caching), [compressed and resized to be served efficiently](/guides/cms/content/performance/overview#image-compression-optimization-and-automatic-image-resizing) and [accessible across all of your hosted domains to reduce cross-origin requests](/guides/cms/content/performance/overview#domain-rewriting). Learn more about the HubSpot CMS [CDN, Security, and Performance](/guides/cms/content/performance/overview).

## Serving HTML and JS files from the file manager

HTML and JavaScript files uploaded to the File Manager and served using a default HubSpot domain to serve files (i.e. `f.hubspotusercontentXX.net`), use Content Type: `text/plain` . This means web browsers will not render and evaluate the code.

If a user goes directly to an HTML file there it will display the HTML code itself to the user. To avoid this, you must serve these files from one of your connected domains instead of a HubSpot default domain.


# Build location-based pages with HubDB

HubDB supports locations as a field type, which enables you to build location-based pages on HubSpot's CMS.

In this tutorial, learn how to create a page with a map on HubSpot’s CMS using HubDB. You can reference more detailed HubDB documentation [here](/guides/cms/storage/hubdb/overview).

You’ll need:

- Approximately one hour.
- Some prior knowledge of HubSpot's CMS, HTML and CSS will be needed to customize your page.

## What's a location?

When you create a column on a HubDB table with the _Location_ field type, you're designating this column as a place to store coordinates that point to a specific location in the world.

A location is often represented as _coordinates_ (a pair of decimal numbers) such as 42.36, -71.11. The first number is the latitude, or the degrees North or South of the equator. The second number is the longitude, or the degrees from the Prime Meridian. The Prime Meridian is an imaginary line starting at the North Pole, running through Greenwich, England, France, Spain, Algeria, Ghana and ending at the South Pole. Longitudes east of that line are positive, west is negative. At 180 degrees, the positive and negative degree values meet.

## 1. Find the visitor's location

With the [Global Positioning System](https://en.wikipedia.org/wiki/Global_Positioning_System) (GPS), you can determine the location of a visitor to your site. Mobile phone devices have GPS chips that can receive GPS signals. These devices can also use nearby Wi-Fi networks or mobile network towers to determine their location. Laptop and desktop computers typically use Wi-Fi to determine their location.

Within a page loaded in the web browser, you can use JavaScript to request a device's location.

```html
<script>
  navigator.geolocation.getCurrentPosition(function (position) {
    console.log(
      "I'm at " + position.coords.latitude + ', ' + position.coords.longitude
    );
  });
</script>
```

This will first ask the visitor if it's OK for the page to get the visitor's location, and then respond with the coordinates. The response may appear like the following:

```html
I'm at 42.370354299999995, -71.0765916
```

## 2. Build a database

Once you know the visitor's location, you can help them find different locations in their general area.

Create a new table in HubDB and add two columns: 'location' (type: Location) an 'cuisine' (type: Text).

It should look like this:
You can build your own database of local eateries or you can import data for lunch spots near HubSpot in Cambridge, Massachusetts. Here's a sample file you can use: [lunch-spots.csv](https://cdn2.hubspot.net/hubfs/327485/lunch-spots.csv). Upload the file to HubDB, and map the columns as shown.
Once you finish the import, you should have a list of eateries that looks like this:
Publish your table, make a note of the table ID (the last number in the URL) and you're ready to start using your data.

## 3. Create a simple listing page

Create a simple list for this data using HubL and the HubDB functions.

In the design manager, create a new template using the code editor. You can name it something like "lunchspots.html".

Add this code just after `<body>` tag, using the table ID you noted above.

```hubl
{% set table_id = YOUR_TABLE_ID %}

     <table>
        {% for row in hubdb_table_rows(table_id) %}
          <tr>
              <td>{{ row.name }}</td>
              <td>{{ row.cuisine }}</td>
          </tr>
        {% endfor %}
    </table>
```

When you preview the template, a simple list of restaurants and a description of their cuisine should appear. If nothing appears, double-check that your table ID is correct and that the table is published.

## 4. Filter to a single listing

Your listing page can also double as a detail page. You can link the restaurant name to a page specific to that restaurant. Make sure to leave in the `set table_id...` line here and in all other code samples on this page.

```hubl
<table>
     {% for row in hubdb_table_rows(table_id) %}
       <tr>
           <td><a href="?{{ request.query }}&row_id={{ row.hs_id }}">{{ row.name }}</a></td>
           <td>{{ row.cuisine }}</td>
       </tr>
     {% endfor %}
</table>
```

Each row will now be linked back to the same page with a query string parameter in `row_id` which is the unique ID of this row. Note the use of `request.query` to make sure any existing query string parameters are preserved on the current URL.

Now we'll add the details:

```hubl
{% if request.query_dict['row_id'] %}
   {% set row = hubdb_table_row(table_id, request.query_dict['row_id']) %}
   <h1>{{ row.name }}</h1>
   <h3>{{ row.cuisine }}</h3>
   <p>{{ row.location['lat'] }}, {{ row.location['lon'] }}</p>
{% else %}
   <table>
      {% for row in hubdb_table_rows(table_id) %}
        <tr>
            <td><a href="?{{ request.query }}&row_id={{ row.hs_id }}">{{ row.name }}</a></td>
            <td>{{ row.cuisine }}</td>
        </tr>
      {% endfor %}
 </table>
{% endif %}
```

The if statement (`{% if request.query_dict['row_id'] %}`) determines whether to show the details or skip down to the block to show the listing. It will look something like this: 

## 5. Determine the visitor's location

You can also ask for the visitor's location, so you can show them how far away each restaurant is. Change your code to the following.

```hubl
{% if request.query_dict['row_id'] %}
  {% set row = hubdb_table_row(table_id, request.query_dict['row_id']) %}
  <h1>{{ row.name }}</h1>
  <h3>{{ row.cuisine }}</h3>
  <p>{{ row.location['lat'] }}, {{ row.location['lon'] }}</p>
{% elif request.query_dict['lat'] %}
      <table>
      {% for row in hubdb_table_rows(table_id) %}
        <tr>
            <td><a href="?{{ request.query }}&row_id={{ row.hs_id }}">{{ row.name }}</a></td>
            <td>{{ row.cuisine }}</td>
            <td>{{ row.location|geo_distance(request.query_dict['lat'], request.query_dict['lon'], "mi")|round(3) }} mi away</td>
        </tr>
      {% endfor %}
      </table>
{% else %}
  Please allow us to read your location so we can show you the closest lunch spots.
  <script>
    navigator.geolocation.getCurrentPosition(function(position) {
      window.location = window.location.href.split('?')[0] + "?lat=" + position.coords.latitude + "&lon=" + position.coords.longitude;
    });
  </script>
{% endif %}
```

This code introduces an `if` statement. In the default case, it asks the visitor to share their location. If they accept, it redirects the page with `lat` and `lon` query parameters. The second time the page loads, it shows the list, now with a third column that is the calculated distance from the provided location.

Since `row.location` just returns the restaurant's coordinates, HubSpot uses the `geo_distance` filter to calculate the distance. It takes the visitor's latitude, longitude and the units of the distance. The units default to meters ("M"), but also accept kilometers ("KM"), miles ("MI"), and feet ("FT"). Lastly, HubSpot round the number a bit to make it more readable.

## 6. Sort the list

If the list got much bigger, it would be helpful to show the closest restaurants first. You can do so by adding sorting options to the `hubdb_table_rows` function. Change this section as follows:

```hubl
{% elif request.query_dict['lat'] %}
      {% set params = "orderBy=geo_distance(location," ~ request.query_dict['lat'] ~ "," ~ request.query_dict['lon'] ~ ")" %}
      <table>
      {% for row in hubdb_table_rows(table_id, params) %}
        <tr>
```

Now the list will be sorted by the distance between the location column and the visitor's coordinates. The key is building the `geo_distance` ordering function which ends up looking like `orderBy=geo_distance(42.37,-71.076)`. To sort the list in reverse order, with the restaurants the furthest away first, you can prepend a `-` (minus sign) to `geo_distance`.

## 7. Filter by distance

You can filter the list by distance as well. To find all restaurants less than one mile away, use `geo_distance` as a query filter.

```hubl
{% elif request.query_dict['lat'] %}
      {% set params = "orderBy=geo_distance(location," ~ request.query_dict['lat'] ~ "," ~ request.query_dict['lon'] ~ ")" %}
      {% set params = "geo_distance(location," ~ request.query_dict['lat'] ~ "," ~ request.query_dict['lon'] ~ ",mi)__lt=1.0&" ~ params %}

      <table>
      {% for row in hubdb_table_rows(table_id, params) %}
        <tr>
```

This constructs another parameter to the HubDB query which looks like `geo_distance(location,42.37,-71.076,mi)__lt=1.0`

The `geo_distance` query filter takes four arguments. First is the location column that you want to filter by. The next three parameters are latitude, longitude, and distance units. After the `geo_distance` operator, specify the filter operator, "less than" or "lt". Finally, after the equal sign is the value to compare with the result of the function.

## 8. Add a map

You can [Google Maps APIs](https://developers.google.com/maps/documentation/javascript/) to create a visual map of the list. Replace the block of code inside the second if condition (`elif request.query_dict['lat']`) with this:

```hubl
<style>
  #map {
    width: 100%;
    height: 1200px;
  }
</style>

<script>
    var map;
    function initMap() {
      map = new google.maps.Map(document.getElementById('map'), {
        center: {lat: 42.3665171, lng: -71.0820328}, zoom: 16
      });

    var marker_origin = new google.maps.Marker({
       map: map,
       title: "you are here",
       icon: { scaledSize: {width:50, height:50}, url: "http://maps.google.com/mapfiles/kml/paddle/blu-blank.png"},
       position: {lat: {{ request.query_dict['lat'] }}, lng: {{ request.query_dict['lon'] }}}
    });

  {% for row in hubdb_table_rows(table_id) %}
    var marker_{{ row.hs_id }} = new google.maps.Marker({
        map: map,
        position: {lat: {{ row.location["lat"] }}, lng: {{ row.location["lon"] }}},
        title: '{{ row.name }}',
        icon: { scaledSize: {width:40, height:40}, url: "http://maps.google.com/mapfiles/kml/shapes/dining.png"}
    });

    marker_{{ row.hs_id }}.addListener('click', function() {
      new google.maps.InfoWindow({
'
      }).open(map, marker_{{ row.hs_id }});
    });
  {% endfor %}
    }
</script>
<script
  src="https://maps.googleapis.com/maps/api/js?key=YOUR_GOOGLE_API_KEY&callback=initMap"
  async
  defer
></script>
```

Add in your table ID and your [Google API key](https://developers.google.com/maps/documentation/javascript/get-api-key) and you should now see an interactive map in place of the listing. Your map should look like this (with your blue origin marker wherever you are).

That's it! You've built a page that uses a visitor's location to find a local place to eat. The key is the `geo_distance` function which can be used to filter, order, and calculate the distance. This simple page could be enhanced by adding a toggle between the listing and map view, a map and more info on the details page, and perhaps a link to each restaurant's website.

If the visitor's browser cannot determine their location, or they want to look for places in a different area, you can allow them to search for a location using the [Google Maps Geocoding API](https://developers.google.com/maps/documentation/geocoding/start).


# HubDB
HubDB is a tool that allows you to create tables to store data in rows, columns, and cells, much like a spreadsheet. You can customize a HubDB table's columns, rows, and other settings based on your needs. For example, you could use a HubDB table to:

- Store feedback from an external mechanism to retrieve at a later time.
- Store structured data that you can use to [build dynamic CMS pages](/guides/cms/content/data-driven-content/dynamic-pages/hubdb) (_**CMS Hub**_ _Professional_ and _Enterprise_ only).
- Store data to use in a [programmable email](https://knowledge.hubspot.com/marketing-email/create-programmable-emails) (_**Marketing Hub**_ _Enterprise_ only).

, and you can retrieve a table's data in multiple ways, depending on your use case. To get data from a HubDB table, you can:

- Query the data externally via the [HubDB API](/guides/api/cms/hubdb).
- Use HubSpot’s HubL markup tags to pull data into the CMS as structured content.
- Use the HubDB API with [serverless functions](/guides/cms/content/data-driven-content/serverless-functions/overview) to provide an interactive web app experience.
- To [use HubDB data in pages](/guides/cms/content/data-driven-content/dynamic-pages/hubdb), you need _**CMS Hub**_ _Professional_ or _Enterprise_.
- To use HubDB data in [programmable emails](https://knowledge.hubspot.com/marketing-email/create-programmable-emails), you need _**Marketing Hub**_ _Enterprise_.
- Publishing, editing, and viewing existing tables requires [HubDB permissions](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#website-tools). Creating, clone, deleting, and editing a HubDB table's settings requires [_HubDB table settings_ permissions](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#settings).
## HubDB architecture

A HubDB table consists of rows, columns, and cells, similar to a spreadsheet.

- **Tables:** a table is a 2-dimensional arrangement of rows and columns. When a table is created, it is assigned a globally unique ID which you can use when needing to specify a table in HubL or through the API.
- **Rows:** rows are horizontal slices of a table. In a spreadsheet application, rows are represented by numbers, starting with 1. Each table row is given a unique ID on creation. Each row in a table comes with a few columns by default:

| Column | Description |
| --- | --- |
| `hs_id` | An automatically assigned, globally unique, numeric ID for this row. |
| `hs_created_at` | A timestamp of when this row was created. |
| `hs_path` | When used with dynamic pages, this string is the last segment of the URL's path for the page. |
| `hs_name` | When used with dynamic pages, this is the title of the page. |
Rich text area columns in HubDB are limited to 65,000 characters. For more information, [view the changelog announcement](https://developers.hubspot.com/changelog/hubdb-rich-text-area-limited-to-65000-characters-and-a-new-meetings-module).
- **Columns:** columns are vertical slices of a table. Each column has a type, which is used to store kinds of data. A table can include as many columns as you'd like, and each is assigned a globally unique ID on creation. Column ID start at a value of `1`, but are not necessarily sequential, and cannot be reused. Column types include:
  - Text
  - Rich text
  - URL
  - Image
  - Select
  - Multi-select
  - Date
  - Date and time
  - Number
  - Currency
  - Checkbox
  - Location (longitude, latitude)
  - Foreign ID
  - Video
- **Cells:** Cells store the values where a row and column intersect. Cells can be read or updated individually or as part of a row. Setting the value of a cell to `null` is equivalent to deleting the cell's value.

## HubDB technical limits

Note the following HubDB technical limits:

- Account limits:

  - 1,000 HubDB tables per account.
  - 1 million HubDB rows per account.

- Table limits:

  - 250 columns per table.
  - 10,000 rows per HubDB table.
  - 700 characters per table name.
  - 700 characters per table label.

- Column limits:
  - 65,000 characters per rich text column.
  - 10,000 characters in each text column.
  - 700 characters per column name.
  - 700 characters per label.
  - 300 characters per column description.
  - 700 characters per selectable option within a column.
- Page limits:
  - 10 calls to the `hubdb_table_rows` HubL function per CMS page.
  - 10 [dynamic pages](/guides/cms/content/data-driven-content/dynamic-pages/hubdb) using the same HubDB table.
  - HubDB's with dynamic pages turned on must have lowercase paths so that URLs to these pages can be case insensitive.

## Create a HubDB table

You can create HubDB tables either [through HubSpot's UI](https://knowledge.hubspot.com/website-and-landing-pages/create-and-hubdb-tables) or through the [HubDB API](/reference/api/cms/hubdb#post-%2Fcms%2Fv3%2Fhubdb%2Ftables).

All new tables created are set with a status of draft. They cannot be used to output data via HubL or API until you publish the table. When creating a table, you can also [manage its settings](https://knowledge.hubspot.com/website-and-landing-pages/create-and-hubdb-tables#manage-table-settings-cms-hub-professional-and-enterprise-only), such as allowing public API access and whether its data will be used to [create dynamic pages](/guides/cms/content/data-driven-content/dynamic-pages/hubdb).

## Join multiple HubDB tables

HubDB tables can be joined using the Foreign ID column type, which allows you to render the combined data from multiple tables. This can be helpful when some data might be shared across multiple data stores, allowing one centralized data table of this information, which can then be accessed across multiple other HubDB table data stores.

Below, learn how to join multiple HubDB tables.

### 1. Add a Foreign ID column to the main table

- In your HubSpot account, navigate to **Marketing** > **Files and Templates** > **HubDB**.
- Locate the table you want to add a table join to, click the **Actions** dropdown menu, then select **Edit**.
- In the top right, click **Edit**, then select **Add column**.
- Enter a **label** and **name** for the new column.
- Click the **Column type** dropdown menu and select **Foreign ID**.
- Click the **Select table** dropdown menu and select the **table** you want to join with your current table.
- Click the **Select column** dropdown menu, then select the **column** from the joining table you have selected to be visible in the Foreign ID field.
- Click **Add column**.
The value you chose as the _Select column_ only dictates which column value you see in the Foreign ID field in the HubDB UI. All table columns are available when rendering the joined HubDB tables.
### 2. Add foreign table rows to your table's rows

Now that you have a _Foreign ID_ column, you will have a multi-select column field on every row in your HubDB table, which allows you to select a foreign table's rows.

The _Select column_ field you chose will be used in this multi-select field to identify which row you are selecting from the foreign table. In the example below, the multi-select values for the _Expertise table join_ field are the values available from _Name_ column of the foreign HubDB table.
It's safe to edit the _Select column_ field of your _Foreign ID_ column, and will simply update which column's values will display in the HubDB UI.
### 3. Render your joined HubDB table data

All of a foreign table's row data is accessible via HubL for rendering, not just the _Select column_ field. HubDB foreign row data is accessible by using a [nested for loop](/reference/cms/hubl/loops), looping through all of the foreign rows associated with an individual row.

```hubl
{% for row in hubdb_table_rows(tableId, filterQuery) %}
  the name for row {{ row.hs_id }} is {{ row.name }}
  {% for foreign_row in row.foreign_table %}
  	the name for foreign row {{ foreign_row.hs_id }} is {{ foreign_row.name }}
  {% endfor %}
{% endfor %}
```

## Access HubDB data using HubL

Using HubL, you can pull HubDB data as to use as structured content on website pages. Below, learn more about how to retrieve table, row, and column data using HubL.
Drafted HubDB table data will appear in the page editor and live previews, but only published HubDB content will appear on the live page. If you're seeing table data appear in the editor or preview that isn't appearing on the live page, confirm that the table has been published since adding that table data.
### Getting rows

To list rows of a table, use the [hubdb_table_rows()](/reference/cms/hubl/functions#hubdb-table-rows) HubL function. You can either access a table by its ID or name. It is recommended to reference a HubDB table by name, as this can help with code portability across HubSpot accounts. The immutable table name is set when creating a new table and can be found at any time by selecting **Actions > Manage Settings** within the table editor. A table's ID can be found in the address bar of the table editor or in the HubDB tables dashboard under the `ID` column.
Below is an example of using `hubdb_table_rows()` to fetch data.

```hubl
{% for row in hubdb_table_rows(<tableId or name>, <filterQuery>) %}
  the value for row {{ row.hs_id }} is {{ row.<column name> }}
{% endfor %}
```
By default, the maximum number of rows returned is 1,000. To retrieve more rows, specify a `limit` in the function query. For example:

`hubdb_table_rows(12345, "orderBy=random()&limit=1500")`.
`<filterQuery>` uses the same syntax as the HTTP API. For example, `hubdb_table_rows(123, "employees__gt=10&orderBy=count")` would return a list of rows where the "employees" column is greater than 10, ordered by the "count" column. A complete list of optional `<filterQuery>` parameters [can be found here](/guides/api/cms/hubdb#filter-returned-rows).

Instead of using multiple row queries with different `<filterQuery>` parameters, you should make one query and use the `selectattr()` or `rejectattr()` filters to filter your rows:

```hubl
{% set all_cars = hubdb_table_rows(<tableId or name>) %}

{% set cars_with_windows = all_cars|selectattr('windows') %}

{% set teslas = all_cars|selectattr('make','equalto','tesla') %}
```

To get a single row, use the `hubdb_table_row()` HubL function.

```hubl
{% set row = hubdb_table_row(<tableId or name>, <rowId>) %}
the value for {{ row.hs_id }} is {{ row.<column name> }}
```

Built-in and custom column names are case insensitive. `HS_ID` will work the same as `hs_id`.

#### Row attributes

| Attribute | Description |
| --- | --- |
| `row.hs_id` | The globally unique id for this row. |
| `row.hs_path` | When using dynamic pages, this string is the Page Path column value and the last segment of the url's path. |
| `row.hs_name` | When using dynamic pages, this string is the Page Title column value for the row. |
| `row.hs_created_at` | Unix timestamp for when the row was created. |
| `row.hs_child_table_id` | When using dynamic pages, this is the ID of the other table that is populating data for the row. |
| `row.column_name` | Get the value of the custom column by the name of the column. |
| `row["column name"]` | Get the value of the custom column by the name of the column. |

### Getting table metadata

To get a table's metadata, including its name, columns, last updated, etc, use the `hubdb_table()` function.

```hubl
{% set table_info = hubdb_table(<tableId or name>) %}
```

### Table attributes
The attributes listed below are in reference to the variable that `hubdb_table()` was assigned to in the above code. Your variable may differ. _Note: It is recommended assigning this to a variable for easier use. If you don't want to do that, you can use `{{ hubdb_table(<tableId>).attribute }}`_
| Attribute | Description |
| --- | --- |
| `table_info.id` | The id of the table. |
| `table_info.name` | The name of the table. |
| `table_info.columns` | List of column information. You can use a [for loop](/reference/cms/hubl/loops) to iterate through the information available in this attribute. |
| `table_info.created_at` | Timestamp of when the table was first created. |
| `table_info.published_at` | Timestamp of when this table was published. |
| `table_info.updated_at` | Timestamp of when this table was last updated. |
| `table_info.row_count` | Number of rows in the table. |

### Getting column metadata

```hubl
{% set table_info = hubdb_table_column(<tableId or name>, <columnId or column name>) %}
```

To get information on a column in table such as its label, type and options, use the `hubdb_table_column()` function

#### Column attributes
The attributes listed below are in reference to the variable that `hubdb_table_column()` was assigned to in the above code. Your variable may differ. _Note: It is recommended assigning this to a variable for easier use. If you don't want to do that, you can use `{{ hubdb_table_column(<tableId>,<columnId or column name>).attribute }}`_
| Attribute | Description |
| --- | --- |
| `table_info.id` | The ID of the column. |
| `table_info.name` | The name of the column. |
| `table_info.label` | The label to be used for the column. |
| `table_info.type` | Type of this column. |
| `table_info.options` | For select column type, this is a map of `optionId` to `option` information. |
| `table_info.foreignIds` | For foreignId column types, a list of foreignIds (with `id` and `name` properties). |

#### Column methods

| Method | Description |
| --- | --- |
| `getOptionByName("` | For select column types, get option information by the options name. |

#### Rich Text columns

The `richtext` column type functions similar to the rich text field you see for modules.

The data is stored as HTML, and the HubDB UI provides a text editing interface. However, when editing a HubDB table through HubSpot's UI, you cannot edit source code directly. This prevents situations where a non-technical user may input invalid HTML, preventing unintended issues with the appearance or functionality of your site. For situations where you need an embed code or more custom HTML you can use the embed feature in the rich text editor to place your custom code.

## HubDB tutorials and resources

- [Event Registration App](/guides/cms/overview)[](/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)[](/guides/cms/storage/hubdb/overview)


# Design Manager

The Design Manager is a web-based integrated development environment that can optionally be used to build [templates](/guides/cms/content/templates/overview), [modules](/guides/cms/content/modules/overview), CSS and JavaScript. The Design Manager can be found by navigating to [**Marketing > Files and Templates > Design Tools**](https://app.hubspot.com/l/design-manager/) in the top navigation bar.
If you prefer to develop locally using your preferred code editor, workflows and technologies you can use the [CMS CLI](/guides/cms/tools/local-development-cli) instead.
Regardless of whether you develop locally or utilize the Design Manager for building out assets, the entire developer file system of your account will be represented and editable in the Design Manager.
Some developers or teams choose to use a [workflow where a mix of the Design Manager and local development](/guides/cms/setup/creating-an-efficient-development-workflow) are used. You can utilize the [`hs fetch`](/guides/cms/tools/local-development-cli#fetch) command of the [CMS CLI](/guides/cms/tools/local-development-cli) to pull Design Manager edits down to your local environment. Alternatively, you can prevent editing and have your entire [codebase continuously integrated with source control](/guides/cms/setup/github-integration).

Check out this [Knowledge Base article](https://knowledge.hubspot.com/design-manager/a-quick-tour-of-the-design-manager) for a comprehensive overview of the Design Manager and its various components.

## Design Manager Settings

The Design Manager has basic IDE settings you can configure to make the experience fit you and your workflows. A settings button appears adjacent to the help button in the bottom bar and can be used to configure the settings.


# HubSpot CLI commands (v7)
The HubSpot <abbr title="Command Line Interface">CLI</abbr> connects your local development tools to HubSpot, allowing you to develop on the HubSpot CMS with version control, your favorite text editor, and various web development technologies.

If you're new to developing on HubSpot, check out our quick start guide where you'll walk through installing the CLI all the way to publishing a live page.
Use this guide as a reference for the available commands and file formatting options for HubSpot's local development tooling. For a walkthrough of how to use these tools, see the [getting started with local development tutorial](/guides/cms/setup/getting-started-with-local-development).
If you prefer, you can use [Yarn](https://classic.yarnpkg.com/en/docs/install) by running commands with the `yarn` prefix.
## Show all commands

Shows all commands and their definitions. To learn more about a specific command, add `--help` to the end of the command.

```shell
hs help
```

## Install the CLI

You can install HubSpot local development tools either globally (recommended) or locally. To install the HubSpot tools globally, in your command line run the command below. To install locally, omit `-g` from the command.

```shell
npm install -g @hubspot/cli
```

To install the tools only in your current directory instead, run the command below. You do not need to install locally if you already have the CLI installed globally.

```shell
npm install @hubspot/cli
```
**Getting an EACCES error when installing?** See [NPM Resolving EACCESS permissions errors when installing packages globally](https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally/).
## Update the CLI

The CLI is updated regularly. To upgrade to the latest version of the local tools, run:

```shell
npm install -g @hubspot/cli@latest
```

## Authentication

The following commands enable you to authenticate HubSpot accounts with the CLI so that you can interact with the account. If you haven't yet authenticated an account with the CLI, you'll first run `hs init` to create a `hubspot.config.yml` file, which will contain the authentication details for any connected HubSpot accounts. The rest of the commands will update that file.

Learn more in the [Getting started with local development guide](/guides/cms/setup/getting-started-with-local-development).

### Initialize config and authentication

Creates a `hubspot.config.yml` file in the current directory and sets up authentication for an account. If you're adding authentication for a new account to an existing config file, run the [auth](#auth) command. When prompted for a name to use for the account, the name can't contain spaces.

```shell
hs init [flags]
```

**Flags**

| Flag | Description |
| --- | --- |
| `--auth-type` | The authentication protocol to use for authenticating your account. Supported values are `personalaccesskey` (default) and `oauth2`. |
| `--account` | The specific account name to authenticate using the CLI. To get a full list of accounts, use the `hs accounts` command. |

### Authenticate an account

Generate authentication for a HubSpot account using a [personal access key](/guides/cms/tools/personal-access-key). You can [generate your access key here](https://app.hubspot.com/login?loginRedirectUrl=https%3A%2F%2Fapp.hubspot.com%2Fshortlink%2Fpersonal-access-key%2F). If you already have a `hubspot.config.yml` file you can use this command to add credentials for additional accounts. For example you might [use your sandbox account as a development environment.](/guides/cms/setup/creating-an-efficient-development-workflow#setting-up-your-development-environment) When prompted for a name to use for the account, the name can't contain spaces.

```shell
hs auth [flags]
```

**Flags**

| Flag | Description |
| --- | --- |
| `--auth-type` | The authentication protocol to use for authenticating your account. Supported values are `personalaccesskey` (default) and `oauth2`. |
| `--account` | The specific account name to authenticate using the CLI. To get a full list of accounts, use the `hs accounts` command. |

### List authenticated accounts

Lists the name, ID, and auth type for the each account in your config file. If you're not seeing the accounts you expect, you may need to run the [auth](#auth) command to add accounts to your config file.

```shell
hs accounts list
```

### Set default account

Set the default account in your config file.

```shell
hs accounts use accountNameOrID
```

| Parameter | Description |
| --- | --- |
| `accountNameOrID` | Identify the new default account by its name (as set in the config file) or ID. |

### Remove an account

Removes an account from your config file.

```shell
hs accounts remove accountNameOrID
```

| Parameter | Description |
| --- | --- |
| `accountNameOrID` | Identify the account to remove by its name (as set in the config file) or ID. |

### Remove invalid accounts

Removes any deactivated HubSpot accounts from your config file.

```shell
hs accounts clean
```

## Interacting with the developer file system

Using the CLI, you can interact with the [developer file system](/guides/cms/overview#developer-file-system), which is the file system in the [Design Manager](/guides/cms/tools/design-manager). These commands enable you to create new assets locally, such as modules and themes, upload them to the account, list files in the HubSpot account, or download existing files to your local environment.

### List files

List files stored in the developer file system by path or from the root. Works similar to using standard `ls` to view your current directory on your local machine.

```shell
hs ls [path]
hs list [path]
```

**Arguments**

| Argument | Description |
| --- | --- |
| `dest` | Path to the remote developer file system directory you would like to list files for. If omitted, defaults to the account root. |

### Fetch files

Fetch a file, or directory and its child folders and files, by path. Copies the files from your HubSpot account into your local environment.

By default, fetching will not overwrite existing local files. To overwrite local files, include the `--overwrite` flag.

```shell
hs fetch --account=<name> <src> [dest]
hs filemanager fetch --account=<name> <src> [dest]
```

**Arguments**

| Argument | Description |
| --- | --- |
| `src` | Path in HubSpot Design Tools |
| `dest` | Path to the local directory you would like the files to be placed, relative to your current working directory. If omitted, this argument will default to your current working directory. |

**Flags**

| Options | Description |
| --- | --- |
| `--account` | Specify an `accountId` or name to fetch fromSupports an alias of `--portal` for backward compatibility with older versions of the CLI. |
| `--overwrite` | Overwrite existing files with fetched files. |
| `--mode` | Specify if fetching a draft or published version of a file from HubSpot. [Click here](#modes) for more info |

### Upload files

Upload a new local asset to your HubSpot account. Changes uploaded through this command will be live immediately.

```shell
hs upload --account=<name> <src> <dest>
hs filemanager upload --account=<name> <src> <dest>
```

**Arguments**

| Argument | Description |
| --- | --- |
| `src` | Path to the local file, relative to your current working directory. |
| `dest` | Path in HubSpot Design Tools, can be a net new path. |

**Flags**

| Options | Description |
| --- | --- |
| `--account` | Specify a `accountId` or name to fetch from.Supports an alias of `--portal` for backward compatibility with older versions of the CLI. |
| `--mode` | Specify if uploaded files are published in HubSpot. [See "modes"](#modes) for more info. |
| `--clean` | An optional flag that will delete the destination directory and its contents before uploading. |
If you use the `--clean` flag, any associated global content configured using the [global content editor](https://knowledge.hubspot.com/design-manager/use-global-content-across-multiple-templates) will be reset to the defaults defined in your [global partials](/guides/cms/content/global-content#global-partials).
**Subcommands**

| Subcommands | Description |
| --- | --- |
| `filemanager` | Uploads the specified src directory to the [File Manager](/guides/cms/storage/file-manager), rather than to the [developer file system](/guides/cms/overview#developer-file-system) in the Design Manager.**Note**: Uploaded files will be set to _public_, making them viewable by anyone with the URL. See our [help documentation](https://knowledge.hubspot.com/files/organize-edit-and-delete-files#edit-the-file-visibility-setting) for more details on file visibility settings. |

### Set a watch for automatic upload

Watch your local directory and automatically upload changes to your HubSpot account on save. Any changes made when saving will be live immediately.

Keep the following in mind when using `watch`:

- Deleting watched files locally will not automatically delete them from HubSpot. To delete files, use `--remove`.
- Renaming a folder locally will upload a new folder to HubSpot with the new name. The existing folder in HubSpot will not be deleted automatically. To delete the folder, use `--remove`.

```shell
hs watch --account=<name> <src> <dest>
```

**Arguments**

| Argument | Description |
| --- | --- |
| `src` | Path to the local directory your files are in, relative to your current working directory. |
| `dest` | Path in HubSpot Design Tools, can be a net new path. |

**Flags**

| Flag | Description |
| --- | --- |
| `--account` | Specify a `accountId` or name to fetch fromSupports an alias of `--portal` for backward compatibility with older versions of the CLI. |
| `--mode` | Specify if uploaded files are published or saved as drafts in HubSpot. [Learn more about using modes](#modes). |
| `--initial-upload` | Upload the directory before watching for updates. Supports an alias of `-i`. |
| `--remove` | Will cause watch to delete files in your HubSpot account that are not found locally. |
| `--notify=` | log to specified file when a watch task is triggered and after workers have gone idle. |

### Move files

Moves files within the [developer file system](/guides/cms/overview#developer-file-system) from one directory to another. Does not affect files stored locally.

```shell
hs mv --account=<name> <src> <dest>
```

**Arguments**

| Argument | Description |
| --- | --- |
| `src` | Path to the remote developer file system directory your files are in. |
| `dest` | Path to move assets to within the developer file system. |

**Flags**

| Flag | Description |
| --- | --- |
| `--account` | Specify a `accountId` or name to move files within. Supports an alias of `--portal` for backward compatibility with older versions of the CLI. |

### Create new files

Creates the folder/file structure of a new asset.

```shell
hs create <type> <name> [dest]
```

**Arguments**

| Argument | Description |
| --- | --- |
| `type` |
| `name` | The name of the new asset. |
| `dest` | The destination folder for the new asset, relative to your current working directory. If omitted, this will default to your current working directory. |

### Remove files

Deletes files, or folders and their files, from your HubSpot account. This does <u>not</u> delete the files and folders stored locally. This command has an alias of `rm`.

```shell
hs remove --account=<name> <path>
```

**Arguments**

| Argument | Description |
| --- | --- |
| `path` | The path of the file or folder in HubSpot's developer file system. |

**Flags**

| Flag | Description |
| --- | --- |
| `--account` | Specify a `accountId` or name to remove a file from.Supports an alias of `--portal` for backward compatibility with older versions of the CLI. |

### Ignore files

You can include a `.hsignore` file to specify files that should not be tracked when using the CLI. This file functions similar to how `.gitignore` files work. Files matching the patterns specified in the `.hsignore` file will not be uploaded to HubSpot when using the [`upload`](/guides/cms/tools/local-development-cli#upload) or [`watch`](/guides/cms/tools/local-development-cli#watch) commands.
By default there are some rules HubSpot automatically enforces. There is no way to override these defaults.

The following are always ignored:

- `hubspot.config.yml`/`hubspot.config.yaml`
- `node_modules` - dependencies
- `.*` - hidden files/folders
- `*.log` - NPM error log
- `*.swp` - Swap file for Vim state
- `Icon\\r` - Mac OS custom Finder icon
- `__MACOSX` - Mac resource fork
- `~` Linux Backup file
- `Thumbs.db` - Windows image file cache
- `ehthumbs.db` - Windows folder config file
- `Desktop.ini` - Windows custom folder attribute information
- `@eaDir` - Windows Synology diskstation "hidden" folder where the server stores thumbnails.
```shell
# ignore all files within a specific directory
/ignore/ignored
# ignore a specific file
/ignore/ignore.md
# ignore all .txt files
*.txt
# ignore all log files - useful if you commonly output serverless function logs as files.
*.log
```

## Locally preview theme

When developing a theme, you can run `hs theme preview` in the theme's root directory to render a live preview of your changes without uploading files to the account. The preview will run on a local proxy server at [https://hslocal.net:3000/](https://hslocal.net:3000/).

Once run, this command will run a watch process so that any saved changes are rendered in the preview.
To allow the local server to run on https, HubSpot must generate a self-signed SSL certificate and register it with your operating system. This will require entering your sudo password.
```shell
hs theme preview <src> <dest>
```

**Arguments**

| Argument | Description |
| --- | --- |
| `src` | Path to the local file, relative to your current working directory. This command should be run in the theme's root directory.. |
| `dest` | The path for the preview. This can be any value, and is only used internally and for display purposes on the preview page. |

The main page at [https://hslocal.net:3000/](https://hslocal.net:3000/) will display a list of your theme's templates and modules, all of which can be individually previewed by clicking the provided links. You'll also see a list of the account's connected domains, which you can use to preview content on specific domains. The domain will be prepended to the `hslocal.net` domain.
## HubDB Commands
The HubDB commands are currently in Developer Preview. They are available to use now but understand they are subject to change. Developer previews are subject to our [developer beta terms](https://legal.hubspot.com/hubspot-beta-terms).
Use these commands to create, delete, fetch, and clear all rows of a HubDB table. The HubSpot account must have access to HubDB to use these commands.

### Create HubDB table

Create a new HubDB table in the HubSpot account.

```shell
hs hubdb create --path [path] --account [account]
```

**Flags**

| Flag | Description |
| --- | --- |
| `--path` | The local [JSON file](#hubdb-table-json) to use to generate the HubDB table. |
| `--account` | Specify a `accountId` or name to create HubDB in. Supports an alias of `--portal` for backward compatibility with older versions of the CLI. |

### Fetch HubDB Table

Download a HubDB table's data to your local machine.

```shell
hs hubdb fetch <table-id> <dest>
```

**Arguments**

| Argument | Description |
| --- | --- |
| `table-id` | HubDB table id found in the HubDB dashboard. |
| `dest` | The local path destination to store the [`hubdb.json`](#hubdb-table-json) file. |

When you fetch a HubDB the data is stored as `tablename.hubdb.json`. When you create a new table you must specify a source JSON file. Below is an example of a table in JSON format.

```json
// store_locations.hubdb.json
{
  "name": "store_locations",
  "useForPages": true,
  "label": "Store locations",
  "allowChildTables": false,
  "allowPublicApiAccess": true,
  "dynamicMetaTags": { "DESCRIPTION": 3, "FEATURED_IMAGE_URL": 7 },
  "enableChildTablePages": false,
  "columns": [
    { "name": "name", "label": "Name", "type": "TEXT" },
    {
      "name": "physical_location",
      "label": "Physical Location",
      "type": "LOCATION"
    },
    { "name": "street_address", "label": "Street address", "type": "TEXT" },
    { "name": "city", "label": "City", "type": "TEXT" },
    {
      "name": "state",
      "label": "State",
      "options": [
        { "id": 1, "name": "Wisconsin", "type": "option", "order": null },
        { "id": 2, "name": "Minnesota", "type": "option", "order": null },
        { "id": 3, "name": "Maine", "type": "option", "order": null },
        { "id": 4, "name": "New York", "type": "option", "order": null },
        { "id": 5, "name": "Massachusetts ", "type": "option", "order": null },
        { "id": 6, "name": "Mississippi", "type": "option", "order": null },
        { "id": 7, "name": "Arkansas", "type": "option", "order": null },
        { "id": 8, "name": "Texas", "type": "option", "order": null },
        { "id": 9, "name": "Florida", "type": "option", "order": null },
        { "id": 10, "name": "South Dakota", "type": "option", "order": null },
        { "id": 11, "name": "North Dakota", "type": "option", "order": null },
        { "id": 12, "name": "n/a", "type": "option", "order": null }
      ],
      "type": "SELECT",
      "optionCount": 12
    },
    { "name": "phone_number", "label": "Phone Number", "type": "TEXT" },
    { "name": "photo", "label": "Store Photo", "type": "IMAGE" }
  ],
  "rows": [
    {
      "path": "super_store",
      "name": "Super Store",
      "isSoftEditable": false,
      "values": {
        "name": "Super Store",
        "physical_location": {
          "lat": 43.01667,
          "long": -88.00608,
          "type": "location"
        },
        "street_address": "1400 75th Greenfield Ave",
        "city": "West Allis",
        "state": { "id": 1, "name": "Wisconsin", "type": "option", "order": 0 },
        "phone_number": "(123) 456-7890"
      }
    },
    {
      "path": "store_123",
      "name": "Store #123",
      "isSoftEditable": false,
      "values": {
        "name": "Store #123",
        "physical_location": {
          "lat": 32.094803,
          "long": -166.85889,
          "type": "location"
        },
        "street_address": "Pacific Ocean",
        "city": "at sea",
        "state": { "id": 12, "name": "n/a", "type": "option", "order": 11 },
        "phone_number": "(123) 456-7891"
      }
    }
  ]
}
```

### Clear rows in a HubDB table

Clear all of the rows in a HubDB table.

```shell
hs hubdb clear <tableId>
```

**Arguments**

| Argument | Description |
| --- | --- |
| `tableId` | HubDB table id found in the HubDB dashboard. |

**Flags**

| Flag | Description |
| --- | --- |
| `--account` | Specify a `accountId` or name to clear HubDB rows from.Supports an alias of `--portal` for backward compatibility with older versions of the CLI. |

### Delete HubDB table

Deletes the specified HubDB table from the account. You will be prompted to confirm the deletion before proceeding. You can use the `--force` flag to bypass this confirmation.

```shell
hs hubdb delete <table-id>
```

**Arguments**

| Argument | Description |
| --- | --- |
| `table-id` | HubDB table ID found in the HubDB dashboard. |

**Flags**

| Flag | Description |
| --- | --- |
| `--account` | Specify a `accountId` or name to delete HubDB from. Supports an alias of `--portal` for backward compatibility with older versions of the CLI. |
| `--force` | Bypass the confirmation prompt and immediately delete the table once the command is executed. |

## Serverless function commands

Use these commands to create and debug [serverless functions](/guides/cms/content/data-driven-content/serverless-functions/overview) (**_CMS Hub_** _Enterprise_ only).

### Create a function

Creates a serverless function using the [create](#create) command. Running this command will guide you through the steps of creating the function, such as naming its parent and function file and defining its methods and endpoint path.

```shell
hs create function
```

### List functions

Prints a list of all of the account's deployed functions, their endpoints, methods, the names of the secrets they use and last updated date.

```shell
hs functions ls --account=<name>
hs functions list --account=<name>
```

**Arguments**

| Argument | Description |
| --- | --- |
| `--account` | The HubSpot account nickname from your hubspot.config. This parameter is required if you do not have a [defaultAccount](/guides/cms/tools/local-development-cli#top-level-parameters) in your `hubspot.config`.Supports an alias of `--portal` for backward compatibility with older versions of the CLI. |
| `--json` | Output JSON into the command line with data on all of the functions. The JSON data includes, portal id, function id, route, raw asset path, method, secrets, created and last modified dates. |

### Get logs

Prints a log from your serverless function. Displays any `console.logs` contained within your function after execution. Logs also include execution time. Logs are retained for 90 days.

```shell
hs logs <endpoint-name> --account=<name> --follow
```

**Arguments**

| Argument | Description |
| --- | --- |
| `endpoint-name` | The endpoint name as defined in your `serverless.json` file (not the path to the function file). |
| `--file` | Output the logs to `function.log`. |
| `--follow` | Tail the logs to get a live update as you are executing your serverless functions. |
| `--latest` | Output only the most recent log. |
| `--account` | The HubSpot account nickname from your hubspot.config. This parameter is required if you do not have a [defaultPortal](/guides/cms/tools/local-development-cli#top-level-parameters) in your hubspot.config.Supports an alias of `--portal` for backward compatibility with older versions of the CLI. |
| `--compact` | Hides log output. Returns success/error and execution time. |
| `--limit=` | limit the amount of logs displayed in the output. |
If you receive this error: `A server error occurred: WARNING: The logs for this function have exceeded the 4KB limit`, your log is too large. This can be caused by trying to console log a very large object, or by a lot of separate console logs. To resolve this, reduce how much you're trying to log, hit your endpoint, then run the command again.
### Add a secret

Add a [secret](/guides/cms/content/data-driven-content/serverless-functions/overview#secrets) to your account which can be used within serverless functions. After running the command you will be prompted to enter the secret's value.

To expose the secret to your function, update your [`serverless.json`](/reference/cms/serverless-functions#serverless-json) file with the secret's name, either to the specific endpoints you want to use it in or globally to make it available to all.

```shell
hs secrets add <secret-name>
```

**Arguments**

| Argument                                      | Description                |
| --------------------------------------------- | -------------------------- |
| `secret-name` | Name of the secret to add. |

### Update a secret

Update the value of a [secret](/guides/cms/content/data-driven-content/serverless-functions/overview#secrets) in your account which can be used within serverless functions. You will then be prompted to enter the secret's value.
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.
```shell
hs secrets update <secret-name>
```

**Arguments**

| Argument | Description |
| --- | --- |
| `secret-name` | The name of the secret, which you'll later use to reference the secret. This can be any unique value, though it's recommended to keep it simple for ease of use. |

### Remove a secret

Remove a [secret](/guides/cms/content/data-driven-content/serverless-functions/overview#secrets) from your account, making it no longer usable within serverless functions. After running this command, edit your [`serverless.json`](/reference/cms/serverless-functions#serverless-json) file to remove the secret's name. You will be prompted to confirm the deletion before proceeding. You can use the `--force` flag to bypass this confirmation.

```shell
hs secrets delete <secret-name>
```

**Arguments**

| Argument | Description |
| --- | --- |
| `secret-name` | Name of secret you want to remove. |

**Flags**

| Flag | Description |
| --- | --- |
| `--force` | Bypass the confirmation prompt and immediately delete the table once the command is executed. |

### List secrets

List [secrets](/guides/cms/content/data-driven-content/serverless-functions/overview#secrets) within your account to know what you have stored already using the add secrets command.

```shell
hs secrets list
```

## Open browser shortcuts

There are so many parts of the HubSpot app that developers need to access frequently. To make it easier to get to these tools you can open them directly from the command line. Your `defaultAccount` or `--account` argument will be used to open the associated tool for that account.

### open

```shell
hs open <shortcut-name or alias>
```

**Arguments**

| Argument | Description |
| --- | --- |
| `shortcut` | Provide the full shortcut name or alias of the short cut you wish to open in your browser. |

```shell
hs open --list
```

**Arguments**

| Argument | Description |
| --- | --- |
| `--list` | Lists all of the shortcuts, their aliases and destinations. |

## Command completion

If you use the CLI frequently, it can be useful to be-able-to tab to auto-complete commands.

```shell
hs completion >> ~/.bashrc
```

For Mac OS X

```shell
hs completion >> ~/.bash_profile
```

## Evaluate themes and templates for SEO and accessibility

Uses [Google's Lighthouse tools](https://developer.chrome.com/docs/lighthouse/overview) to score the quality of your themes and templates for their adherence to the following categories:

- Accessibility
- Web best practices
- Performance
- PWA
- SEO

The following types of templates are scored:

- landing pages
- website pages
- Blog posts
- Blog listing page

If any templates fail to generate a score because of Lighthouse errors, a list of these templates will be provided.

```shell
hs cms lighthouse-score --theme=path
```

**Flags**

| Flag | Description |
| --- | --- |
| `--theme-path` | Path to a theme in the Design Manager. |
| `--verbose` | <ul><li>When this parameter is excluded, the returned score is an average of all the theme's templates (default).</li><li>When this parameter is included, the individual template scores are shown. You'll also receive [Lighthouse report](https://developer.chrome.com/docs/lighthouse/overview/#report-viewer) links for each template.</li></ul> |
| `--target` | This can either be desktop or mobile to see respective scores. By default, the target is desktop. |

### Retrieve an existing React theme

To fetch an existing React theme from your account, use the following command:

```shell
hs cms get-react-module <name> <dest>
```

**Arguments**

| Argument | Description                                                      |
| -------- | ---------------------------------------------------------------- |
| `name`   | The name of the module to download.                              |
| `dest`   | The destination on your local machine to download the module to. |

## Generate theme field selectors for in-app highlighting

When creating a theme, use the following command to generate an `editor-preview.json` file which maps CSS selectors to theme fields. This enables content creators to see which theme elements will be impacted by updates to a field's styling options.

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 this command will make a rudimentary guess as to 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. Learn more about [theme editor field highlighting](/guides/cms/content/fields/overview#theme-editor-field-highlighting).

```shell
hs theme generate-selectors <themePath>
```

## Modes

The `\--mode` option allows you to determine if local changes are published when uploaded to HubSpot. This option can be used in each command or set as a default in your `hubspot.config.yml` file.

The two options for `\--mode` are `\--mode=draft` and `\--mode=publish.`

The following is the order of precedence for setting `\--mode`:

1.  Using `\--mode` in a command will override all other settings.
2.  Setting a `defaultMode` for each account in your `hubspot.config.yml file`, removes the need to use `\--mode` in each command. It will override the top-level setting.
3.  Setting a `defaultMode` at the top-level in your `hubspot.config.yml file`, sets a default`\--mode` for all accounts. It will override the default behavior.
4.  The default behavior for `\--mode` is `publish`.

## Environment variables

The HubSpot CLI supports the use of environment variables, this can be especially useful when creating automations like a GitHub Action.

Run any command using the `--use-env` flag to use the environment variables instead of the `hubspot.config.yml`.

```shell
hs upload example-project example-project-remote --use-env
```

| Name | Description |
| --- | --- |
| `HUBSPOT_ACCOUNT_ID` | The HubSpot account ID. |
| `HUBSPOT_PERSONAL_ACCESS_KEY` | The [personal access key](/guides/cms/tools/personal-access-key) of a user on the HubSpot account. All updates made will be associated to this user. |
| `HUBSPOT_CLIENT_ID` | The OAuth client ID. |
| `HUBSPOT_CLIENT_SECRET` | The OAuth secret. |
As of November 30, 2022, HubSpot API Keys 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.
## Marketplace asset validation

The CLI provides a suite of automated tests you can perform on your assets to get them in-line with the marketplace requirements prior to submitting. Passing all automated tests does not mean you will for sure pass the review process, further review is conducted to ensure quality beyond what can be easily automated.

### Validate theme

The theme validation command allows you to quickly run automated tests on your theme to identify problems that need to be fixed prior to submission to the asset marketplace. These will be returned in your CLI as a list of \[error\] and \[success\] messages separated into groups that represent types of assets within a theme.

Before you can validate a theme, you'll first need to upload it to your account with `hs upload`. Then, run the following command to validate the uploaded theme.

```shell
hs theme marketplace-validate <path>
```

**Arguments**

| Argument | Description |
| --- | --- |
| `path` | Root relative path to the theme folder in the design manager. |

### Validate module

Similar to validating a theme, this command allows you to quickly run automated tests on a module to identify problems that need to be fixed prior to submission to the asset marketplace.

Before you can validate a module, you'll first need to upload it to your account with `hs upload`. Then, run the following command to validate the uploaded module.

```shell
hs module marketplace-validate <src>
```

**Arguments**

| Argument | Description |
| --- | --- |
| `src` | Root relative path to the module folder in the design manager. |

## Custom objects
The custom object commands are currently in beta. They are available to use now but understand they are subject to change. Developer previews are subject to our [developer beta terms](https://legal.hubspot.com/hubspot-beta-terms).
Manage custom objects using the `schema` subcommand to manage custom object schemas and the `create` subcommand to create a new custom object.

### Fetch schema for a single custom object

To fetch the schema for an existing custom object, run the following command:

```shell
hs custom-object schema fetch <name> <dest>
```

**Arguments**

| Argument | Description                                                  |
| -------- | ------------------------------------------------------------ |
| `name`   | The name of the custom object to fetch the schema for.       |
| `dest`   | The destination on your local machine to save the schema to. |

### Fetch schema for all custom objects

Fetch the schemas for all custom objects in an account.

```shell
hs custom-object schema fetch-all <dest>
```

**Arguments**

| Argument | Description                                                   |
| -------- | ------------------------------------------------------------- |
| `dest`   | The destination on your local machine to save the schemas to. |

### Update the schema for a custom object

Update the schema for an existing custom object with the definition at the provided path.

```shell
hs custom-object schema update --path=definition
```

**Flags**

| Flag     | Description                                                    |
| -------- | -------------------------------------------------------------- |
| `--path` | The path to a schema definition located on your local machine. |

### Delete the schema for a custom object

Delete the schema for an existing custom object. You will be prompted to confirm the deletion before proceeding. You can use the `--force` flag to bypass this confirmation.

```shell
hs custom-object schema delete <name>
```

**Arguments**

| Flag   | Description                                     |
| ------ | ----------------------------------------------- |
| `name` | The name of the custom object schema to delete. |

### Create a new custom object

Create a new custom object with the provided definition for its schema.

```shell
hs custom-object create <name> --path=definition
```

**Arguments**

| Flag   | Description                                |
| ------ | ------------------------------------------ |
| `name` | The name of your new custom object schema. |

**Flags**

| Flag     | Description                                                    |
| -------- | -------------------------------------------------------------- |
| `--path` | The path to a schema definition located on your local machine. |

## Projects and sandboxes (BETA)

If you're building UI extensions for a [private app](https://developers.hubspot.com/docs/guides/crm/private-apps/quickstart) or [public app](https://developers.hubspot.com/docs/guides/crm/public-apps/quickstart), or you're building CMS React modules, you can use the HubSpot CLI to manage your projects.

A full reference of project-related commands, as well as any commands related to managing your sandboxes, can be found in [this article](https://developers.hubspot.com/docs/guides/crm/developer-projects/project-cli-commands).


# HubSpot CLI commands (v6)

Use this guide as a reference to the commands available in v6 of the HubSpot CLI.
A new version of the HubSpot CLI, v7.0, is now available. It's strongly recommended that you upgrade to the latest version by running `npm install -g @hubspot/cli@latest`. Learn more about using [CLI v7.0](/guides/cms/tools/hubspot-cli/cli-v7).
If you prefer, you can use [Yarn](https://classic.yarnpkg.com/en/docs/install) by running commands with the `yarn` prefix.
## Show all commands

Shows all commands and their definitions. To learn more about a specific command, add `--help` to the end of the command.

```shell
hs help
```

## Install the CLI

You can install HubSpot local development tools either globally (recommended) or locally. To install the HubSpot tools globally, in your command line run the command below. To install locally, omit `-g` from the command.

```shell
npm install -g @hubspot/cli
```

### Install to just the current directory

To install the tools only in your current directory instead, run the command below. You do not need to install locally if you already have the CLI installed globally.

```shell
npm install @hubspot/cli
```
**Getting an EACCES error when installing?** See [NPM Resolving EACCESS permissions errors when installing packages globally](https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally/).
## Update the CLI

The CLI is updated regularly. To upgrade to the latest version of the local tools, run:

```shell
npm install -g @hubspot/cli@latest
```
The CLI changed from `@hubspot/cms-cli` to `@hubspot/cli`. If you are still using the old cms-cli you will need to uninstall it prior to installing the new version.

To see which version you're on, run `hs --version`

If your version number is less than 3.0.0, you're on the old version.

To uninstall the old version run `npm uninstall -g @hubspot/cms-cli`
## Authentication

The following commands enable you to authenticate HubSpot accounts with the CLI so that you can interact with the account. If you haven't yet authenticated an account with the CLI, you'll first run `hs init` to create a `hubspot.config.yml` file, which will contain the authentication details for any connected HubSpot accounts. The rest of the commands will update that file.

Learn more in the [Getting started with local development guide](/guides/cms/setup/getting-started-with-local-development).

### init

Creates your `hubspot.config.yml` file in the current directory and sets up authentication for an account. If you're adding authentication for a new account to an existing config file, run the [auth](#auth) command. When prompted for a name to use for the account, the name can't contain spaces.

```shell
hs init
```

### Authenticate an account

Generate authentication for a HubSpot account using a [personal access key](/guides/cms/tools/personal-access-key). You can [generate your access key here](https://app.hubspot.com/login?loginRedirectUrl=https%3A%2F%2Fapp.hubspot.com%2Fshortlink%2Fpersonal-access-key%2F). If you already have a `hubspot.config.yml` file you can use this command to add credentials for additional accounts. For example you might [use your sandbox account as a development environment.](/guides/cms/setup/creating-an-efficient-development-workflow#setting-up-your-development-environment) When prompted for a name to use for the account, the name can't contain spaces.

```shell
hs auth
```

### List authenticated accounts

Lists the name, ID, and auth type for the each account in your config file. If you're not seeing the accounts you expect, you may need to run the [auth](#auth) command to add accounts to your config file.

```shell
hs accounts list
```

### Set default account

Set the default account in your config file.

```shell
hs accounts use accountNameOrID
```

| Parameter | Description |
| --- | --- |
| `accountNameOrID` | Identify the new default account by its name (as set in the config file) or ID. |

### Remove an account

Removes an account from your config file.

```shell
hs accounts remove accountNameOrID
```

| Parameter | Description |
| --- | --- |
| `accountNameOrID` | Identify the account to remove by its name (as set in the config file) or ID. |

### Remove invalid accounts

Removes any deactivated HubSpot accounts from your config file.

```shell
hs accounts clean
```

## Interacting with the developer file system

Using the CLI, you can interact with the [developer file system](/guides/cms/overview#developer-file-system), which is the file system in the [Design Manager](/guides/cms/tools/design-manager). These commands enable you to create new assets locally, such as modules and themes, upload them to the account, list files in the HubSpot account, or download existing files to your local environment.

### List files

List files stored in the developer file system by path or from the root. Works similar to using standard `ls` to view your current directory on your computer.

```shell
hs ls [path]
hs list [path]
```

| Argument | Description |
| --- | --- |
| `dest` | Path to the remote developer file system directory you would like to list files for. If omitted, defaults to the account root. |

### Fetch files

Fetch a file, or directory and its child folders and files, by path. Copies the files from your HubSpot account into your local environment.

By default, fetching will not overwrite existing local files. To overwrite local files, include the `--overwrite` flag.

```shell
hs fetch --account=<name> <src> [dest]
hs filemanager fetch --account=<name> <src> [dest]
```

| Argument | Description |
| --- | --- |
| `src` | Path in HubSpot Design Tools |
| `dest` | Path to the local directory you would like the files to be placed, relative to your current working directory. If omitted, this argument will default to your current working directory. |

| Options | Description |
| --- | --- |
| `--account` | Specify an `accountId` or name to fetch fromSupports an alias of `--portal` for backward compatibility with older versions of the CLI. |
| `--overwrite` | Overwrite existing files with fetched files. |
| `--mode` | Specify if fetching a draft or published version of a file from HubSpot. [Click here](#modes) for more info |

### Upload files

Upload a new local asset to your HubSpot account. Changes uploaded through this command will be live immediately.

```shell
hs upload --account=<name> <src> <dest>
hs filemanager upload --account=<name> <src> <dest>
```

| Argument | Description |
| --- | --- |
| `src` | Path to the local file, relative to your current working directory. |
| `dest` | Path in HubSpot Design Tools, can be a net new path. |

| Options | Description |
| --- | --- |
| `--account` | Specify a `accountId` or name to fetch from.Supports an alias of `--portal` for backward compatibility with older versions of the CLI. |
| `--mode` | Specify if uploaded files are published in HubSpot. [See "modes"](#modes) for more info. |
| `--clean` | An optional flag that will delete the destination directory and its contents before uploading. |
If you use the `--clean` flag, any associated global content configured using the [global content editor](https://knowledge.hubspot.com/design-manager/use-global-content-across-multiple-templates) will be reset to the defaults defined in your [global partials](/guides/cms/content/global-content#global-partials).
| Subcommands | Description |
| --- | --- |
| `filemanager` | Uploads the specified src directory to the [File Manager](/guides/cms/storage/file-manager), rather than to the [developer file system](/guides/cms/overview#developer-file-system) in the Design Manager.**Note**: Uploaded files will be set to _public_, making them viewable by anyone with the URL. See our [help documentation](https://knowledge.hubspot.com/files/organize-edit-and-delete-files#edit-the-file-visibility-setting) for more details on file visibility settings. |

### Set a watch for automatic upload

Watch your local directory and automatically upload changes to your HubSpot account on save. Any changes made when saving will be live immediately.

Keep the following in mind when using `watch`:

- Deleting watched files locally will not automatically delete them from HubSpot. To delete files, use `--remove`.
- Renaming a folder locally will upload a new folder to HubSpot with the new name. The existing folder in HubSpot will not be deleted automatically. To delete the folder, use `--remove`.

```shell
hs watch --account=<name> <src> <dest>
```

| Argument | Description |
| --- | --- |
| `src` | Path to the local directory your files are in, relative to your current working directory |
| `dest` | Path in HubSpot Design Tools, can be a net new path. |

| Options | Description |
| --- | --- |
| `--account` | Specify a `accountId` or name to fetch fromSupports an alias of `--portal` for backward compatibility with older versions of the CLI. |
| `--mode` | Specify if uploaded files are published or saved as drafts in HubSpot. [Learn more about using modes](#modes). |
| `--initial-upload` | Causes an initial upload to occur before file saves have occured. Supports an alias of `-i` |
| `--remove` | Will cause watch to delete files in your HubSpot account that are not found locally. |
| `--notify=` | log to specified file when a watch task is triggered and after workers have gone idle. |

### Move files

Moves files within the [developer file system](/guides/cms/overview#developer-file-system) from one directory to another. Does not affect files stored locally.

```shell
hs mv --account=<name> <src> <dest>
```

| Argument | Description |
| --- | --- |
| `src` | Path to the remote developer file system directory your files are in. |
| `dest` | Path to move assets to within the developer file system. |

| Options | Description |
| --- | --- |
| `--account` | Specify a `accountId` or name to move files within.Supports an alias of `--portal` for backward compatibility with older versions of the CLI. |

### Create new files

Creates the folder/file structure of a new asset.

```shell
hs create <type> <name> [dest]
```

| Argument | Description |
| --- | --- |
| `type` |
| `name` | The name of the new asset |
| `dest` | The destination folder for the new asset, relative to your current working directory. If omitted, this will default to your current working directory. |

### Remove files

Deletes files, or folders and their files, from your HubSpot account. This does <u>not</u> delete the files and folders stored locally. This command has an alias of `rm`.

```shell
hs remove --account=<name> <path>
```

| Argument                               | Description                  |
| -------------------------------------- | ---------------------------- |
| `path` | Path in HubSpot Design Tools |

| Options | Description |
| --- | --- |
| `--account` | Specify a `accountId` or name to remove a file from.Supports an alias of `--portal` for backward compatibility with older versions of the CLI. |

### Ignore files

You can include a `.hsignore` file to specify files that should not be tracked when using the CLI. This file functions similar to how `.gitignore` files work. Files matching the patterns specified in the `.hsignore` file will not be uploaded to HubSpot when using the [`upload`](/guides/cms/tools/local-development-cli#upload) or [`watch`](/guides/cms/tools/local-development-cli#watch) commands.
By default there are some rules HubSpot automatically enforces. There is no way to override these defaults.

The following are always ignored:

- `hubspot.config.yml`/`hubspot.config.yaml`
- `node_modules` - dependencies
- `.*` - hidden files/folders
- `*.log` - NPM error log
- `*.swp` - Swap file for Vim state
- `Icon\\r` - Mac OS custom Finder icon
- `__MACOSX` - Mac resource fork
- `~` Linux Backup file
- `Thumbs.db` - Windows image file cache
- `ehthumbs.db` - Windows folder config file
- `Desktop.ini` - Windows custom folder attribute information
- `@eaDir` - Windows Synology diskstation "hidden" folder where the server stores thumbnails.
```shell
# ignore all files within a specific directory
/ignore/ignored
# ignore a specific file
/ignore/ignore.md
# ignore all .txt files
*.txt
# ignore all log files - useful if you commonly output serverless function logs as files.
*.log
```

## Locally preview theme

When developing a theme, you can run `hs theme preview` in the theme's root directory to render a live preview of your changes without uploading files to the account. The preview will run on a local proxy server at [https://hslocal.net:3000/](https://hslocal.net:3000/).

Once run, this command will run a watch process so that any saved changes are rendered in the preview.
To allow the local server to run on https, HubSpot must generate a self-signed SSL certificate and register it with your operating system. This will require entering your sudo password.
```shell
hs theme preview <src> <dest>
```

| Argument | Description |
| --- | --- |
| `src` | Path to the local file, relative to your current working directory. This command should be run in the theme's root directory.. |
| `dest` | The path for the preview. This can be any value, and is only used internally and for display purposes on the preview page. |

The main page at [https://hslocal.net:3000/](https://hslocal.net:3000/) will display a list of your theme's templates and modules, all of which can be individually previewed by clicking the provided links. You'll also see a list of the account's connected domains, which you can use to preview content on specific domains. The domain will be prepended to the `hslocal.net` domain.
## HubDB Commands
The HubDB commands are currently in Developer Preview. They are available to use now but understand they are subject to change. Developer previews are subject to our [developer beta terms](https://legal.hubspot.com/hubspot-beta-terms).
Use these commands to create, delete, fetch, and clear all rows of a HubDB table. The HubSpot account must have access to HubDB to use these commands.

### Create HubDB table

Create a new HubDB table in the HubSpot account.

```shell
hs hubdb create <src>
```

| Argument | Description |
| --- | --- |
| `src` | The local [JSON file](#hubdb-table-json) to use to generate the HubDB table. |

| Options | Description |
| --- | --- |
| `--account` | Specify a `accountId` or name to create HubDB in.Supports an alias of `--portal` for backward compatibility with older versions of the CLI. |

### Fetch HubDB Table

Download a HubDB table's data to your computer.

```shell
hs hubdb fetch <tableId> <dest>
```

| Argument | Description |
| --- | --- |
| `tableId` | HubDB table id found in the HubDB dashboard. |
| `dest` | The local path destination to store the [`hubdb.json`](#hubdb-table-json) file. |

When you fetch a HubDB the data is stored as `tablename.hubdb.json`. When you create a new table you must specify a source JSON file. Below is an example of a table in JSON format.

```json
// store_locations.hubdb.json
{
  "name": "store_locations",
  "useForPages": true,
  "label": "Store locations",
  "allowChildTables": false,
  "allowPublicApiAccess": true,
  "dynamicMetaTags": { "DESCRIPTION": 3, "FEATURED_IMAGE_URL": 7 },
  "enableChildTablePages": false,
  "columns": [
    { "name": "name", "label": "Name", "type": "TEXT" },
    {
      "name": "physical_location",
      "label": "Physical Location",
      "type": "LOCATION"
    },
    { "name": "street_address", "label": "Street address", "type": "TEXT" },
    { "name": "city", "label": "City", "type": "TEXT" },
    {
      "name": "state",
      "label": "State",
      "options": [
        { "id": 1, "name": "Wisconsin", "type": "option", "order": null },
        { "id": 2, "name": "Minnesota", "type": "option", "order": null },
        { "id": 3, "name": "Maine", "type": "option", "order": null },
        { "id": 4, "name": "New York", "type": "option", "order": null },
        { "id": 5, "name": "Massachusetts ", "type": "option", "order": null },
        { "id": 6, "name": "Mississippi", "type": "option", "order": null },
        { "id": 7, "name": "Arkansas", "type": "option", "order": null },
        { "id": 8, "name": "Texas", "type": "option", "order": null },
        { "id": 9, "name": "Florida", "type": "option", "order": null },
        { "id": 10, "name": "South Dakota", "type": "option", "order": null },
        { "id": 11, "name": "North Dakota", "type": "option", "order": null },
        { "id": 12, "name": "n/a", "type": "option", "order": null }
      ],
      "type": "SELECT",
      "optionCount": 12
    },
    { "name": "phone_number", "label": "Phone Number", "type": "TEXT" },
    { "name": "photo", "label": "Store Photo", "type": "IMAGE" }
  ],
  "rows": [
    {
      "path": "super_store",
      "name": "Super Store",
      "isSoftEditable": false,
      "values": {
        "name": "Super Store",
        "physical_location": {
          "lat": 43.01667,
          "long": -88.00608,
          "type": "location"
        },
        "street_address": "1400 75th Greenfield Ave",
        "city": "West Allis",
        "state": { "id": 1, "name": "Wisconsin", "type": "option", "order": 0 },
        "phone_number": "(123) 456-7890"
      }
    },
    {
      "path": "store_123",
      "name": "Store #123",
      "isSoftEditable": false,
      "values": {
        "name": "Store #123",
        "physical_location": {
          "lat": 32.094803,
          "long": -166.85889,
          "type": "location"
        },
        "street_address": "Pacific Ocean",
        "city": "at sea",
        "state": { "id": 12, "name": "n/a", "type": "option", "order": 11 },
        "phone_number": "(123) 456-7891"
      }
    }
  ]
}
```

### Clear rows in a HubDB table

Clear all of the rows in a HubDB table.

```shell
hs hubdb clear <tableId>
```

| Argument | Description |
| --- | --- |
| `tableId` | HubDB table id found in the HubDB dashboard. |

| Options | Description |
| --- | --- |
| `--account` | Specify a `accountId` or name to clear HubDB rows from.Supports an alias of `--portal` for backward compatibility with older versions of the CLI. |

### Delete HubDB table

Deletes the specified HubDB table from the account.

```shell
hs hubdb delete <tableId>
```

| Argument | Description |
| --- | --- |
| `tableId` | HubDB table id found in the HubDB dashboard. |

| Options | Description |
| --- | --- |
| `--account` | Specify a `accountId` or name to delete HubDB from.Supports an alias of `--portal` for backward compatibility with older versions of the CLI. |

## Serverless function commands

Use these commands to create and debug [serverless functions](/guides/cms/content/data-driven-content/serverless-functions/overview) (**_CMS Hub_** _Enterprise_ only).

### Create a function

Creates a serverless function using the [create](#create) command. Running this command will guide you through the steps of creating the function, such as naming its parent and function file and defining its methods and endpoint path.

```shell
hs create function
```

### List functions

Prints a list of all of the account's deployed functions, their endpoints, methods, the names of the secrets they use and last updated date.

```shell
hs functions ls --account=<name>
hs functions list --account=<name>
```

| Argument | Description |
| --- | --- |
| `--account` | The HubSpot account nickname from your hubspot.config. This parameter is required if you do not have a [defaultAccount](/guides/cms/tools/local-development-cli#top-level-parameters) in your `hubspot.config`.Supports an alias of `--portal` for backward compatibility with older versions of the CLI. |
| `--json` | Output JSON into the command line with data on all of the functions. The JSON data includes, portal id, function id, route, raw asset path, method, secrets, created and last modified dates. |

### Get logs

Prints a log from your serverless function. Displays any `console.logs` contained within your function after execution. Logs also include execution time. Logs are retained for 90 days.

```shell
hs logs <endpoint-name> --account=<name> --follow
```

| Argument | Description |
| --- | --- |
| `endpoint-name` | The endpoint name as defined in your serverless.json file (not the path to the function file). |
| `--file` | Output the logs to function.log |
| `--follow` | Tail the logs to get a live update as you are executing your serverless functions. |
| `--latest` | Output only the most recent log |
| `--account` | The HubSpot account nickname from your hubspot.config. This parameter is required if you do not have a [defaultPortal](/guides/cms/tools/local-development-cli#top-level-parameters) in your hubspot.config.Supports an alias of `--portal` for backward compatibility with older versions of the CLI. |
| `--compact` | hides log output/info. Returns success/error and execution time. |
| `--limit=` | limit the amount of logs displayed in output |
If you receive this error: `A server error occurred: WARNING: The logs for this function have exceeded the 4KB limit`, your log is too large. This can be caused by trying to console log a very large object, or by a lot of separate console logs. To resolve this, reduce how much you're trying to log, hit your endpoint, then run the command again.
### Add a secret

Add a [secret](/guides/cms/content/data-driven-content/serverless-functions/overview#secrets) to your account which can be used within serverless functions. After running the command you will be prompted to enter the secret's value.

To expose the secret to your function, update your [`serverless.json`](/reference/cms/serverless-functions#serverless-json) file with the secret's name, either to the specific endpoints you want to use it in or globally to make it available to all.

```shell
hs secrets add <secret-name>
```

| Argument                                      | Description                |
| --------------------------------------------- | -------------------------- |
| `secret-name` | Name of the secret to add. |

### Update a secret

Update the value of a [secret](/guides/cms/content/data-driven-content/serverless-functions/overview#secrets) in your account which can be used within serverless functions. You will then be prompted to enter the secret's value.
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.
```shell
hs secrets update <secret-name>
```

| Argument | Description |
| --- | --- |
| `secret-name` | The name of the secret, which you'll later use to reference the secret. This can be any unique value, though it's recommended to keep it simple for ease of use. |

### Remove a secret

Remove a [secret](/guides/cms/content/data-driven-content/serverless-functions/overview#secrets) from your account, making it no longer usable within serverless functions. After running this command, edit your [`serverless.json`](/reference/cms/serverless-functions#serverless-json) file to remove the secret's name.

```shell
hs secrets delete <secret-name>
```

| Argument | Description |
| --- | --- |
| `secret-name` | Name of secret you want to remove. |

### List secrets

List [secrets](/guides/cms/content/data-driven-content/serverless-functions/overview#secrets) within your account to know what you have stored already using the add secrets command.

```shell
hs secrets list
```

## Open browser shortcuts

There are so many parts of the HubSpot app that developers need to access frequently. To make it easier to get to these tools you can open them directly from the command line. Your `defaultAccount` or `--account` argument will be used to open the associated tool for that account.

### open

```shell
hs open <shortcut-name or alias>
```

| Argument | Description |
| --- | --- |
| `shortcut` | Provide the full shortcut name or alias of the short cut you wish to open in your browser. |

```shell
hs open --list
```

| Argument | Description |
| --- | --- |
| `--list` | Lists all of the shortcuts, their aliases and destinations. |

## Command completion

If you use the CLI frequently, it can be useful to be-able-to tab to auto-complete commands.

```shell
hs completion >> ~/.bashrc
```

For Mac OS X

```shell
hs completion >> ~/.bash_profile
```

## Evaluate themes and templates for SEO and accessibility

Uses [Google's Lighthouse tools](https://developer.chrome.com/docs/lighthouse/overview) to score the quality of your themes and templates for their adherence to the following categories:

- Accessibility
- Web best practices
- Performance
- PWA
- SEO

The following types of templates are scored:

- landing pages
- website pages
- Blog posts
- Blog listing page

If any templates fail to generate a score because of Lighthouse errors, a list of these templates will be provided.

```shell
hs cms lighthouse-score --theme=path
```

| Parameter | Description |
| --- | --- |
| `--theme-path` | Path to a theme in the Design Manager. |
| `--verbose` | <ul><li>When this parameter is excluded, the returned score is an average of all the theme's templates (default).</li><li>When this parameter is included, the individual template scores are shown. You'll also receive [Lighthouse report](https://developer.chrome.com/docs/lighthouse/overview/#report-viewer) links for each template.</li></ul> |
| `--target` | This can either be desktop or mobile to see respective scores. By default, the target is desktop. |

## Generate theme field selectors for in-app highlighting

When creating a theme, use the following command to generate an `editor-preview.json` file which maps CSS selectors to theme fields. This enables content creators to see which theme elements will be impacted by updates to a field's styling options.

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 this command will make a rudimentary guess as to 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. Learn more about [theme editor field highlighting](/guides/cms/content/fields/overview#theme-editor-field-highlighting).

```shell
hs theme generate-selectors <theme-directory-path>
```

## Modes

The `\--mode` option allows you to determine if local changes are published when uploaded to HubSpot. This option can be used in each command or set as a default in your `hubspot.config.yml` file.

The two options for `\--mode` are `\--mode=draft` and `\--mode=publish.`

The following is the order of precedence for setting `\--mode`:

1.  Using `\--mode` in a command will override all other settings.
2.  Setting a `defaultMode` for each account in your `hubspot.config.yml file`, removes the need to use `\--mode` in each command. It will override the top-level setting.
3.  Setting a `defaultMode` at the top-level in your `hubspot.config.yml file`, sets a default`\--mode` for all accounts. It will override the default behavior.
4.  The default behavior for `\--mode` is `publish`

## Environment variables

The HubSpot CLI supports the use of environment variables, this can be especially useful when creating automations like a GitHub Action.

Run any command using the `--use-env` flag to use the environment variables instead of the `hubspot.config.yml`.

```shell
hs upload example-project example-project-remote --use-env
```

| Name | Description |
| --- | --- |
| `HUBSPOT_PORTAL_ID` | The HubSpot account ID. |
| `HUBSPOT_PERSONAL_ACCESS_KEY` | The [personal access key](/guides/cms/tools/personal-access-key) of a user on the HubSpot account. All updates made will be associated to this user. |
| `HUBSPOT_CLIENT_ID` | The OAuth client ID. |
| `HUBSPOT_CLIENT_SECRET` | The OAuth secret. |
As of November 30, 2022, HubSpot API Keys 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.
## Marketplace asset validation

The CLI provides a suite of automated tests you can perform on your assets to get them in-line with the marketplace requirements prior to submitting. Passing all automated tests does not mean you will for sure pass the review process, further review is conducted to ensure quality beyond what can be easily automated.

### Validate theme

The theme validation command allows you to quickly run automated tests on your theme to identify problems that need to be fixed prior to submission to the asset marketplace. These will be returned in your CLI as a list of \[error\] and \[success\] messages separated into groups that represent types of assets within a theme.

Before you can validate a theme, you'll first need to upload it to your account with `hs upload`. Then, run the following command to validate the uploaded theme.

```shell
hs theme marketplace-validate <src>
```

| Argument | Description |
| --- | --- |
| `src` | Root relative path to the theme folder in the design manager. |

### Validate module

Similar to validating a theme, this command allows you to quickly run automated tests on a module to identify problems that need to be fixed prior to submission to the asset marketplace.

Before you can validate a module, you'll first need to upload it to your account with `hs upload`. Then, run the following command to validate the uploaded module.

```shell
hs module marketplace-validate <src>
```

| Argument | Description |
| --- | --- |
| `src` | Root relative path to the module folder in the design manager. |


# Personal Access Key

**Personal access keys are the recommended way of authenticating with local development tools.** Personal access keys work in a similar fashion to API Keys but are tied to a specific user in an account. Personal access keys only work with local development tools.

### Personal access keys compared to other Auth methods

The advantage of personal access keys over implementations like API keys is that API keys effectively have super admin permissions. Personal access keys are limited to the permissions that the individual user in the portal has. If the user has Super Admin, they see no difference in their functionality, but the advantage is that if say an individual developer needs to be removed from an account, the act of disabling their user on the account will disable their local development capabilities.

Because personal access keys are tied to the individual user in an account we are able to display more useful information, for example, if a developer changes or uploads a file using the local development tools while using a personal access key, we can attribute the change in-app to that user. This makes it easier to work with teams and understand who did what.

Personal access keys are tied to the individual user in the specific HubSpot account, and not the user directly. What this means is that using the local development tools you will need to generate a new personal access key for each account you wish to use the development tools with. This provides a layer of security for accounts, as a malicious actor obtaining your access key would then only be able to affect the individual portals and as that individual user.

#### Similarity to OAuth2
### Protect your credentials

Guard your personal access keys as if they are your account password, share them with no-one. They enable whoever has them to authenticate as if they are you and take any action you personally can take.

### Using personal access keys with the local development tools

Personal access keys were built to be used with local development tools.

[Get started with the local development tools](/guides/cms/setup/getting-started-with-local-development).

[View your personal CMS access key.](http://app.hubspot.com/l/personal-access-key)

When used for auth in the local development tools, your `hubspot.config.yml` file will resemble this:

```yaml
defaultPortal: production
portals:
  - name: production
    portalId: <portalId>
    authType: personalaccesskey
    personalAccessKey: >-
      CJDVnLanLRICEQIYyLu8LyDh9E4opf1GMhkAxGuU5XN_O2O2QhX0khw7cwIkPkBRHye-OfIADgLBAAADAIADAAAAAAAAAAJCGQC8a5TlhtSU8T-2mVLxOBpxS18aM42oGKk
    auth:
      tokenInfo:
        accessToken: >-
          CJDVnLanLRICEQIYyLu8LyDh9E4opf1GMhkAxGuU5XN_O2O2QhX0khw7cwIkPkBRHye-OfIADgLBAAADAIADAAAAAAAAAAJCGQC8a5TlhtSU8T-2mVLxOBpxS18aM42oGKk
        expiresAt: '2020-01-01T00:00:00.000Z'
```


# Create a project (BETA)

Using projects, you can package, build, and deploy to HubSpot locally using the CLI. Depending on your HubSpot subscription, you can use projects to build the following:

- **UI extensions:**
  - Building UI extensions for private apps in standard HubSpot accounts requires a **_Sales Hub_** or **_Service Hub_** _Enterprise_ subscription. However, a paid subscription is not required for creating UI extensions for private apps in test accounts within developer accounts.
  - Public apps are built in developer accounts, which don't require a paid subscription. If you're enrolled in the UI extensions for public apps Early Access beta, learn more about [building UI extensions for public apps](/guides/crm/public-apps/overview).
- **CMS:** all accounts can create projects to build [CMS React modules](/guides/cms/content/modules/build-modules-and-partials-with-react). However, to include serverless functions that are invoked by public URLs, you'll need a _**Content Hub**_ _Enterprise_ subscription.

This guide walks through how to create a project from scratch. After creating and uploading the project, you'll then create a [private app](/guides/crm/private-apps/creating-private-apps) or [public app](/guides/crm/public-apps/creating-public-apps) within it, followed by [UI extensions](/guides/crm/ui-extensions/create) or [CMS React modules](https://github.hubspot.com/cms-js-building-block-examples/).
To get started developing a project, app, and UI extension from a template, check out the following quickstart guides:

- [UI extensions for private apps](/guides/crm/private-apps/quickstart)
- [UI extensions for public apps](/guides/crm/public-apps/quickstart)

Or, check out [HubSpot's sample UI extensions](/guides/crm/ui-extensions/sample-extensions/overview) to examples of how these pieces fit together.
## Create a project

To create a project from scratch:

- In the terminal, navigate to the folder where you'll be storing the project locally. A project can live anywhere locally, but will be stored in HubSpot as a root-level directory in the developer file system.
- Run `hs project create`.
- Enter a **name** for your project, then press **Enter**.
- Press **Enter** to create the project with the suggested directory. Or enter a new directory path, then press **Enter**.
- Select **No template** to create a project with no template.

A project directory will then be created with the following assets:

- An `hsproject.json` file in the root directory to configure the project.

```json
// Example project config file
{
  "name": "my_project",
  "srcDir": "src",
  "platformVersion": "2023.2"
}
```

| Field | Type | Description |
| --- | --- | --- |
| `name` | String | The project's name |
| `srcDir` | String | The directory that contains the rest of your project files. |
| `platformVersion` | String | The [version of the developer project platform](/guides/crm/developer-projects/platform-versioning) you're developing on. As improvements are made and features are added to the projects platform, some might include breaking changes. By specifying a version, you can control which features are accessible in your project. If not specified, the project will fall back to the current version. |

- An `src` folder where you'll store your app's files. This folder can have any name as long as it matches the `srcDir` value in the `hsproject.json` file.
Next, upload the project to HubSpot by running `hs project upload`. This will upload the empty project to your account where you can then view it.
The source directory cannot be greater than 50MB uncompressed, otherwise it will fail to upload.
## View the project in HubSpot

To view your deployed project in HubSpot, you can run `hs project open` in the terminal, or navigate to it in HubSpot:

- In your HubSpot account, navigate to **CRM Development**.
- In the left sidebar menu, navigate to **Projects**. The project card will display a _This project is empty_ message, which is expected because you've uploaded a project without a private app.
- Click the **name** of the project. The project details page will display information about build history once a private app is added to the project.

From the project details page, you can also manage auto-deploy settings:

- On the project page, click the **Settings** tab.
- Click to toggle the **Auto-deploy successful builds** switch off.
Next, create a [private app](/guides/crm/private-apps/creating-private-apps) or [public app](/guides/crm/public-apps/creating-public-apps) in the project so that you can start building your [UI extensions](/guides/crm/ui-extensions/create) or [CMS React modules](/guides/cms/content/modules/build-modules-and-partials-with-react).


# Link a GitHub repository to a project (BETA)

Connect a GitHub account to HubSpot to automatically trigger a new project build when your team pushes a change to an associated GitHub repository. You can then use familiar GitHub tools and workflows to streamline development on your project. Follow the instructions below to create a new project and associate it with an existing GitHub repository.

## Prerequisites

Before you link your repository, make sure you've run `hs project create` in the root directory of your repository, and you've committed and pushed the resulting code to GitHub. If you're creating a project for the first time, check out the [setup guide](/guides/crm/setup) to configure your local environment.

## Create a project in HubSpot

To link your GitHub repository to a new project:

- In your HubSpot account, navigate to **CRM Development**.
- In the left sidebar menu, select **Projects**.
- In the top right, click **New project**.
- In the dialog box, select **Project from GitHub**, then click **Connect to GitHub**.
- Log into your GitHub account, then authorize the HubSpot Projects integration:
  - To grant HubSpot full access to all repositories in your account, select **All repositories**. If you only want to link a single repository to a project, select **Only select repositories**, then select a **repository**. After you've installed the integration, you can always authorize other repositories if needed by updating repository access in the _HubSpot Projects_ application configuration in your [GitHub account settings](https://github.com/login?return_to=https%3A%2F%2Fgithub.com%2Fsettings%2Finstallations).
  - Click **Install & Authorize**.
- Back in HubSpot, in the dialog box, select the **radio button** next to the repository you want to link to your new project.
- Click the **Select branch** dropdown menu and select a **branch** that will trigger new builds when you or a team member pushes a change.
- Click **Link GitHub repo**.
Once linked, HubSpot will create a new build of your project any time someone pushes a change to the repository. If you've turned on [auto-deploy](/guides/crm/intro/create-a-project#builds-and-deploying), the project will be deployed after the build completes.
- When a project is linked to a GitHub repository, team members <u>cannot</u> upload code directly to the project using the [HubSpot CLI](/guides/crm/developer-projects/project-cli-commands#upload-to-hubspot).
- After linking a GitHub repository, if you upgrade your GitHub plan to Enterprise and enforce SAML SSO, you'll need to reauthorize the HubSpot connection. Otherwise, HubSpot will not be able to retrieve existing project repositories or link new ones. Learn more about [SAML SSO for GitHub Enterprise](https://docs.github.com/en/enterprise-cloud@latest/authentication/authenticating-with-saml-single-sign-on/about-authentication-with-saml-single-sign-on#about-oauth-apps-github-apps-and-saml-sso).
## Link and unlink a repository

In addition to creating a new project from an existing repository, you can also link an existing project to a repository. To link a GitHub repository to a project:

- In your HubSpot account, navigate to **CRM Development**.
- In the left sidebar menu, select **Projects**.
- In the project, click the **Settings** tab.
- Under _GitHub connection_, click **Link now**.

  

- In the right sidebar, select the **repository** and **branch** to connect to, then click **Link GitHub repo**.

To unlink a GitHub repository from one of your projects:

- In your HubSpot account, navigate to **CRM** **Development**.
- In the left sidebar menu, select **Projects**.
- Click the **name** of a project.
- Click the **Settings** tab.
- Under _GitHub Connection_, click **Unlink project from GitHub.**
- In the dialog box, click **Unlink project**.

## Uninstall the HubSpot app in your GitHub account

To fully disconnect GitHub from your HubSpot account:

- In your HubSpot account, navigate to **Settings**.
- In the left sidebar menu, navigate to **Integrations** > **Connected Apps**.
- On the GitHub integration tile, click **Actions**, then select **Uninstall**.

The GitHub account will then be disconnected from the account, unlinking any projects in the account that have been linked to the connected branch. Commits to the branch will no longer trigger new builds in HubSpot.


# Projects platform versioning (BETA)

HubSpot developer projects include a `platformVersion` field that enables you to control which version of the projects platform you're developing on. This enables you to access updated functionalities, rollback to previous versions, and generally coordinate changes as needed.

Below, learn how to set the `platformVersion` in a project, along with the available versions and their updates.

## Manage versions

To set which version of the developer platform your projects are running on, use the `platformVersion` field in your project's `hsproject.json` file.

After changing the version number, you'll need to upload the project to the HubSpot account to enable access to the version updates.

```json
// Example project.json config file
{
  "name": "my_project",
  "srcDir": "src",
  "platformVersion": "2025.1"
}
```

## Versions

- **Latest version**: `2025.1` (April 1, 2025)
- **Initial release:** `2023.1` (sunset as of March 31, 2024)

| Parameter | Description |
| --- | --- |
| `2025.1` | Available as of April 1, 2025. This platform version increases the minimum required version of Node.js to v20. |
| `2023.2` Learn more about working with these changes below. |
| `2023.1` | Initial release of the developer platform. This version is no longer be available as of March 31, 2024. Attempts to upload projects at this version will fail. |

## Changes in 2025.1

Version `2025.2` increases the minimum version of Node.js to v20 for serverless functions, app functions, and endpoint functions.

## Changes in 2023.2

Version `2023.2` of the developer platform includes the changes below.

### Serverless function configuration

The following changes have been made for serverless function configuration (`serverless.json`):

- Previously, serverless functions in projects supported two types of functions: app and API endpoint. App functions have been updated to support public URLs for making API requests, so you no longer need to build these as separate types.
- With this update, the `runtime` and `version` fields have also been removed.
- This version uses Node18, and lower versions cannot be specified.

**Previous serverless function configuration**

```js
{
  "runtime": "nodejs18.x",
  "version": "1.0",
  "appFunctions": {
    "functionName": {
       "file": "function1.js"
    }
  },
"endpoints": {
  "path/to/endpoint": {
  "file": "githubUserFunction.js",
  "method": ["GET", "POST"]
  }
 }
}
```

**Updated serverless function configuration**

```js
{
  "appFunctions": {
    "functionName": {
       "file": "function1.js",
       "endpoint": {
          "path": "path/to/endpoint",
          "method": ["GET"],
        }
     }
  }
}
```

### Async support

Projects now support asynchronous functions. Callbacks are no longer supported in this version.

To update your serverless functions to use async:

- Add `async` to the function definition.
- Remove the callback (sometimes referred to as `sendResponse`), and use return statements to return response.
- Use `await` and `try`/`catch` instead of promise chaining.
- Return the desired response or throw an error.

#### Before

```js
const hubspot = require('@hubspot/api-client');

exports.main = myFunction = (context = {}, sendResponse) => {
  const { hs_object_id } = context.propertiesToSend;
  const userId = context.parameters.userId;

  const hubspotClient = new hubspot.Client({
    accessToken: process.env['PRIVATE_APP_ACCESS_TOKEN'],
  });

  const properties = {
    name: 'newName',
  };
  const SimplePublicObjectInput = { properties };
  const objectType = 'myObject';
  const objectId = hs_object_id;
  const idProperty = undefined;

  hubspotClient.crm.objects.basicApi
    .update(objectType, objectId, SimplePublicObjectInput, idProperty)
    .then((res) => {
      sendResponse(res);
    })
    .catch((err) => {
      console.error(err);
      sendResponse(err);
    });
};
```

#### After

```js
const hubspot = require('@hubspot/api-client');

exports.main = async (context = {}) => {
  const { hs_object_id } = context.propertiesToSend;
  const userId = context.parameters.userId;

  const hubspotClient = new hubspot.Client({
    accessToken: process.env['PRIVATE_APP_ACCESS_TOKEN'],
  });

  const properties = {
    name: 'newName',
  };
  const SimplePublicObjectInput = { properties };
  const objectType = 'myObject';
  const objectId = hs_object_id;
  const idProperty = undefined;

  try {
    const res = await hubspotClient.crm.objects.basicApi.update(
      objectType,
      objectId,
      SimplePublicObjectInput,
      idProperty
    );
    return res;
  } catch (err) {
    console.error(err);
    return err;
  }
};
```

### Private app access token authentication

Whereas previously you would refer to private app access tokens with `context.secrets.PRIVATE_APP_ACCESS_TOKEN`, you'll now use `process.env` rather than `context.secrets`. For example:

```js
// Include HubSpot node API client
const hubspot = require('@hubspot/api-client');

exports.main = async (context = {}) => {
  // instantiate HubSpot node API client
  const hubspotClient = new hubspot.Client({
    accessToken: process.env['PRIVATE_APP_ACCESS_TOKEN'],
  });

  //your function

  return response;
};
```

### Improved logging

Version `2023.2` increases log size from 4KB to 256KB and guarantees logs to be in order of execution. You can also take advantage of improved in-app logging, such as [log tracing](/guides/crm/private-apps/creating-private-apps#log-traces).


# Developer projects CLI commands (BETA)

The HubSpot command line interface (CLI) connects your local development tools to HubSpot, allowing you to develop on HubSpot with version control, your favorite text editor, and various web development technologies.
A new version of the HubSpot CLI, v7.0, is now available. It's strongly recommended that you upgrade to the latest version by running `npm install -g @hubspot/cli@latest`. Learn more about using [CLI v7.0](/guides/cms/tools/hubspot-cli/cli-v7).

The commands described in this guide are based on v7.0 of the CLI.
Below, learn about the CLI commands available while you're developing with HubSpot projects. You can also refer to the [standard CLI commands](/guides/cms/tools/local-development-cli) reference for general commands such as `hs auth`.
You can run `hs project --help` at any time in your terminal to learn more about project-specific commands.
## Update the CLI

Update your CLI to the latest version.

```shell
npm i -g @hubspot/cli@latest
```

## View all commands

List all project-specific CLI commands.

```shell
hs project help
```
To learn more about a specific command, enter the command followed by `--help`.
## Create a new project

Create a project in a specified directory. You'll be prompted to give the project a name, as well as confirm the local location. You'll then select whether to start the project from scratch or from a sample template.

A new folder will be created in the specified directory containing an `hsproject.json` file and an `src` folder where you'll build out your [project components](/reference/ui-components/overview).

Once you've created a project, you can run other project commands inside your project directory and HubSpot will automatically recognize your project.

```shell
hs project create
```

## Upload to HubSpot

Upload the project to your HubSpot account and create a build. If the project hasn't been created in the account yet, you'll be asked whether you want to create it.

If the project is configured to auto-deploy, this command will automatically deploy after the build is successful. By default, new projects are set to auto-deploy.

```shell
hs project upload
```

You can upload a project to a specific account in your `hubspot.config.yml` file by adding `--account=accountName` to the command. For example, `hs project upload --account=main`. This can be useful when switching between uploading to a sandbox account and then uploading to the main account. For example, your workflow might look like:

- When developing your project in a sandbox, you upload changes with `hs project upload --account=sandbox`.
- Then when uploading the project to a main account, you upload the project with `hs project upload --account=main`.

You can use the same configuration when using the [watch](#watch-for-changes) command.

## Deploy to HubSpot

Manually deploy the most recent build if the project is not set to auto-deploy.

```shell
hs project deploy
```

## Migrate a public app

Migrate a public app to the developer projects framework. Learn more in the [public app migration guide](/guides/crm/public-apps/migrate-a-public-app-to-projects).

```shell
hs project migrate-app
```

## Clone a public app

Create a copy of an existing public app that hasn't been migrated to the projects framework.

```shell
hs project clone-app
```

You can deploy any build by adding `--buildId=buildID`. For example, `hs project deploy --buildId=123`.

## Start a local development server

Start a local development server to view extension changes in the browser without needing to refresh. With the server running, saving changes to your extension's front end files and serverless function files will cause the extension to automatically refresh. This does not include changes made to the `.json` config files, which need to be manually uploaded instead.

All CRM object definitions will be copied from production to development sandbox. You can use the [import tool](https://knowledge.hubspot.com/import-and-export/import-objects) to import production object record data, or manually create sample data for testing.

When a project has multiple extensions, you'll be prompted to select which extensions to run. You can run multiple extensions from the same app, but not multiple extensions across multiple apps.

```shell
hs project dev
```

## Open project in HubSpot

Opens the project in HubSpot where you can view the project's settings, build history, and more. By default, will attempt to open the project in the default account set in `hubspot.config.yml`. Specify an account by adding the `--account=accountName` flag.

```shell
hs project open
```

## Watch for changes

Watches the project directory and uploads to HubSpot upon saving, including deleting files. Each upload will result in a new build with a new build ID. A successful build will deploy automatically if the project’s [auto-deploy setting](/guides/crm/intro/create-a-project#builds-and-deploying) is turned on.

```shell
hs project watch
```

You can further configure watch to send changes to a specific account with `---account=accountName`. For example, `hs project watch --account=main`.

## View logs

Get logs for a specific function within a project.

```shell
hs project logs
```

Running this command will guide you through selecting the project, app, and serverless function to get logs for. However, you can also manually specify this information by including the following flags in the command:

| Flag | Description |
| --- | --- |
| `--project=projectName` | The name of the project as set in the `hsproject.json` file. |
| `--app=appName` | The name of the app as set in the `app.json` file. |
| `--function=functionName` | For app functions, the name of the serverless function as set in the `serverless.json` file. |
| `--endpoint` | For endpoint functions, the public endpoint path. |

## Sandbox commands

Interact with [standard sandboxes](https://knowledge.hubspot.com/account-management/set-up-a-hubspot-standard-sandbox-account) and [development sandboxes](/guides/crm/setup#create-and-use-development-sandboxes) using the commands below.

### Create a sandbox

Creates a new sandbox in a production account. When running this command, you can select whether you want to create a standard sandbox or a development sandbox.

If creating a standard sandbox, when running this command, all supported assets will be synced from production to the standard sandbox by default. You can choose to trigger a one-time sync of the last updated 5,000 contacts and, if applicable, up to 100 associated companies, deals, and tickets (for each associated object type).

A production account can have one standard sandbox and two development sandboxes at a time. Additional standard sandboxes can be purchased as an [add-on](https://legal.hubspot.com/hubspot-product-and-services-catalog#Addons). Learn more about [development sandbox limits](/guides/crm/setup#create-and-use-development-sandboxes).

```shell
hs sandbox create
```

### Delete a sandbox

Deletes a sandbox connected to the production account. Follow the prompts to select the sandbox account to delete, then confirm the permanent deletion.

```shell
hs sandbox delete
```

### Sync

Sunset on September 10th, 2024. Use the HubSpot UI to [sync supported assets from production to standard sandbox](https://knowledge.hubspot.com/account-management/sync-automated-emails-forms-lists-and-workflows-to-a-standard-sandbox-account).

```shell
hs sandbox sync #sunset on September 10th, 2024
```


Previously, this page was titled "CRM development tools overview." It has been updated to reflect the expanded toolset, as UI extensions can also be used to customize the help desk preview panel UI. All previous CRM development tools documentation has been expanded and moved to the [guides section](/guides/crm/ui-extensions/overview). This page has been updated for posterity.
# UI customization overview (BETA)

Customize HubSpot's UI by building UI extensions, powered by public and private apps. UI extensions are developed locally using the developer projects framework, and enable you to use features like development sandboxes, version control, and integration with GitHub to streamline your process.

For example, you can build an app card that retrieves data from an external source.
Below is an overview of the tools you'll use during the UI extension development process, along with links to in-depth documentation to guide you along the way.
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 an _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).
## Projects

Projects are the highest level container of UI extension development, as they contain not only UI extensions, but also the app that powers them, along with all other supporting assets. Projects also enable a build and deploy process on HubSpot's platform with the `hs project` CLI commands, which you'll use for version control as you build. Once deployed to an account, you can view and manage the project, app, and any included UI extensions in HubSpot. This includes viewing build history and monitoring API calls.

Learn more about [creating projects](/guides/crm/developer-projects/create-a-project), and get started by following the UI extension quickstart guides for [private apps](/guides/crm/private-apps/quickstart) or [public apps](/guides/crm/public-apps/quickstart).

## Apps

Apps are the powerhouse of UI extensions, enabling you to authenticate requests for data fetching and more. You can build UI extensions for both public apps and private apps, depending on your use case. The main differences between which type of app to use include:

- **HubSpot subscription:** you'll need an _Enterprise_ subscription to build UI extensions for private apps in a standard HubSpot account. However, you can try out the toolset for free by opting a [developer test account](/getting-started/account-types#developer-test-accounts) into the [beta](#get-started). And because developer accounts are free, a paid subscription is not required for building with public apps.
The ability to build UI extensions for public apps is currently in Early Access beta, and enrollment is separate from the _CRM development tools to build UI extensions with React as frontend_ beta for private apps.

[Request access to the UI extensions for public apps beta](https://developers.hubspot.com/build-app-cards)
- **Distribution:** private apps are more intended for use in individual accounts as needed, as they must be created on a per-account basis. Public apps are intended for use in multiple accounts and include an easy installation flow for the end-user. If you intend to list your app on the App Marketplace, you'll need to build a public app.
- **Authentication:** private apps fetch data using serverless functions, and HubSpot provides the back-end by authenticating requests with an access token. Public apps fetch data with the `hubspot.fetch()` API and authenticate requests with OAuth, which require you to provide a custom back-end. Because OAuth tokens are short-lived, your integration will need to have the infrastructure to securely store, manage, and refresh tokens.
- **Supported features:** while both app types support UI extensions, public apps enable you to include more types of extensions as features in your app, such as timeline events or using the calling SDK.

Learn more about the [differences between these types of apps](/guides/apps/overview), as well as how to create and configure both [public apps](/guides/crm/public-apps/quickstart) and [private apps](/guides/crm/private-apps/quickstart) locally using projects.

## UI extensions

UI extensions are custom-built, React-based UI elements that enrich HubSpot's interface. At this time, possible UI extensions include app cards for CRM records and help desk. For example, you can create a card that enables users to submit form data to an external database from any contact record, or a card that customer support reps can use to take action on open tickets in the help desk UI.

UI extensions are built using projects and consist of a front-end and back-end:

- **UI extension front-end:** the user-facing part of the extension in the form of an app card consisting of [HubSpot-provided components](/reference/ui-components/overview). Along with displaying information, users can interact with components to perform a variety of actions. You'll build the front-end with either React or TypeScript.
- **UI extension back-end:** private apps fetch data through [serverless functions](/guides/crm/private-apps/serverless-functions), which allows a UI extension to send and retrieve data to display in components, while the back-end is provided by HubSpot. Public apps instead use the [hubspot.fetch API](/guides/crm/public-apps/fetching-data), which requires you to bring your own REST-based back end for authenticating and authorizing requests.

Learn more about [UI extensions](/guides/crm/ui-extensions/overview).
The app cards you can build with projects are separate from [classic CRM cards](/guides/apps/extensions/overview), and they cannot be built interchangeably. If you've previously built a public app that includes a classic CRM card, learn how to [migrate it to the projects framework](/guides/crm/public-apps/migrate-a-public-app-to-projects).
## Development sandboxes

For private app development, you can use development sandboxes to develop projects in a lightweight testing environment to ensure your project's components work as expected before deploying to a [standard sandbox or production account](/getting-started/account-types). Development sandboxes are created through the CLI and can be accessed within production HubSpot accounts. Development sandboxes sync some, but not all, account assets on creation, and have additional limits compared to standard sandboxes.

When building UI extensions for a public app, you'll instead use test accounts within your developer account.

Learn more about [setting up a development sandbox](/guides/crm/setup).

## GitHub integration

If you prefer to use GitHub for version control, you can connect a [GitHub repository to a project](/guides/crm/developer-projects/link-a-github-repository-to-a-project) to automatically trigger project builds when you push a change to the connected repository. You can then use use GitHub tools and workflows to streamline development, whether you work alone or with a team.

## Get started

### Get started with private apps

If you're a [Super Admin](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#super-admin) in an _Enterprise_ account, you can join this beta from the _Product Updates_ page in your HubSpot account:

- In your HubSpot account, click your **account name** in the top right corner, then click **Product updates**.
- In the left sidebar, select **In beta**.
- In the list of betas, search for or scroll to the **CRM development tools to build UI extensions with React as frontend** beta, then click **Join beta**.

  

After joining the beta, get started with any of the following options:

- Follow the [quickstart guide](/guides/crm/private-apps/quickstart) to quickly build and deploy a working example app card.
- Check out [HubSpot's sample extensions](/guides/crm/ui-extensions/sample-extensions/overview) to see examples of what's possible.
- Build your project from scratch by starting with the [HubSpot projects guide](/guides/crm/developer-projects/create-a-project).

### Get started with public apps

The ability to create UI extensions for public apps is in Early Access beta, which you can request access to [here](https://developers.hubspot.com/build-app-cards). Once you're enrolled in the beta, start with the following resources:

- [UI extensions for public apps overview](/guides/crm/public-apps/overview)
- [UI extensions for public apps quickstart](/guides/crm/public-apps/quickstart)
- [Migrate an existing public app to the projects framework](/guides/crm/public-apps/migrate-a-public-app-to-projects)

## More resources

- [CRM development tools on the HubSpot Community](https://community.hubspot.com/t5/CRM-Development-Tools-Beta/gh-p/crm-development-tools-beta)
- [CRM development tools on the HubSpot Developer Blog](https://developers.hubspot.com/blog/crm-development-tools-for-hubspot-developers-beta)


# Create private apps using projects (BETA)

When building with the developer projects framework, private apps enable you to authenticate data fetch requests using their private app access token. Private apps can only be installed into the account where they're defined, versus [public apps](/guides/crm/public-apps/creating-public-apps) which are installed into multiple accounts through an OAuth flow.

In your project's structure, a private app is stored in a folder that contains an `app.json` configuration file, and will also contain extension and serverless function files needed by the app. A project can contain multiple apps, and each app can contain multiple UI extensions.

In this guide, learn how to create a private app in a project and view it in HubSpot. Then, you can [create a UI extension within the app](/guides/crm/ui-extensions/create) to customize the CRM UI, or build [CMS React modules](/guides/cms/content/modules/build-modules-and-partials-with-react).
Building UI extensions for private apps in a standard HubSpot account requires an _Enterprise_ subscription. However, you can try out building private apps using projects in [developer test accounts](/getting-started/account-types#developer-test-accounts) for free.
## Create a private app within a project

To create a private app in a project, you'll first need to [install the HubSpot CLI](/guides/cms/setup/getting-started-with-local-development). To get started with HubSpot projects, you can also check out the [quickstart guide](/guides/crm/private-apps/quickstart).
You can create up to 20 private apps in a HubSpot account, and each app is subject to [HubSpot's API rate limits](/guides/apps/api-usage/usage-details#rate-limits).
To create a private app within your project:

- Within the `src` folder of your project directory, create a folder for your app.
- Within the app folder, create an `app.json` file. This file will contain your app definitions.
- Copy the following code into your `app.json` file:

```json
// Example app config file
{
  "name": "Get started App with React",
  "description": "This is an example of private app that shows a custom card on the Contact record tab built with React-based frontend. This card demonstrates simple handshake with HubSpot's serverless backend.",
  "scopes": ["crm.objects.contacts.read", "crm.objects.contacts.write"],
  "uid": "unique-app-name",
  "public": false,
  "extensions": {
    "crm": {
      "cards": [
        {
          "file": "extensions/example-card.json"
        }
      ]
    }
  }
}
```

| Parameter | Type | Description |
| --- | --- | --- |
| `name` | String | A unique name for the app. |
| `description` | String | The app's description. |
| `scopes` | Array | The app's allowed [scopes](/guides/apps/authentication/scopes). At least one scope is required. |
| `uid` | String | The app's uniquely identifying name. 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` | Boolean | Set to `false` for private apps. |
| `extensions` | Object | Contains the extensions included in the app. For app cards, include a `crm` object, followed by a `cards` array that contains a `file` field that references the card's JSON configuration file. If you have not yet defined your extension, you can leave this object empty. |

## Add a serverless function to the app

Within the private app, you'll add a [serverless function](/guides/crm/private-apps/serverless-functions#create-a-serverless-function) to serve as your UI extension's back end. Serverless function files are stored in a separate folder within the app folder.

In the app directory, create a `.functions` directory with the following files:

- `serverless.json`
- `package.json`
- `example-function.js`

  

The example code below will get you started, but [check out the serverless functions guide](/guides/crm/private-apps/serverless-functions#create-a-serverless-function) to learn more about authenticating calls, managing secrets, debugging, and more.

### serverless.json

```json
// Example serverless config file

{
  "runtime": "nodejs18.x",
  "version": "1.0",
  "appFunctions": {
    "myFunc": {
      "file": "example-function.js",
      "secrets": []
    }
  }
}
```

| Parameter | Type | Description |
| --- | --- | --- |
| `appFunctions` | Object | The object that contains definitions for each serverless function. |
| `myFunc` | Object | The object containing the name of the serverless function `file` along with any secrets needed for authenticating requests in the `secrets` array.`myFunc` can be any value. You'll later reference this name when running the function in your React front end. |

### package.json

```json
// Example serverless function package.json

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

### example-function.js

```js
// Example serverless function

exports.main = (context = {}, callback) => {
  const { text } = context.parameters;

  const ret = `This is coming from a serverless function! You entered: ${text}`;
  try {
    callback(ret);
  } catch (error) {
    callback(error);
  }
};
```

With your app defined and your serverless function configured, upload the project to HubSpot using the `hs project upload` command.

On upload, the build will be triggered and HubSpot will validate your project files and provision any needed resources. By default, HubSpot will automatically deploy successful builds. Alternatively, you can [link a GitHub repository to the project](/guides/crm/developer-projects/link-a-github-repository-to-a-project) to automatically trigger a new build when you push a change to the project files in GitHub.

With your app uploaded, learn how to view it in HubSpot below. Then, [create a UI extension](/guides/crm/ui-extensions/create) in the app to customize CRM records with various UI components.

## View the app in HubSpot

If you're a [Super admin](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#super-admin), you can access and manage private apps in your HubSpot account, including viewing your apps, their build history, and the access token required to [make API calls](/guides/apps/private-apps/overview#make-api-calls-with-your-app-s-access-token).

To view a private app's details in HubSpot:

- In your HubSpot account, navigate to **CRM Development**.
- In the left sidebar menu, navigate to **Private apps**.
- Click the **name** of the private app.

You'll then be taken to the app's home page where you can view and manage its access token, view request logs and included extensions, and delete the app. Learn more about each tab below.

### Overview

On the _Overview_ tab, review high-level information about the app's API calls and extension requests and responses.
### Auth

On the _Auth_ tab, view authentication-related information, such as the private app's access token and scopes. You can also delete the app from this page.
### Logs

On the _Logs_ tab, view detailed information about the app's various calls and events. The tabs under the _Logs_ tab break down different types of events that occur while the app is running, and are helpful for debugging errors that your app might run into. Learn more about [serverless function debugging](/guides/crm/private-apps/serverless-functions#debug-a-serverless-function).

- **API calls:** click the **API calls** tab to view logs for API calls made by the app.
- **Endpoint functions:** click the **Endpoint** **functions** tab to view logs for serverless endpoint functions included in the app. You can view further event details by clicking an **event row**. Log details will be displayed in a panel on the right, and will include a _View log trace_ link that you can click to view the log track, along with a _Trade ID_ that you can copy for use on the [_Log Traces_](#log-traces) tab.
- **Serverless functions:** click the **Serverless functions** tab to view logs for serverless app functions included in the app. You can view further event details by clicking an **event row**. Log details will be displayed in a panel on the right, and will include a _View log trace_ link that you can click to view the log track, along with a _Trade ID_ that you can copy for use on the [_Log Traces_](#log-traces) tab.
- **Extensions:** click the **Extensions** tab to view React top-level event logs, such as a UI extension successfully loading. Click an **event row** to view log details, including the user who initialized the request. Log details will also include a _View log trace_ link that you can click to view the log trace, along with a _Trace ID_ that you can copy for use on the _Log Traces_ tab.
- **Security:** click the **Security** tab to view security-related events, such as private app access token viewing and rotating.
As an example, say your app is running into an error when trying to retrieve company proximity information through a serverless function. You can click the **Logs** tab, then click the **Serverless functions** tab. Then, you can click the **errored request** to view the log output in the right panel. In the panel, you can then click **View log trace** for more information.
Alternatively, you can click **Copy Trace ID** in the log output panel, then use the ID in the _Log Traces_ tab.
### Log Traces

On the _Log Traces_ tab, view log traces by trace ID, which you can retrieve from log outputs on the [_Logs_](#logs) [tab](#logs). Log tracing enables you to trace front-end and back-end logs with a single ID, making it easier to debug issues happening in production.

You can also retrieve log IDs on CRM record pages where an extension has failed to load. This can be especially useful if your extension includes [custom log messages](/guides/crm/ui-extensions/sdk#send-custom-log-messages-for-debugging). 
Log tracing is not available for private apps built on [platform version](/guides/crm/developer-projects/platform-versioning) `2023.1`.
### Extensions

On the _Extensions_ tab, view information about the extensions that are included in the app.


# UI extensions for private apps quickstart (BETA)
This quickstart guide walks through setting up HubSpot's example UI extension, which includes a card for contact records. This card sends data from the React front end to the serverless function back end, then displays that data in a success alert. You’ll then customize the card by adding a new component. By the end of this guide, you'll be familiar with the basic processes of developing UI extensions locally with React.
- A new version of the HubSpot CLI, [v7.0](/guides/cms/tools/hubspot-cli/cli-v7), is now available. It's strongly recommended that you upgrade to the latest version by running `npm install -g @hubspot/cli@latest`. The commands described in this guide are based on v7.0 of the CLI.
- Building UI extensions for private apps in a standard HubSpot account requires an _Enterprise_ subscription. However, you can try out building private apps using projects in [developer test accounts](/getting-started/account-types#developer-test-accounts) for free.
## Prerequisites

Before proceeding, ensure that you've [opted in to the CRM development tools beta](/guides/crm/overview#get-started).

This guides assumes that you're already familiar with:

- The basics of React and JavaScript (or optionally Typescript)
- Using the [HubSpot CLI for local development](/guides/cms/setup/getting-started-with-local-development)

## 1. Set up your local environment

Because UI extensions are developed locally, you'll first need to set up your environment:

- Install [Node.js](https://nodejs.org/en/download/) which enables HubSpot’s local development tools. Versions 18 or higher are supported. It's recommended to use a package manager like [Homebrew](https://brew.sh/) or [nvm](https://github.com/nvm-sh/nvm) to install Node.
- Navigate to the directory where you'll be storing your project, app, and extension files.
- Run `npm install -g @hubspot/cli@latest` to update the HubSpot CLI to the latest version.
- Run `hs init`, then follow the prompts to initialize the HubSpot configuration file to connect the CLI to your HubSpot account.

After setting up your environment, proceed to the next steps where you'll create your project.

## 2. Create a new project

- Create a new project in your working directory.

```shell
hs project create
```

- After running the command, you'll be prompted to name the new project, select its location, then select a template. For this guide, select `CRM getting started project with private apps`. A new directory will then be created using the project name you assigned.
- Run `hs project install-deps` to install the dependencies required to start the local development server.

```shell
hs project install-deps
```
This sample project uses a free API which doesn't require any secret handling. Learn more about [how to include secrets](/guides/crm/private-apps/serverless-functions#managing-secrets) so that they're accessible during local development and when deployed.
## 3. Start local development

To start local development, you'll use the `hs project dev` command. This command will walk you through setting up a development sandbox and allow you to test changes locally before uploading to your development sandbox or production account.

```shell
hs project dev
```

- After running `hs project dev`, select the account you want to work in:
  - To create your extension in an existing sandbox, use the **arrow keys** to select the **sandbox**, then press **Enter**.
  - To create and test your extension in a new development sandbox, select **< Test on a new development sandbox >**. Then, name the sandbox and press **Enter**. HubSpot will then create the new development sandbox in the production account. This sandbox will sync with the production account's data, including CRM object definitions and up to 100 of the most recently created contacts and their associated deals, tickets, and companies (up to 100 each).
If you receive the error `The personal access key you provided doesn't include sandbox permissions`, you'll need to deactivate the account's Personal Access Key, then create a new one with sandbox permissions. To do so, run `hs auth`, then follow the prompts to select your account. Then, click **Deactivate** next to the personal access key, and generate a new one with the proper scopes.
- To create and test your extension in the production account, select `< ! Test on this production account ! >`.
If a project has multiple extensions, you'll be prompted to select which extension to run. You can run multiple extensions from the same app, but not multiple extensions across multiple apps.
Once the project is created, built, and deployed in the selected account, the local development server will start and you can begin building and modifying your extension.

- The browser will automatically refresh to pick up the latest saved front end code. This includes changes made to React files and serverless functions. Note that changes to serverless functions will be picked up at invocation time.
- Changes made to configuration files, such as `app.json` and `hsproject.json`, require a manual upload before you can continue development. To upload those changes, first stop the local development server with `q`, then run `hs project upload`. After your changes are uploaded, run `hs project dev` again to restart the server.

## 4. View the extension in HubSpot

With the local development server running, you can add the card to the contact record view, then view the card:

- Log in to your HubSpot account.
- In your HubSpot account, navigate to **Contacts** > **Contacts**. Then, click the **name** of a contact to view its record.
- At the top of the contact record, click **Customize record**. You'll then be brought to the _Record Customization_ tab of the contacts settings page.

  

- Click **Default view** to edit the default contact record view.
- For the purposes of this tutorial, click the **\+ plus icon tab** at the top of the editor to add a new tab.

  

- In the dialog box, enter a **name** for your new tab, then click **Done**.
- With the new tab added, click the **Add cards** dropdown menu, then select your **new card**.
- In the top right, click **Save and exit**.
- Navigate back to the contact record, then refresh the page. You should now see your new tab, which will contain your new card. With the local development server running, you'll see a _Developing locally_ tag displayed at the top of the card.

.

## 5. Add a new component to the card

With the card uploaded and local development server running, you'll now add a new [UI component](/reference/ui-components/overview) to the card and refresh the page to see your changes.

Because the React file serves as the extension's front-end, you'll find the UI extension components in the `Example.jsx` file in the `src/app/extensions` directory. For the purposes of this guide, you'll add an `Icon` component to display an icon next to the first `Text` component.

- First, add `Icon` to the import at the top of the file to make the component available for use.

```js
import {
  Tile,
  Button,
  Text,
  Input,
  Stack,
  hubspot,
  Link,
  Icon,
} from '@hubspot/ui-extensions';
```

- Then add the `Icon` component before the first closing `</Text>` tag (line 6 below).

```js
return (
  <>
    <Text>
      <Text format={{ fontWeight: 'bold' }}>
        Your first UI extension is ready!
        <Icon name="faceHappy" />
      </Text>
      Congratulations, {context.user.firstName}! You just deployed your first HubSpot
      UI extension. This example demonstrates how you would send parameters from
      your React frontend to the serverless function and get a response back.
    </Text>
    <Flex direction="row" align="end" gap="small">
      <Input name="text" label="Send" onInput={(t) => setText(t)} />
      <Button type="submit" onClick={handleClick}>
        Click me
      </Button>
    </Flex>
    <Divider />
    <Text>
      What now? Explore all available{' '}
      <Link href="https://developers.hubspot.com/docs/platform/ui-extension-components">
        UI components
      </Link>
      , get an overview of <Link href="https://developers.hubspot.com/docs/platform/ui-extensions-overview">
        UI extensions
      </Link>, learn how to <Link href="https://developers.hubspot.com/docs/platform/create-ui-extensions">
        add a new custom card
      </Link>, jump right in with our <Link href="https://developers.hubspot.com/docs/platform/ui-extensions-quickstart">
        Quickstart Guide
      </Link>, or check out our <Link href="https://github.com/HubSpot/ui-extensions-react-examples">
        code Samples
      </Link>.
    </Text>
  </>
);
```

- Save your local changes. This will trigger the local development server to update and reload the extension.
- The CRM record page should now show the new component without requiring a refresh.
## Next steps

Now that you're familiar with the basics of creating, uploading, and updating a UI extension, you can continue customizing the example card with [other UI components](/reference/ui-components/overview), updating the serverless function to [fetch different data](/guides/crm/ui-extensions/overview#fetch-hubspot-crm-data), or [create a new card from scratch](/guides/crm/ui-extensions/create).


This page is primarily focused on serverless functions for UI extensions. While similar to using serverless functions for CMS pages, there are slight differences in the implementation. To learn more about building serverless functions for the CMS, checkout the [serverless functions for CMS](/guides/cms/content/data-driven-content/serverless-functions/overview) documentation.
# Fetching data with serverless functions (BETA)
When needing to fetch data for a project-based private app, whether for a [UI extension](/guides/crm/ui-extensions/overview) or [CMS React project](/guides/cms/content/modules/build-modules-and-partials-with-react), you'll use a serverless function. When executed, HubSpot runs the serverless function server-side using JavaScript. Because HubSpot runs the function, you don't need to manage your own server.

Your access to using serverless functions depends on your HubSpot subscription:

- **UI extensions:** using serverless functions for UI extensions requires an _Enterprise_ subscription. You can also test out UI extensions for private apps for free in a [developer test account](/getting-started/account-types#developer-test-accounts).
- **CMS:** using serverless functions for CMS React projects requires a _**Content Hub**_ _Enterprise_ subscription.
## Create a serverless function

Serverless functions for private apps in projects can be executed in two ways:

- **App function:** a serverless function that gets executed directly from within the project using the `hubspot.serverless` API.
- **Endpoint function** (**_Content Hub_** _Enterprise_ only)**:** an app function that is invoked by calling a public URL. The URL is determined by the account's connected domains and the path specified in the `serverless.json` file. This is more common for [implementing serverless functions for the CMS](/guides/cms/content/data-driven-content/serverless-functions/overview).
Serverless functions cannot import other functions. You'll need to include all the code the serverless function needs to execute within the serverless function file.
### Configuration

In the serverless functions directory, configure the serverless function with a `serverless.json` file. In this file, you'll configure your serverless function name and the path of the JavaScript file to execute, along with any secrets that might need to be included for authentication.

If you have a **_Content Hub_** _Enterprise_ subscription, you can configure a serverless function to be invoked by a public URL by including an `endpoint` object in the config. Learn more about [endpoint functions](#endpoint-functions).

```js
{
  "appFunctions": {
    "functionName": {
       "file": "function1.js",
       "secrets": ["SOME_SECRET"],
       "endpoint": {
         "path": "path/to/endpoint",
         "method": ["GET"]
        }
     }
  }
}
```

| Parameter | Type | Description |
| --- | --- | --- |
| `file` | String | The `.js` file in the project to execute. |
| `secrets` | Array | To authenticate requests, you can include secrets as string values in this array. Learn more about [managing secrets](#managing-secrets). |
| `endpoint` | Object | If invoking the function by a public URL (**_Content Hub_** _Enterprise_ only), this object defines the endpoint details:<ul><li>`path`: the endpoint path.</li><li>`method`: the request method.</li></ul> |

### Building the function

Each serverless function exports a `main` function that gets called when HubSpot executes it. The function receives the `context` argument, which is an object that contains data based on how the function is being used. This includes context about where the card is loaded as well as the authenticated user and account. Learn more about [what's included in the context object](/guides/crm/ui-extensions/sdk#access-context-data).

Below is an example of a function that returns a `200` status code and a _Hello World_ message:

```js
exports.main = async (context) => {
  return {
    statusCode: 200,
    body: { message: 'Hello World' },
  };
};
```

#### Calling the function

In your extension's front-end React code, you'll call the function using the `hubspot.serverless()` API.
  <strong>Please note:</strong> the `hubspot.serverless()` API was introduced as
  of `@hubspot/ui-extensions@0.8.5`. While the previous method of using
  `runServerlessFunction` is still supported, the new API is more intuitive and
  efficient, as it avoids unnecessary prop-drilling in sub-components and
  provides responses with resolved promises regardless of the outcome. For
  posterity, you can find documentation for `runServerlessFunction` in the [SDK
  reference](#/guides/crm/ui-extensions/sdk#runserverlessfunction-deprecated).
`hubspot.serverless()` expects a string of the name of the function to call (as defined in the `serverless.json` file), and an object containing the `propertiesToSend` and `parameters` fields.
```js
// Example React front-end file
import { hubspot, Button } from '@hubspot/ui-extensions';

hubspot.extend(() => <Extension />);

const Extension = () => {
  const handleSubmit = () => {
    hubspot
      .serverless('my-function-name', {
        propertiesToSend: ['hs_object_id'],
        parameters: { extra: 'data' },
      })
      .then((response) => {
        // handle response, which is the value returned from the function on success
      })
      .catch((error) => {
        // handle error, which is an Error object if the function failed to execute
      });
  };
  return <Button onClick={handleSubmit} label="Click me" />;
};
```
```json
{
  "appFunctions": {
    "my-function-name": {
      "file": "function.js",
      "secrets": []
    }
  }
}
```
| Parameter | Type | Description |
| --- | --- | --- |
| `propertiesToSend` | Array | An array of properties to send the serverless function from the front-end. Property values are pulled from the currently displaying CRM record. This enables retrieving property values on the server-side when the function is called. |
| `parameters` | Object | Additional key-value pairs to supply to the function. |

On success, `hubspot.serverless()` will return `Promise<any>`. On failure, it will reject the promise.
Serverless functions have a response limit of 15 seconds. Functions that take longer to execute will fail.
### Endpoint functions

If you have a **_Content Hub_** _Enterprise_ subscription, you can configure a serverless function to be invoked by a public URL. When calling the URL of a HubSpot-hosted serverless function, you can use any [domain connected to your account](https://knowledge.hubspot.com/domains-and-urls/connect-a-domain-to-hubspot) with the following URL structure: `https://<domain>/hs/serverless/<endpoint-path-from-config>`.

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/to/endpoint` or `https://subdomain.brand.com/hs/serverless/path/to/endpoint`.

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.

```json
{
  "appFunctions": {
    "functionName": {
      "file": "somefunction.js",
      "secrets": ["SOME_SECRET"],
      "endpoint": {
        "path": "path/to/endpoint",
        "method": ["GET"]
      }
    }
  }
}
```

## Authenticate requests

Every private app comes with an access token that you can use to authenticate calls to HubSpot's APIs. Alternatively, you can authenticate calls using secrets, which you'll [manage through the CLI](#managing-secrets).

### Private app access tokens

To authenticate HubSpot API requests with private app access tokens, you'll access the token with `process.env['PRIVATE_APP_ACCESS_TOKEN']`. For example, to make a request using HubSpot's Node.js library:

- Ensure the [@hubspot/api-client library](/getting-started/overview) is [included as a dependency](#including-dependencies) in your project.
- In the serverless function JavaScript file, include `@hubspot/api-client`.
- Instantiate the client within `exports.main` and include `PRIVATE_APP_ACCESS_TOKEN` for authentication:

```js
// Include HubSpot node API client
const hubspot = require('@hubspot/api-client');

exports.main = async (context = {}) => {
  // Get hs_object_id of the record in context
  const { hs_object_id } = context.propertiesToSend;
  console.log(`Looking up contact by ID [${hs_object_id}]`);

  // Instantiate HubSpot Node API client
  const hubspotClient = new hubspot.Client({
    accessToken: process.env['PRIVATE_APP_ACCESS_TOKEN'],
  });

  // Your function
};
```

- Configure the rest of the function using Node.js. For example, the following code would create a serverless function that retrieves the current contact by ID using the [contacts API](/guides/api/crm/objects/contacts):

```js
// Include HubSpot node API client
const hubspot = require('@hubspot/api-client');

exports.main = async (context = {}) => {
  // Get hs_object_id of the record in context
  const { hs_object_id } = context.propertiesToSend;
  console.log(`Looking up contact by ID [${hs_object_id}]`);

  // instantiate HubSpot Node API client
  const hubspotClient = new hubspot.Client({
    accessToken: process.env['PRIVATE_APP_ACCESS_TOKEN'],
  });
  const properties = ['firstname', 'lastname'];

  try {
    const apiResponse = await hubspotClient.crm.contacts.basicApi.getById(
      hs_object_id,
      properties
    );
    console.log(JSON.stringify(apiResponse));
  } catch (e) {
    e.message === 'HTTP request failed'
      ? console.error(JSON.stringify(e.response, null, 2))
      : console.error(e);
  }
};
```

To access a private app access token during local development:

- If you're running HubSpot CLI version 7.0.0 or above, private app access tokens are automatically available for authentication as long as your personal access key has the `developer.app_functions.write` and `developer.app_functions.read` scopes.
- If you're running HubSpot CLI version 6.x or below, you'll need to add the token to a `.env` file within the `.functions` directory, as shown below. You can find the [private app access token in the app's settings in HubSpot](/guides/crm/private-apps/creating-private-apps#view-the-app-in-hubspot).

```shell
PRIVATE_APP_ACCESS_TOKEN=pat-na1-******
```
Calls authenticated with private app access tokens count against your [API call limits](/guides/apps/api-usage/usage-details).
### Secrets

To authenticate a serverless function requests using a secret:

- Create a secret by running `hs secrets add <secret-name>`. HubSpot will securely store this secret on its backend and inject them into the runtime environment when a function is invoked in production.
- In the `serverless.json` file, include a `secrets` field within the corresponding function object, then specify the name of the secret. Do not include `PRIVATE_APP_ACCESS_TOKEN` in this array, as this is automatically created for you and already available in the serverless function.

```json
// serverless.json
{
  "appFunctions": {
    "functionName": {
      "file": "function1.js",
      "secrets": ["GITHUB_ACCESS_TOKEN"]
    }
  }
}
```

- To make secrets available for local development, create a `.env` file in the `.functions` directory. HubSpot will never retrieve your secrets outside of its protected infrastructure, so you'll need to specify the secret values that you want to use when the function is executed locally.

```shell
SECRET=my-secret-value
ANOTHER_SECRET=another-secret-value
```

- After saving secrets to the `.env` file, you can access them in your function using `process.env['secretName']`.

```js
export const main = async (context) => {
  const secret = process.env['ANOTHER_SECRET'];
  const token = process.env['PRIVATE_APP_ACCESS_TOKEN'];
  ...
}
```
To get started, you can find Node.js code snippets in each of [HubSpot's API reference docs](/reference/api/overview).
#### Managing secrets

HubSpot provides a set of CLI commands for create and managing secrets:

- [Create a secret](/guides/cms/tools/hubspot-cli/cli-v7#add-a-secret) by running `hs secrets add <secret name>`. HubSpot will securely store this secret on its backend and inject them into the runtime environment when a function is invoked in production.
- [Update a secret](/guides/cms/tools/hubspot-cli/cli-v7#update-a-secret) by running the `hs secrets update` command. If you're using a Node runtime of 14 or higher, updated secret values will automatically be updated in your deployed function within one minute, meaning you won't have to build and deploy to get the updated secret.
- [View existing secrets](/guides/cms/tools/hubspot-cli/cli-v7#list-secrets) stored in your account by running `hs secrets list`.
- [Delete a secret](/guides/cms/tools/hubspot-cli/cli-v7#list-secrets) by running `hs secrets delete <secret-name>`. The command line will prompt you to confirm the deletion, which you can bypass by including the `--force` flag.
- To limit exposure of a secret, it's strongly recommended to never include it in console statements to prevent it from being recorded in logs.
- If your project is [linked to a GitHub repository](/guides/crm/developer-projects/link-a-github-repository-to-a-project), be sure to <u>never</u> commit the `.env` file when uploading to GitHub. You can include an entry for `.env` in your `.gitignore` file to ensure that it is omitted from your commits.
## Best practices

Keep the following recommendations in mind while you develop and test your serverless function:

#### Variable assignment inside functions

To ensure that variables are correctly assigned and initialized with every function invocation, you should opt to assign variables within the function itself. This practice prevents potential issues related to stale or persistent variable states, which can lead to unexpected behaviors. See the example below for additional context:

```js
// Preferred
exports.myHandler = async (event, context) => {
  const myDynamicVariable = process.env['SOME_KEY'];
  // The rest of your handler logic
};

// Discouraged
const myDynamicVariable = process.env['SOME_KEY'];
exports.myHandler = async (event, context) => {
  // The rest of your handler logic
};
```

#### Stay up to date with the latest platform version

The `hsprojects.json` configuration file includes a `platformVersion` field which specifies which [platform version](/guides/crm/developer-projects/platform-versioning) to run the project on. It's strongly encouraged to use the latest platform version to ensure that your project and its assets are up to date with the latest improvements, optimizations, and feature enhancements. In addition, some previously available features may not be available in older versions due to deprecation.

The platform version also dictates which version of Node the project runs on. The latest version, `2023.2`, uses Node18, and doesn't support older versions of Node.

## Including dependencies

By default, HubSpot provides a small number of [NPM](https://www.npmjs.com/) dependencies in addition to the [Node.js](https://nodejs.org/en/download/) standard library. To add your own dependencies, you can list the package in `dependencies` within the `package.json` file. 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](https://www.npmjs.com/package/lodash) library in a serverless function, you would first update `package.json` to include the dependency:

```json
{
  "name": "app.functions",
  "version": "1.0.0",
  "description": "",

  "dependencies": {
    "lodash": "^4.17.21"
  }
}
```

Then, at the top of your serverless function JavaScript file, you would require the lodash dependency:

```js
const _ = require('lodash');

exports.main = async (context) => {
  const objectA = {};
  const objectB = {};
  const equal = _.isEqual(objectA, objectB);
  return {
    statusCode: 200,
    body: {
      isEqual: equal,
    },
  };
};
```

## Debug a serverless function

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 for both app functions and endpoint functions, 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.

### 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`


# Working with webhooks in project-based private apps

Build webhooks into your private app to subscribe to events occurring in the account.

In this guide, learn about:

- [Defining webhook subscriptions](#defining-webhook-subscriptions)
- [Subscription types](#subscription-types)
- [Response payloads](#response-payloads)
- [Updating your private app configuration to point to your webhooks JSON file](#update-your-private-app-configuration-to-point-to-your-webhooks-json-file)
- [Viewing webhook subscriptions in HubSpot](#view-webhook-subscriptions-in-hubspot)

## Defining webhook subscriptions

Webhook subscriptions are defined in the `webhooks.json` file within a `webhooks` folder in the same directory as your app (e.g., `src/app`). You'll also need to update your [app.json](#update-your-private-app-configuration-to-point-to-your-webhooks-json-file) configuration file to reference that file.
The `webhooks.json` file contains fields for defining the webhook's settings and event subscriptions.

```json
// Example webhooks.json
{
  "settings": {
    "targetUrl": "https://example.com/webhook",
    "maxConcurrentRequests": 10
  },
  "subscriptions": {
    "crmObjects": [
      {
        "subscriptionType": "object.propertyChange",
        "objectName": "contact",
        "propertyName": "firstname",
        "active": true
      },
      {
        "subscriptionType": "object.creation",
        "objectName": "contact",
        "active": true
      }
    ],
    "legacyCrmObjects": [
      {
        "subscriptionType": "contact.propertyChange",
        "propertyName": "lastname",
        "active": true
      },
      {
        "subscriptionType": "contact.deletion",
        "active": true
      }
    ],
    "hubEvents": [
      {
        "subscriptionType": "contact.privacyDeletion",
        "active": true
      }
    ]
  }
}
```

| Parameter | Type | Description |
| --- | --- | --- |
| `settings` | Object | An object that specifies your webhook settings, including `targetUrl` and `maxConcurrentRequests`. |
| `targetUrl` | String | The URL that webhooks will be sent to. Learn more about the [response payload](#response-payloads) sent with the `POST` request. |
| `maxConcurrentRequests` | String | The maximum number of concurrent requests that will be sent. |
| `subscriptions` | Object | An object that specifies the webhook subscriptions. |
| `crmObjects` | Array | An array containing event subscription definitions. This is the standard array to include, and should be used for all events in the new format (`object.*`). Classic webhook subscription types should instead be included in `legacyCrmObjects` and `hubEvents` arrays, depending on the event. See [subscription types](#subscription-types) for more information. |
| `subscriptionType` | String | The type of event being subscribed to. Learn more about [subscription types](#subscription-types). |
| `objectName` | String | The CRM object being subscribed to. |
| `propertyName` | String | The property on the CRM object being subscribed to. |
| `active` | Boolean | Whether webhooks will be sent for this subscription. |
| `legacyCrmObjects` | Array | An array containing classic subscription types, such as `contact.creation` and `deal.deletion`. See the [webhooks API guide](/guides/api/app-management/webhooks#webhook-subscriptions) for more information. |
| `hubEvents` | Array | An array containing the classic subscription types `contact.privacyDeletion` and `conversation.*`. See the [webhooks API guide](/guides/api/app-management/webhooks#webhook-subscriptions) for more information. |

## Subscription types

Private apps built with projects use [generic webhook subscription](/guides/apps/public-apps/create-generic-webhook-subscriptions) syntax, using the format `object.*` rather than specifying the object name in the subscription type (e.g., `contact.*`).

| Subscription type  | Format                     |
| ------------------ | -------------------------- |
| Creation           | `object.creation`          |
| Deletion           | `object.deletion`          |
| Merge              | `object.merge`             |
| Restore            | `object.restore`           |
| Property change    | `object.propertyChange`    |
| Association change | `object.associationChange` |

To specify the type of CRM object you're subscribing to, include the `objectName` field in the subscription definition object using any of the supported objects in the table below. You'll need to include the object's corresponding scopes in your `app.json` file.

<table>
  <tbody>
    <tr>
      <td>
        <code>appointment</code>
      </td>
      <td>
        <code>fee</code>
      </td>
      <td>
        <code>quote_template</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>call</code>
      </td>
      <td>
        <code>feedback_submission</code>
      </td>
      <td>
        <code>task</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>cart</code>
      </td>
      <td>
        <code>goal_target</code>
      </td>
      <td>
        <code>tax</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>commerce_payment</code>
      </td>
      <td>
        <code>line_item</code>
      </td>
      <td>
        <code>ticket</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>communication</code>
      </td>
      <td>
        <code>listing</code>
      </td>
      <td>
        <code>partner_client</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>company</code>
      </td>
      <td>
        <code>meeting_event</code>
      </td>
      <td>
        <code>lead</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>contact</code>
      </td>
      <td>
        <code>note</code>
      </td>
      <td>
        <code>service</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>course</code>
      </td>
      <td>
        <code>order</code>
      </td>
      <td>
        <code>subscription</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>deal</code>
      </td>
      <td>
        <code>postal_mail</code>
      </td>
      <td>
        <code>invoice</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>discount</code>
      </td>
      <td>
        <code>product</code>
      </td>
      <td>
        <code>user</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>email</code>
      </td>
      <td>
        <code>quote</code>
      </td>
      <td>
        <code>partner_account</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>engagement</code>
      </td>
      <td>&nbsp;</td>
      <td>&nbsp;</td>
    </tr>
  </tbody>
</table>

```json
// Example webhooks.json
{
  "settings": {
    "targetUrl": "https://example.com/webhook",
    "maxConcurrentRequests": 10
  },
  "subscriptions": {
    "crmObjects": [
      {
        "subscriptionType": "object.propertyChange",
        "objectName": "contact",
        "propertyName": "firstname",
        "active": true
      }
      ...
    ]
  }
}
```

If you need to subscribe to [classic subscription types](/guides/api/app-management/webhooks#webhook-subscriptions), you can store them in the `legacyCrmObjects` and `hubEvents` arrays instead, depending on the type of subscription.

```json
{
  "settings": {
    "targetUrl": "https://example.com/webhook",
    "maxConcurrentRequests": 10
  },
  "subscriptions": {
    "legacyCrmObjects": [
      {
        "subscriptionType": "contact.propertyChange",
        "propertyName": "lastname",
        "active": true
      },
      {
        "subscriptionType": "contact.deletion",
        "active": true
      }
    ],
    "hubEvents": [
      {
        "subscriptionType": "contact.privacyDeletion",
        "active": true
      }
    ]
  }
}
```

| Parameter | Type | Description |
| --- | --- | --- |
| `legacyCrmObjects` | Array | An array containing [classic subscription types](/guides/api/app-management/webhooks#webhook-subscriptions) (e.g., `contact.propertyChange`). |
| `hubEvents` | Array | An array that can contain the `contact.privacyDeletion` and `conversation.*` [classic subscription types](/guides/api/app-management/webhooks#webhook-subscriptions). |

## Response payloads

When an event that the app is subscribed to occurs, the `targetUrl` you specify in `webhooks.json` will receive a `POST` request containing JSON formatted data from HubSpot. All events will include the same base set of fields, with other fields being added depending on the event type. Learn more about [parsing webhook payloads for specific event types](/guides/apps/public-apps/create-generic-webhook-subscriptions#parse-generic-webhook-payloads).

Below is an example payload for `propertyChange` event. This type of event contains all generic fields, plus the `propertyName` and `propertyValue` fields to show which property changed and the new value.

```json
[
  {
    "appId": 3715530,
    "eventId": 100,
    "subscriptionId": 2764026,
    "portalId": 123456,
    "occurredAt": 1723651850844,
    "subscriptionType": "object.propertyChange",
    "attemptNumber": 0,
    "objectId": 987654,
    "changeSource": "CRM",
    "objectTypeId": "0-1",
    "propertyName": "firstname",
    "propertyValue": "sample-value",
    "isSensitive": false
  }
]
```

| Field | Description |
| --- | --- |
| `appId` | The ID of your private app. This can be helpful if you have multiple private apps pointing to the same webhook URL. |
| `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 the event. |
| `portalId` | The [ID of the HubSpot account](https://knowledge.hubspot.com/account/manage-multiple-hubspot-accounts#check-your-current-account) where the event occurred. |
| `occurredAt` | When the event occurred as a unix timestamp (in milliseconds). |
| `subscriptionType` | The webhook subscription type. Can be one of the following:<ul><li>`object.creation`</li><li>`object.deletion`</li><li>`object.merge`</li><li>`object.restore`</li><li>`object.propertyChange`</li><li>`object.associationChange`</li></ul> |
| `attemptNumber` | Starting at `0`, which number attempt this is to notify your service of this event.<br /><br /> |
| `objectId` | The ID of the object that was created, changed, or deleted. For example, for a contact-related event, `objectId` would be the contact's ID. |
| `changeSource` | The source of the change. This can be any of the change sources that appear in contact property histories. |
| `objectTypeId` | The type of object that triggered the event. See the [full list of object type IDs](/guides/api/crm/understanding-the-crm#object-type-ids) for more information. |
| `propertyName` | The name of the property that was updated (for `object.propertyChange` events only). |
| `propertyValue` | The new property value that resulted from the change (for `object.propertyChange` events only). |
| `isSensitive` | Will be `true` if the property is a [sensitive data property](/reference/api/crm/sensitive-data). |
| `sourceId` | The ID of the source that triggered the event (e.g., a user ID if it was a user that updated the property data). |

The `object.associationChange` subscription will trigger for all associations, including custom association labels. Association change events will also trigger on both incoming and outgoing association changes. This means that an `object.associationChange` event defined for an `objectName` of `contact` will not only trigger on a `CONTACT_TO_DEAL` association change, but also on a `DEAL_TO_CONTACT` association change.

```json
[
  {
    "eventId": 668521389,
    "subscriptionId": 621550,
    "portalId": 123456,
    "appId": 3715530,
    "occurredAt": 1715708228603,
    "subscriptionType": "object.associationChange",
    "attemptNumber": 0,
    "changeSource": "USER",
    "associationType": "CONTACT_TO_DEAL",
    "associationCategory": "HUBSPOT_DEFINED",
    "associationTypeId": 4,
    "fromObjectId": 3788,
    "fromObjectTypeId": "0-3",
    "toObjectId": 4658499728,
    "toObjectTypeId": "0-1",
    "associationRemoved": true,
    "isPrimaryAssociation": false,
    "sourceId": "userId:864745280"
  },
  {
    "eventId": 2975980077,
    "subscriptionId": 621550,
    "portalId": 885814039,
    "appId": 5553555,
    "occurredAt": 1715708228603,
    "subscriptionType": "object.associationChange",
    "attemptNumber": 0,
    "changeSource": "USER",
    "associationType": "DEAL_TO_CONTACT",
    "associationCategory": "HUBSPOT_DEFINED",
    "associationTypeId": 3,
    "fromObjectId": 4658499728,
    "fromObjectTypeId": "0-3",
    "toObjectId": 3788,
    "toObjectTypeId": "0-1",
    "associationRemoved": true,
    "isPrimaryAssociation": false,
    "sourceId": "userId:864745280"
  }
]
```

Learn more about [fields included in `associationChange` event payloads](/guides/apps/public-apps/create-generic-webhook-subscriptions#association-events).

## Update your private app configuration to point to your webhooks JSON file

After you add your `webhooks.json` file and configure your webhooks, you'll need to update your `app.json` file to point to the webhooks JSON file.

The following `app.json` file provides an example of updating the `"webhooks"` field to point to your `webhooks.json` file, [using the file structure from the quick start guide](https://developers.hubspot.com/docs/guides/crm/private-apps/quickstart).

```json
{
  "name": "Get started App",
  "description": "This is an example private app that shows a custom card on the Contact record tab built with React-based frontend. This card demonstrates a simple handshake with HubSpot's serverless backend.",
  "uid": "get_started_app",
  "scopes": ["crm.objects.contacts.read", "crm.objects.contacts.write"],
  "public": false,
  "extensions": {
    "crm": {
      "cards": [
        {
          "file": "extensions/example-card.json"
        }
      ]
    }
  },
  "webhooks": {
    "file": "./webhooks/webhooks.json"
  }
}
```

## View webhook subscriptions in HubSpot

On the private app settings page in HubSpot, you can view a list of each event subscription for each subscription in the app's `webhooks.json` file.

To view your private app's webhook subscriptions in HubSpot:

- In your HubSpot account, navigate to **CRM Development**.
- In the left sidebar menu, click **Private apps**.
- Click the **name** of your private app.
- Click the **Webhooks** tab.

  

- Under _Event subscriptions_, you can view each of the app's webhook subscriptions.
- To view more information about a subscription, including number of times triggered and number of errors, click the **name** of the subscription.

  - Click the numbers in the **Total count** and **Errors** columns to navigate to the webhooks monitoring tab.
  - Hover over a subscription type then click **Details** to open the details panel on the right. This panel includes a sample event response payload and a testing feature.

    


The features described in this guide are in Early Access beta, separate from the _CRM development tools to build UI extensions with React as frontend_ beta for private app UI extension development.

[Request access to the UI extensions for public apps beta](https://developers.hubspot.com/build-app-cards)
# Create public apps using projects (BETA)

Public apps enable you to authenticate requests to HubSpot's APIs in your project using OAuth. Public apps can be installed in multiple accounts through a streamlined end-user flow, and can be submitted to HubSpot's App Marketplace. When creating using the developer projects framework, public apps can include UI extensions in addition to other app extensions, such as timeline extensions and workflow actions.

Below, learn how to create and configure a public app using projects.

## Prerequisites

Before getting started, ensure you've also set up the following:

- [Set up your local environment](/guides/crm/setup) for project development.
- Create a [developer account](/getting-started/account-types#app-developer-accounts).
- In your developer account, [create a test account](/getting-started/account-types#create-a-developer-test-account) that you can use to install and test the app.

- Because you'll be using OAuth, you'll need to set up a self-hosted backend environment to complete the OAuth process. If you haven't set up with OAuth with HubSpot before, it's recommended to follow the [OAuth quickstart guide](/guides/apps/authentication/oauth-quickstart-guide). That guide uses a HubSpot-provided [Node.js OAuth quickstart app](https://github.com/HubSpot/oauth-quickstart-nodejs) which is configured to work with the example project you'll be creating in this tutorial.

## Create an app

Using the CLI, you can create a project with a public app using any of the following methods:

- Follow the [quickstart guide](/guides/crm/public-apps/quickstart) to create an app from a boilerplate project template, which includes a card for contact records.
- [Migrate an existing public app](/guides/crm/public-apps/migrate-a-public-app-to-projects) to the developer projects framework.
- Create a public app from scratch locally with a developer project.

To create a public app from scratch:

- In the terminal, navigate to the directory that you'll be storing your project files in.

```shell
cd path/to/working/directory
```

- Since you'll need to deploy your project to your app developer account, set it as the default account by running `hs accounts use <accountName>` and selecting your developer account.
- Run `hs project create` to create a new project.

```shell
hs project create
```

- Follow the prompts in the terminal to enter a **name** and **location** for the project.
- When prompted to choose a template, select **Create an empty project (no template)**.
- With your project created, navigate into the newly created directory using the `cd` command.

```shell
cd <project-directory-name>
```

- Create the following files and directories:
  - In the src directory, create an `app` directory.
  - In `src/app`, create a `public-app.json` file. Below is the minimum schema required for uploading an app. View the full `public-app.json` schema in the [App configuration](#app-configuration) section below.
The `uid` value must be a unique value, and cannot be changed after upload. Changing this name later will result in a new app being created and associated with the project on upload. This value is an internal name and won't be exposed to end-users.
```json
// Example public-app.json
{
  "name": "App name here",
  "uid": "immutable-unique-app-id-here",
  "description": "An example public app built with developer projects.",
  "auth": {
    "redirectUrls": ["http://localhost:3000/oauth-callback"],
    "requiredScopes": [
      "crm.objects.contacts.read",
      "crm.objects.contacts.write",
      "oauth"
    ]
  }
}
```

- After saving the file, upload it to your developer account by running `hs project upload`. Then, follow the prompts in the terminal to create the project in your account.

```shell
hs project upload
```

## App configuration

Public apps built with projects include a `public-app.json` file to configure the app's details, such as its name, scopes, included extension files, and more.

The `public-app.json` file uses the following schema:

```json
// Example public-app.json
{
  "name": "Getting Started App",
  "uid": "getting-started-app-uid",
  "description": "An example project of how to build a Public App with Developer Projects",
  "logo": "./my-company.png",
  "allowedUrls": [
    "https://app.example.com",
    "https://app.example2.com:8080",
    "https://api.example.com/user/",
    "https://api.example2.com/user"
  ],
  "auth": {
    "redirectUrls": ["http://localhost"],
    "requiredScopes": [
      "crm.objects.contacts.read",
      "crm.objects.contacts.write"
    ],
    "optionalScopes": [],
    "conditionallyRequiredScopes": []
  },
  "support": {
    "supportEmail": "support@example.com",
    "documentationUrl": "https://example.com/docs",
    "supportUrl": "https://example.com/support",
    "supportPhone": "+18005555555"
  },
  "extensions": {
    "crm": {
      "cards": [
        {
          "file": "./extensions/example-card.json"
        }
      ]
    }
  },
  "webhooks": {
    "file": "./webhooks.json"
  }
}
```

| Parameter | Type | Description |
| --- | --- | --- |
| `name` | String | The app's name. Can be any string up to 200 characters. Must not start or end with a whitespace character. |
| `uid` | String | The app's unique ID. Can be any string up to 64 characters. Characters can be uppercase or lowercase, and can include numbers, underscores (`_`), dashes (`-`), and periods (`.`). |
| `description` | String | Describes what the app does for the installing user. Can be any string up to 8192 characters. |
| `logo` | String | The file path for the app's logo, which appears next to the app's name. Must be a valid image with one of the following extensions:<ul><li>png</li><li>jpeg/jpg</li><li>gif</li><li>bmp</li></ul>The image must be less than 5,000,000 bytes (roughly 5MB). |
| `allowedUrls` | Array | An array containing the URLs that the React app is allowed to call. URLs must use the HTTPS scheme and must contain an [authority](https://developer.mozilla.org/en-US/docs/Learn_web_development/Howto/Web_mechanics/What_is_a_URL#authority), followed by an optional path prefix if needed. You do not need to add any override URLs specified in `local.json` to this array.A request URL will match an allowed URL match when:<ul><li>The scheme is HTTPS</li><li>The authority is an exact match</li><li>The path starts with the allowed URL's path prefix if specified.</li></ul>Query parameters are ignored. Regex is not supported.In the above example code, the following request URLs will be allowed:<ul><li>https://app.example.com</li><li>https://app.example.com/</li><li>https://app.example.com/home</li><li>https://app.example2.com:8080/</li><li>https://api.example.com/user/12345</li><li>https://api.example2.com/users</li></ul>And the following request URLs will <u>not</u> be allowed:<ul><li>http://localhost</li><li>http://app.example.com</li><li>https://example.com</li><li>https://app.example.com.us</li><li>https://app.example2.com</li><li>https://api.example.com/users</li><li>https://api.example2.com/v2/users</li></ul>The local development server will log a warning if the UI extension requests a URL that doesn't match any of the `allowedUrls`, but will allow the request through. |
| `redirectUrls` | Array | An array of strings containing the URLs that the OAuth process is allowed to reroute back to. Each app must have at least one auth redirect URL, and it must use HTTPS. The only exception is that `http://localhost` is allowed for testing. |
| `advancedScopeSettingsEnabled` | Boolean | Configures when [optional and conditionally required scopes](/guides/apps/public-apps/overview#configure-scopes) can be added to your app's auth configuration. When set to `false`, new scopes for conditionally required or optional scopes cannot be added or enforced (default is `true`).**Please note:** you must set this field to `true` (or leave unspecified) and declare your app's use of optional and conditionally required scopes when seeking update approval for your App Marketplace app after adding a UI extension. |
| `requiredScopes` | Array | An array of strings listing your app's required scopes. Each app must include at least one scope, and the installing user must grant these scopes to successfully install the app. See [full list of scopes](/guides/apps/authentication/scopes). |
| `optionalScopes` | Array | An array of strings listing your app's optional scopes. These scopes can be excluded from the authorization during installation if the account or user installing the app doesn't have the proper permissions. In that case, the scope will not be included in the resulting refresh token or access token. See [full list of scopes](/guides/apps/authentication/scopes). |
| `supportEmail` | String | A valid email address that users can contact for support. |
| `documentationUrl` | String | The external URL that users can navigate to for supporting documentation. Must use HTTPS. |
| `supportUrl` | String | The external URL that users can navigate to for additional support. Must use HTTPS. |
| `supportPhone` | String | The phone number that users can contact for support. Must start with a plus sign (`+`). |
| `extensions` | Object | An object that defines the path to the relevant UI extension configuration file for each app card. Multiple extensions can be included as comma-separated `file` objects in the `cards` array. |
| `webhooks` | Object | An object that defines the path to the app's webhooks configuration file. |

### Managing app configuration

When building public apps using projects, some features must be updated locally in your project files, while others must be managed in the app settings UI in HubSpot.

The following app details can only be managed in your local project files:

- App name, logo, and description ([public.app.json](#app-configuration)).
- Contact and support information ([public.app.json](#app-configuration)).
- Auth redirect URLs, scopes and scope settings ([public.app.json](#app-configuration)[](#public-app-json-schema)).
- UI extension functionality (`ExtensionFile.jsx`/`.tsx`)
- Webhooks and webhook settings ([webhooks.json](/guides/crm/public-apps/webhooks#defining-webhook-subscriptions))

All other public app features, such as timeline events, will need to be maintained using the in-app app settings page in HubSpot. When a setting can only be managed locally, the app settings page will disable the field and display a lock icon, which you can hover over for more information.
## View your app in HubSpot

To view and manage your app in HubSpot, including monitoring usage:

- In the left sidebar of your developer account, navigate to **Apps**.
- Click the **name** of the app.
- On the _App Info_ tab, you can view basic information about your app, such as the app name and description. Fields that are greyed out with a lock icon can only be updated locally.

  

- Click the **Auth** tab to view the app's authentication information, such as _App ID_, _Client ID_, _Client secret_, _Install URL_, and scopes.
- In the left sidebar, navigate to **Contact & Support** to view the app's company information and support details. On this tab, you can also [set up a verified domain](/guides/apps/public-apps/overview#add-a-verified-domain).
- In the left sidebar, navigate to **Monitoring** to [monitor your app's usage data](/guides/apps/public-apps/overview#monitor-app-behavior). For public apps built with projects, you can also [view detailed UI extension logs](#view-ui-extension-logs).
- In the left sidebar, you can also view the app's features, including:
  - [Classic CRM cards](/guides/api/crm/extensions/crm-cards) (separate from the app cards you can build with UI extensions)
  - [UI extensions](/guides/crm/ui-extensions/create)
  - [Timeline events](/guides/api/crm/extensions/timeline)
  - [Webhooks](/guides/crm/public-apps/webhooks)
  - [App settings page](/guides/apps/public-apps/create-an-app-settings-page)

### View UI extension logs

On the _Monitoring_ page, you can view logs for the app's UI extensions, including logs for extension rendering and `hubspot.fetch()` requests. To view an app's UI extension logs:

- In the left sidebar of your developer account, navigate to **Apps**.

  

- Click the **name** of the app.
- On the app settings page, in the left sidebar, click **Monitoring**.
- Click the **UI extensions** tab.

  

- In the table, you can view all logs for the app's included UI extensions. To filter logs by specific criteria, use the **Filter by** dropdown menus:
  - **Result:** filter logs by the type of result, including error and success logs for extension rendering and `hubspot.fetch()` requests.
  - **CRM object type:** filter logs by the type of CRM object associated with the event.
  - **Extension name:** filter logs by extension, which can be useful when your app includes multiple extensions.
- To view more details for an event, click the **event**, then view the right panel.

  - You can use the _Log ID_ or _Request ID_ to search for the event by ID on the _UI extensions_ tab.

    

  - You can use the _Trace ID_ or _Traceparent_ value to search for the event on the _Log traces_ tab. Alternatively, you can click **View log trace** to navigate directly to the event on the _Log traces_ tab.

    

Learn more about log traces below.

### Log traces

When troubleshooting errors, log traces are helpful for quickly debugging issues. You can [navigate to log traces directly from an event](#log-traces) on the _UI extensions tab_ by clicking **View log trace**, or by searching for the event by trace ID on the _Log traces_ tab.
When viewing the log trace for an event, you'll see a more detailed breakdown of the event, including error details, request timing, and more, in the _Log details_ section.
### Deleting a project-based public app

To delete a public app that was created in a project, you'll first need to delete the project and its association to the app.

To delete a project associated with a public app:

- In the left sidebar of your developer account, navigate to **Projects**.

  

- Click the **name** of the project.
- Click the **Settings** tab, then click **Delete \[project name\]**.

  

- In the dialog box, enter the **project name** and click **Delete project**.

With the project deleted, you can delete the app:

- In the left sidebar of your developer account, click **Apps**.
- Click the **name** of the app.
- On the _App info_ tab, in the _Delete this app_ section, click **Delete \[app name\]**.

  

- In the dialog box, click the **Export your logs** link to export a record of the app's activity. Then, select the **I've exported the logs** checkbox and click **Delete**.


The features described in this guide are in Early Access beta, separate from the _CRM development tools to build UI extensions with React as frontend_ beta for private app UI extension development.

[Request access to the UI extensions for public apps beta](https://developers.hubspot.com/build-app-cards)
# Fetching data for public apps (BETA)

While private apps can use serverless functions to fetch data, when you build UI extensions inside public apps, you need to bring your own REST-based back-end and use the `hubspot.fetch()` API to fetch data.

Requests made with `hubspot.fetch()` are subject to the following limits:

- Each app is allowed up to 20 concurrent requests per account that it's installed in. Additional requests will be rejected with the status code `429`, and can be retried after a delay.
- Each request has a maximum timeout of 15 seconds. Both request and response payloads are limited to 1MB. You can specify a lower timeout per request using the `timeout` field. Request duration time includes the time required to establish an HTTP connection. HubSpot will automatically retry a request once if there are issues establishing a connection, or if the request fails with a `5XX` status code within the 15 second window.

Below, learn more about using the `hubspot.fetch()` API along with examples.

## Method

The method contract for `hubspot.fetch()` is as follows:

```js
import { hubspot } from '@hubspot/ui-extensions';

interface Options {
  method?: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'PATCH';
  timeout?: number;
  body?: {
    [key: string  | number]: unknown;
  }
}

hubspot.fetch(resource: string | URL): Promise<Response>
hubspot.fetch(resource: string, options?: Options): Promise<Response>
```

| Parameter | Type | Description |
| --- | --- | --- |
| `method` | String | The method to use. |
| `timeout` | Number | Time in milliseconds to allow for the request to complete before timing out. Timeout will occur when the back-end request duration exceeds this value <u>or</u> 15 seconds—whichever is smaller. Request duration time includes the time required to establish an HTTP connection.HubSpot will retry a request once if there are issues establishing a connection, or if the request fails with a `5XX` status code within the 15 second window. |
| `body` | Object | The request body. |
`hubspot.fetch()` does <u>not</u> support request and response headers. The HubSpot signature must be used to perform authentication/authorization in your back-end. For security reasons, you should <u>not</u> store secrets in your React code to communicate with third-party back-ends. Instead, use your own back-end to fetch data from other third-party APIs after [validating the HubSpot signature](/guides/apps/authentication/validating-requests) on the request.
## Specify URLs to fetch from

To enable your extension to request data from a URL, you'll first need to add it to the `allowedUrls` array in the [public-app.json file](/guides/crm/public-apps/creating-public-apps#app-configuration). URLs not included in this array cannot be requested by the extension.

```json
// Example public-app.json
{
  "name": "My public app",
  "uid": "my-public-app",
  "description": "This is my public app.",
  "logo": "./my-company.png"
  "allowedUrls": [
    "https://app.example.com",
    "https://app.example2.com:8080",
    "https://api.example.com/user/",
    "https://api.example2.com/user"
  ]
 ...
}
```
When running the local development server, any `hubspot.fetch()` request will still go to your hosted back-end via a HubSpot-managed data fetch service, and the request URL must be on the allowlist in `public-app.json`. If you need to update the allowlist, you'll need to run `hs project upload` for the change to take effect.
## Headers and metadata

To ensure that the requests hitting your back-end are coming from HubSpot, several headers are included in the request. You can use these headers, along with the incoming request fields, to verify the signature of the request. Learn more about [validating HubSpot requests](/guides/apps/authentication/validating-requests).

HubSpot will always populate headers related to request signing and also allow you to pass an `Authorization` header from `hubspot.fetch()`. See the [example hubspot.fetch() request with Authorization header](#authorization-header) for more information.
While you can use `hubspot.fetch()` to pass an `Authorization` request header, `hubspot.fetch()` does <u>not</u> pass other client-supplied request headers or return response headers set by your back-end server.
HubSpot also automatically adds the following query parameters to each request to supply metadata: `userId`, `portalId`, `userEmail`, and `appId`. As the request URL is hashed as part of the signature header, this will help you securely retrieve the identity of the user making requests to your back-end.
If you're not seeing appended metadata passed with `hubspot.fetch()` requests, check whether you have a `local.json` file that's currently rerouting requests via a proxy. If so, disable this local data fetch feature by renaming the file to `local.json.bak` and restarting the development server.
## Proxying requests to a locally running back-end

If you have a locally running back-end, you can set up a proxy to remap `hubspot.fetch()` requests made during local development. This proxy is configured through a `local.json` file in your project, and will prevent requests from being routed through HubSpot's data fetch service.

To proxy requests to a locally running back-end:

- Create a `local.json` file in the same directory as your `public-app.json` file. In this file, define a proxy that remaps requests made using `hubspot.fetch()`. This mapping will only happen for the locally running extension. You can include multiple proxy key-value pairs in the `proxy` object.

```json
// Example local.json file
{
  "proxy": {
    "https://example.com": "http://localhost:8080"
  }
}
```
- Each proxy URL must be a valid URL and use HTTPS.
- Path-based routing is not supported. For example, the following proxy won't work: `"https://example.com/a": "http://localhost:8080"`
- Upload your project by running `hs project upload`.
- With your project uploaded, run `hs project dev` to start the development server. The CLI should confirm that it has detected your proxy.

  

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

  

### Request signatures during proxied local development

By default, when proxying requests during local development, requests will <u>not</u> be signed or include metadata in query parameters. However, if you want to introduce request signing into the local development process, you can inject the `CLIENT_SECRET` environment variable into the local development process.

#### Injecting CLIENT_SECRET

After setting up your `local.json` file to proxy specific domains, you can inject the `CLIENT_SECRET` variable when starting the local development server by prepending the `hs project dev` command with the variable:

```shell
CLIENT_SECRET="abc123" hs project dev
```

Note that this doesn't have to be a real `CLIENT_SECRET` value, as long as you inject the same variable in your locally running back-end that you're using for `hs project dev`. For example, your back-end might include the following Node or Python code:
```js
CLIENT_SECRET="abc123" node my-app.js
# and also
CLIENT_SECRET="abc123" npm run dev
```
```py
CLIENT_SECRET="abc123" python my-app.py
```
And to access the `CLIENT_SECRET` variable:
```js
console.log(process.env.CLIENT_SECRET);
```
```py
import os
print(os.environ['CLIENT_SECRET'])
```
Once you've finalized your request signing logic and have permanently added it to your back-end code, you'll need to inject the `CLIENT_SECRET` variable from your app into the `hs project dev` command permanently. For ease of use, consider baking the variable into your start scripts for local development.

To validate HubSpot request signatures in your custom back-end, check out the [request validation guide](/guides/apps/authentication/validating-requests). You can also use HubSpot's [`@hubspot/api-client`](https://www.npmjs.com/package/@hubspot/api-client) npm module to verify requests and sign them yourself. For example:

```js
import { Signature } = from '@hubspot/api-client'

const url = `${req.protocol}://${req.header('host')}${req.url}`
const method = req.method;
const clientSecret = process.env.CLIENT_SECRET
const signatureV3 = req.header('X-HubSpot-Signature-v3');
const timestamp = req.header('X-HubSpot-Request-Timestamp');

// Reject the request if the timestamp is older than 5 minutes.
if (parseInt(timestamp, 10) < (Date.now() - 5 * 60 * 1000)) {
  throw Error('Bad request. Timestamp too old.')
}

const requestBody = req.body === undefined || req.body === null
  ? ''
  : req.body;

const validV3 = Signature.isValid({
  signatureVersion: 'v3',
  signature: signatureV3,
  method,
  clientSecret,
  requestBody,
  url,
  timestamp,
});

if (!validV3) {
  throw Error('Bad request. Invalid signature.')
}
```

## hubspot.fetch examples

Below are examples of `hubspot.fetch` requests to illustrate promise chaining, async/await, and authorization header usage.

### Promise chaining

```js
import React, { useEffect } from 'react';
import { hubspot, Text } from '@hubspot/ui-extensions';

hubspot.extend(({ context }) => <Hello context={context} />);

const Hello = ({ context }) => {
  useEffect(() => {
    let url = 'https://run.mocky.io/v3/98c56581'; // replace with your own
    hubspot
      .fetch(url, {
        timeout: 2_000,
        method: 'GET',
      })
      .then((response) => {
        console.log('Server response:', response.status);
        response
          .json()
          .then((data) => console.log(data))
          .catch((err) => console.error('Failed to parse as json', err));
      })
      .catch((err) => {
        console.error('Something went wrong', err);
      });
  }, []);

  return <Text>Hello world</Text>;
};
```

### Async/await

```js
import React, { useEffect } from 'react';
import { hubspot, Text } from '@hubspot/ui-extensions';

hubspot.extend(({ context }) => <Hello context={context} />);

const Hello = ({ context }) => {
  useEffect(() => {
    const fetchData = async () => {
      let url = 'https://run.mocky.io/v3/98c56581'; // replace with your own
      const response = await hubspot.fetch(url, {
        timeout: 2_000,
        method: 'GET',
      });
      console.log('Server response:', response.status);
      try {
        const data = await response.json();
        console.log(data);
      } catch (err) {
        console.error('Failed to parse as json', err);
      }
    };
    fetchData().catch((err) => console.error('Something went wrong', err));
  }, []);

  return <Text>Hello world</Text>;
};
```

### Authorization header

You may return a short-lived authorization token from your back-end service after validating the HubSpot signature. You can then use this token to access other resources.

To get the access token from your back-end server in the UI extension:

```js
hubspot.fetch(`${BACKEND_ENDPOINT}/get-access-token`, {
  timeout: 3000,
  method: 'GET',
})
.then((response: Response) => {
  response.json().then((data) => setAccessToken(data.accessToken));
})
```

To return a short-lived access token from your back-end server:

```js
app.get('/get-access-token', (req, res) => {
  validateHubspotSignatureOrThrow(req);
  res.json({
    accessToken: generateShortLivedAccessToken(req.query.userEmail),
    expiryTime: addMinutes(currentTime, 10),
  });
});
```

To attach access tokens to other UI extension requests:

```js
hubspot.fetch('https://www.oauth-enabled-api.com/', {
  timeout: 3000,
  method: 'GET',
  headers: {
    'Authorization': `Bearer ${accessToken}`
  }
}).then((response: Response) => {
  ...
})
```


The features described in this guide are in Early Access beta, separate from the _CRM development tools to build UI extensions with React as frontend_ beta for private app UI extension development.

[Request access to the UI extensions for public apps beta](https://developers.hubspot.com/build-app-cards)
# Migrate a public app to the developer projects framework (BETA)

Using the CLI, you'll migrate an existing production public app to the projects framework. App features that are supported by projects, such as auth configuration and webhooks, will be included automatically in the migration. Features not supported by projects, such as custom workflow actions and timeline events, can be configured through the app settings UI or API as before.

## Prerequisites

This guide assumes that you meet the following criteria:

- You have an existing public app that has not yet been converted to the developer projects framework.
- You are ready to migrate your public app to the developer projects framework, enabling the creation of UI extensions for your app. If you want to start with a dry run of the migration, you can instead make a copy of your app in a project by running `hs project clone-app`.
- You have [set up your local environment for project development](/guides/crm/setup) and set your developer account as the default in the CLI.

  - To view your connected accounts, run `hs accounts list` in the terminal. The terminal will list all currently connected accounts, along with the default.

    

  - If your developer account is connected but not the default, run `hs accounts use <accountName>`.
  - If you haven't connected your developer account to the CLI yet, revisit the [setup guide](/guides/crm/setup).

## Migrate to the projects framework

To begin migration, you'll switch your public app to be configured using projects. This process will preserve the original auth credentials, and all other existing app features, App Marketplace listings, and app installs. No changes are required in your app backend, and customers will not experience any interruptions in service.

- Ensure you've installed the latest version (v6.1.0 or greater) of the HubSpot CLI by running `npm i -g @hubspot/cli@latest`.

```shell
npm i -g @hubspot/cli@latest
```

- With your developer account connected to the CLI and set as the default, run `hs project migrate-app`.

```shell
hs project migrate-app
```

- Select the **public app** that you'd like to migrate. This will create a new project containing a single public app component that represents the current state of your app.
- Enter a **name** and **local path** for your project, then press **Enter**. The terminal will then display a message to outline the migration process and confirm your intention to convert the app.
- Press **Enter** to confirm that you're ready to proceed with migration.
- The migration process will then begin, and the terminal will display the migration status. The migration includes:
  - Creating the project in your app developer account.
  - Converting the project-supported features of the app to source code files, which you can use int he future to update feature configuration.
  - Building and deploying the new project (Build #1), which completes the association between the public app and its project. This will preserve the original auth credentials, all app features, App Marketplace listing, and installs.
  - Downloading the new project source files to the specified local directory.
With build #1 succeeded, you'll now have captured your original app's configuration state. As you continue to build your app and UI extensions, you can always safely revert to this state by redeploying build #1 by running the `hs project deploy --buildId=1` command.

Before you can begin UI extension development and run a local development server, deploy the successful build by running `hs project deploy`.

```shell
hs project deploy
```
After migration, features defined in the project source code will no longer be editable through HubSpot’s app management UI or developer APIs. You’ll instead need to manage those locally through the project using `hs project upload`. Other features, such as custom workflow actions or timeline events, can continue to be managed in the developer account app settings UI as before.
With the app successfully migrated, you can now update the public app with app cards.

## Add new app cards

After the migration is complete, you can add [UI extensions](/guides/crm/ui-extensions/create) to it to customize the HubSpot UI with app cards, along with any other required app feature definition changes from your local development project to the new project-based public app. For guidance around building UI extensions, you can:

- Follow the [public app quickstart guide](/guides/crm/public-apps/quickstart) to see HubSpot's boilerplate project template, which includes a simple app card.
- Check out [HubSpot's sample UI extensions](/guides/crm/ui-extensions/sample-extensions/overview).
- Review the utilities available in the [UI extensions SDK](/guides/crm/ui-extensions/sdk).
- Browse the available [UI components](/reference/ui-components/overview) for building the UI of your cards.

While developing a UI extension, you can start a [local development server](/guides/crm/ui-extensions/local-development) to see your changes in the browser in real-time without needing to upload. To enable local development:

- Install dependencies by running `npm i` in the `src/app/extensions` directory.
- [Add the card to the UI](/guides/crm/ui-extensions/create#adding-cards-to-the-ui) location through the account's settings. For apps that are currently listed on the App Marketplace, you'll need to complete this step after [setting the test account feature flag](#set-test-account-feature-flag).

## Workflow for unlisted apps

If your app is not listed in the App Marketplace, you can begin developing the new app card functionality without needing to set any feature flags on your account. If you do want to selectively control which accounts can access your app's new cards, you can use the [feature flag API](/reference/api/app-management/feature-flags) to control each account's `flagState`.

If your app is listed in the App Marketplace, learn more about the [development workflow for listed apps](#workflow-for-app-marketplace-apps).

- [Enable local development](#enable-local-development), then start the local development server by running `hs project dev` in the terminal.

```shell
hs project dev
```

- Continue building out your UI extension. The local development server will pick up any changes saved in the React extension file. If you make any changes to non-React files, such as the .json config files, you'll need to end the server and run the `hs project upload` command.
- When you're satisfied with your changes, upload and build the project by running `hs project upload`.

```shell
hs project upload
```

- Deploy all updates by running `hs project deploy`, or through the project management UI in HubSpot.

```shell
hs project deploy
```
Alternatively, you can run both upload and deploy commands at the same time by running `hs project upload && hs project deploy`. You can also [enable automatic deploy on successful build](/guides/crm/developer-projects/create-a-project#view-the-project-in-hubspot) in the project's settings.
## Workflow for App Marketplace apps

If your app is listed in the App Marketplace, your migrated app will automatically have a feature flag enabled that controls access to app cards in the accounts where the app is installed. You'll need to use the [feature flag API](/reference/api/app-management/feature-flags) to selectively enable accounts to use the new app cards. By default, app cards for listed public apps are restricted to a maximum of <u>five</u> accounts.

### 1. Set test account feature flag

To start developing new app cards without impacting production accounts, first [create a test account](/getting-started/account-types#developer-test-accounts) within your developer account if you haven't done so yet. Then, using your developer account's developer API key, set your test account's feature flag to `ON` by making a request to the [feature flag API](/reference/api/app-management/feature-flags).

```shell
curl -XPUT -s --json '{"flagState": "ON"}' 'https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-release-app-cards/portals/{portalId}?hapikey={developerApiKey}'
```

With your test account feature flag configured, you'll be able to view new app cards in HubSpot as you develop your UI extensions.

### 2. Begin local development

- [Enable local development](#enable-local-development), then start the local development server by running `hs project dev` in the terminal.

```shell
hs project dev
```

- Select your developer test account, then press **Enter**. The terminal will update with a message displaying the status of the development server. You can press **q** at any time to end the server.
If you receive an `Unable to find extensions files` error when attempting to start the local development server, ensure that you've added the [extensions definition in your `public-app.json` file](/guides/crm/public-apps/creating-public-apps#app-configuration).
- When you're satisfied with your updates, upload your changes to your developer account.

```shell
hs project upload
```

- When the build succeeds, deploy your changes by running `hs project deploy`.

```shell
hs project deploy
```
Alternatively, you can run both upload and deploy commands at the same time by running `hs project upload && hs project deploy`. You can also [enable automatic deploy on successful build](/guides/crm/developer-projects/create-a-project#view-the-project-in-hubspot) in the project's settings.
### 3. Continue testing and submit for review

You can continue testing your app and its new cards in additional accounts by using the feature flag API to set the `flagState` to `ON`, as shown in the first step above. You can enable this flag for up to five accounts, including beta customers.
If at any point you want to start over, you can always safely revert the app by redeploying build #1 with the `hs project deploy --buildId=1` command.
Once you've sufficiently tested your updated app, you'll need to submit it for review by emailing the following required information to [app-card-review@hubspot.com](mailto:app-card-review@hubspot.com):

- Production app ID
- Build ID
- Hub ID of your developer account
- Hub ID of the test account where the app is installed
- Credentials or access to any third-party platform necessary to test the app card, if applicable
- Demo video or detailed instructions of how users will interact with the card (such as a quick [Loom](https://www.loom.com/))
- Additional context or details HubSpot might need that aren't covered by the above

After submitting your app for review, invite `app-card-review@hubspot.com` as a user to your test account where the app is installed. Grant this user all the permissions necessary to fully test your app cards (for simplicity, Super Admin is recommended). A member of the review team will then test your app card and provide feedback if changes are needed. You'll continue to work with app-card-review@hubspot.com to address provided feedback until all additional app card criteria are met.

Once your app is approved, you can begin distributing it in the App Marketplace, and you will not need to resubmit it for approval when making future changes.

### 4. Distribute to the App Marketplace

After receiving approval for unlimited distribution from the App Card marketplace review process, your app will be ready for distribution. Before proceeding with app distribution, it's recommended to consider the following:

- If your app previously included a classic CRM card in the sidebar, consider [adding card update guidance](#add-card-update-guidance) so that users can seamlessly add your new cards and remove the old ones.
- It’s recommended to [gradually roll out your app card updates](#gradually-roll-out-your-app-cards) to catch any issues before distributing to the full install base. You can also [roll out to all installed accounts](#roll-out-to-all-accounts-simultaneously) at the same time.
- As your install base begins using your new app cards, you can [hide classic CRM cards](#4.d-hiding-classic-crm-cards) from the onboarded accounts. The final step of migration is to delete the old cards from the app.

#### 4.a Add card update guidance

To help users migrate to your new cards, HubSpot includes a default update state that you can apply to your classic cards. This update includes messaging to indicate that the card has available updates.

For account admins, a link to the app's settings page will be included. Non-admin users will see similar messaging, but will be guided to contact their account admin to assist with setup.

| Admin view | Non-admin view |
| --- | --- |
|  |

To add this state to your classic cards, include `"showMigrationMessage": "true"` in the card's JSON response. This should be included at the top-level of the response.

```json
response.status(200).send({"showMigrationMessage": "true", "results": [crmCardDetails]});
```

Alternatively, you can build your own custom update state by manually updating the card's JSON response directly, or even using the feature flag API for conditional responses.

With the default update state enabled for your classic cards, their content will be replaced, and you can walk through what the end-user experience will be for super admins who want to upgrade to your new cards:

- In the test account, navigate to a CRM record that contains your classic card.
- On the CRM record, locate the card, then click the **Set up now** link to navigate to the app settings page.

  

- The app settings page will display all available cards. Click the **link** provided for each card to navigate to the customization page for that object.
- Users can then proceed to customize their record pages as needed. They can find your new cards within the _Apps_ section of the customization sidebar.
- After adding the new card, the old card can be removed by locating it in the view editor then clicking **Remove**. Alternatively, you can [hide the old card from the account](#4.d-hiding-classic-crm-cards) using the feature flag API.

  

Learn more about the card updating user experience on [HubSpot's Knowledge Base](https://knowledge.hubspot.com/integrations/install-and-manage-app-cards).

#### 4.b Gradually roll out app cards

Using the [feature flag API](/reference/api/app-management/feature-flags), you can gradually roll out your app in two ways:

- [Roll out app cards to new installs](#roll-out-to-new-installs)
- [Roll out app cards to a subset of accounts](#roll-out-to-a-subset-of-accounts)

##### Roll out to new installs

To roll out your app cards to new installs only, you'll use the [feature flag API](/reference/api/app-management/feature-flags) to turn off the `hs-release-app-cards` feature flag for all existing installs. Then, you'll switch the flag's state to `ON` so that new installs have the flag enabled by default.
  The `hs-release-app-cards` flag will apply for all app cards included in the
  app. You cannot selectively release individual cards within an app.
- For the first request, set the `flagState` to `OFF` for all accounts that currently have the app installed. You'll need to gather the `portalId` for all existing installed accounts, then make a `POST` request to `https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-release-app-cards/portals/batch/upsert?hapikey={developerAPIKey}`.
  

```json
// Example POST to https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-release-app-cards/portals/batch/upsert?hapikey={developerAPIKey}
{
  "portalStates": [
    {
      "portalId": 1234,
      "flagState": "OFF"
    },
    {
      "portalId": 4567,
      "flagState": "OFF"
    },
    {
      "portalId": 78910,
      "flagState": "OFF"
    }
  ]
}
```

  
```shell
curl --request POST \
  --url 'https://api.hubapi.com/feature-flags/v3/0/flags/hs-release-app-cards/portals/batch/upsert?hapikey={developerAPIKey}' \
  --header 'content-type: application/json' \
  --data '{
    "portalStates": [
      {
        "portalId": 1234,
        "flagState": "OFF"
      },
      {
        "portalId": 4567,
        "flagState": "OFF"
      },
      {
        "portalId": 78910,
        "flagState": "OFF"
      }
    ]
  }'
```
- With existing app installs set to `OFF`, you can now set the `defaultState` to `ON`. To do so, make a `PUT` request to `https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-release-app-cards?hapikey={developerAPIKey}`
```json
// Example PUT to https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-release-app-cards?hapikey={developerAPIKey}
{
  "defaultState": "ON"
}
```
```shell
curl -- request PUT \
  --url 'https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-release-app-cards?hapikey={developerAPIKey}' \
  --header 'content-type: application/json' \
  --data '{
    "defaultState": "ON"
  }'
```
- Once you feel confident in new customer adoption, you can begin to remove customer `portalIds` that you switched to the `OFF` state by making a `POST` request to `https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-release-app-cards/portals/batch/delete?hapikey={developerAPIKey}` with the account IDs you want to remove from the `OFF` list.
```json
// Example POST to https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-release-app-cards/portals/batch/delete?hapikey={developerAPIKey}
{
  "portalIds": [1234, 4567, 78910]
}
```
```shell
  curl --request POST  \
    --url 'https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-release-app-cards/portals/batch/delete?hapikey={developerAPIKey}' \
    --header 'content-type: application/json; \
    --data '{
      "portalIds": [
        1234,
        4567,
        78910
      ]
    }'
```
To check which accounts still have their flag set to `OFF`, you can make a `GET` request to `/flags/hs-release-app-cards/portals/?hapikey={developerAPIKey}`. Learn more in the [feature flag API reference documentation](/reference/api/app-management/feature-flags).
- When all previously added accounts have been deleted using the above endpoint, you''ll have successfully completed the rollout and finished migration. At this point, you can safely delete your feature flag by making a `DELETE` request to `https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-release-app-cards?hapikey={developerAPIKey}`.

```shell
curl --request DELETE \
  --url 'https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-release-app-cards?hapikey={developerAPIKey}'
```

#### Roll out to a subset of accounts

Alternatively, rather than starting with new installs, you can selectively enable your app cards for a subset of accounts that have your app installed. To do so, you'll need to have a list of the account IDs for all existing public app installs. With that list, you can make a `POST` request to `https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-release-app-cards/portals/batch/upsert?hapikey={developerAPIKey}` and set their `flagState` to `ON`.
```json
{
  "portalStates": [
    {
      "portalId": 1234,
      "flagState": "ON"
    },
    {
      "portalId": 4567,
      "flagState": "ON"
    },
    {
      "portalId": 78910,
      "flagState": "ON"
    }
  ]
}
```
```shell
curl --request POST \
  --url 'https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-release-app-cards/portals/batch/upsert?hapikey={developerAPIKey}' \
  --header 'content-type: application/json' \
  --data '{
    "portalStates": [
      {
        "portalId": 1234,
        "flagState": "ON"
      },
      {
        "portalId": 4567,
        "flagState": "ON"
      },
      {
        "portalId": 78910,
        "flagState": "ON"
      }
    ]
  }'
```
You can continue making this request for subsets of accounts until all accounts have been migrated. Then, you can delete your feature flag by making a `DELETE` request to `https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-release-app-cards?hapikey={developerAPIKey}`.

```shell
curl --request DELETE \
  --url 'https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-release-app-cards?hapikey={developerAPIKey}'
```

#### 4.c Roll out to all accounts simultaneously

To release your app cards to all installed accounts simultaneously, delete the app's feature flag by making a `DELETE` request to `https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-release-app-cards?hapikey={developerAPIKey}`.

```shell
curl --request DELETE \
  --url 'https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-release-app-cards?hapikey={developerAPIKey}'
```

#### 4.d Hiding classic CRM cards

In addition to using the feature flags API to roll out app cards to your install base, you can also use it to facilitate the removal of classic CRM cards by setting the `hs-hide-crm-cards` flag. For example, for existing installs, you may want to continue showing the old cards to remind users to upgrade to your new cards, but then hide them from new installs and accounts that have upgraded.
  The `hs-hide-crm-cards` flag will apply for all classic CRM cards included in
  the app. You cannot selectively hide individual classic CRM cards within an
  app.
To use the `hs-hide-crm-cards` flag to manage classic CRM card access:

- Make a request to set your app's `defaultState` to `OFF`. This initializes the app flag and ensures that the app's classic CRM cards will be visible in every existing and new installed account.
  

    ```json
    // Example PUT to https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-hide-crm-cards?hapikey={developerAPIKey}
    {
      "defaultState": "OFF"
    }
    ```
    ```shell
    curl --request PUT \
      --url 'https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-hide-crm-cards?hapikey={developerAPIKey}' \
      --header 'content-type: application/json' \
      --data '{
        "defaultState": "OFF"
      }'
    ```

  
- Next, set the `flagState` to `OFF` for all accounts that currently have the app installed. In the request body, you'll provide a `portalId` for each account.
```json
// Example POST to https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-hide-crm-cards/portals/batch/upsert?hapikey={developerAPIKey}
{
  "portalStates": [
    {
      "portalId": 1234,
      "flagState": "OFF"
    },
    {
      "portalId": 4567,
      "flagState": "OFF"
    },
    {
      "portalId": 78910,
      "flagState": "OFF"
    }
  ]
}
```
    ```shell
        curl --request POST \
          --url 'https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-hide-crm-cards/portals/batch/upsert?hapikey={developerAPIKey}' \
          --header 'content-type: application/json' \
          --data '{
            "portalStates": [
              {
                "portalId": 1234,
                "flagState": "OFF"
              },
              {
                "portalId": 4567,
                "flagState": "OFF"
              },
              {
                "portalId": 78910,
                "flagState": "OFF"
              }
            ]
          }'
    ```
- With existing app installs set to `OFF`, make another request to set the `defaultState` to `ON`. This prevents new installs from ever seeing the app's classic CRM cards.

```shell
curl --request PUT \
  --url 'https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-hide-crm-cards?hapikey={developerAPIKey}'\
  --header 'content-type: application/json' \
  --data '{
    "defaultState": "ON"
  }'

```

- As you observe customers using the new app cards, such as receiving `hubspot.fetch()` calls from their accounts, you can remove them from the `hs-hide-crm-cards` flag list using the request below. Because you hid the classic CRM cards from new installs, you'll only need to take this step for accounts you previously set to `OFF`.
  

```json
// Example POST to https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-hide-crm-cards/portals/batch/delete?hapikey={developerAPIKey}
{
  "portalIds": [1234, 4567, 78910]
}
```

  
  

```shell
curl --request POST \
  --url 'https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-hide-crm-cards/portals/batch/delete?hapikey={developerAPIKey}'\
  --header 'content-type: application/json' \
  --data '{
    "portalIds": [
      1234,
      4567,
      78910
    ]
  }'
```

  
- When all previously added accounts have been removed from the flag list, you can be sure that your entire install base can no longer access the old classic CRM cards. At this point, you can navigate to the app's settings page in your developer account and [delete the classic CRM cards](/guides/api/crm/extensions/crm-cards#delete-a-crm-card).
- Finally, make a request to delete the `hs-hide-crm-cards` flag from the app.

```shell
curl --request DELETE \
  --url 'https://api.hubapi.com/feature-flags/v3/{appId}/flags/hs-hide-crm-cards?hapikey={developerAPIKey}'
```

## Feature flag API reference

Use the feature flag API to control availability of your app cards in customer accounts. All endpoints are under the `https://api.hubapi.com/feature-flags/v3/{appId}` root path. The API currently supports a single App Flag: `hs-release-app-cards`. Attempts to specify other App Flags will receive an error.

View the full [feature flag API reference documentation](/reference/api/app-management/feature-flags).


The features described in this guide are in Early Access beta, separate from the _CRM development tools to build UI extensions with React as frontend_ beta for private app UI extension development.

[Request access to the UI extensions for public apps beta](https://developers.hubspot.com/build-app-cards)
# Build UI extensions for public apps (BETA)

To customize the HubSpot UI, from CRM record views to the help desk preview panels, you can build UI extensions. UI extensions are built locally using the developer projects framework, and are powered by a public or private app, depending on your needs. The toolset for developing UI extensions is largely the same whether you're building for a public or private app, with a few differences in authentication, installation, and feature support.

Below, learn about building UI extensions for public apps at a high level. For more detail, follow the links in each section.
## UI extensions in public apps vs private apps

UI extensions can be built for both private apps and public apps, and follow the same [principles and limitations](/guides/crm/ui-extensions/overview) in both types of apps. However, while most UI extension concepts are transferable from private apps to public apps, there are two key differences:

- When developing a UI extension using a private app, you install the app directly in the target production account. When developing using a public app, you instead build the app in a developer account, then its install URL can be used to install it into other accounts. So while private apps are created on a per-account basis, public apps are intended to be installed across multiple accounts.
- UI extensions in private apps use [serverless functions](/guides/crm/private-apps/serverless-functions) and the private app's access token to fetch external data. UI extensions in public apps instead require you to bring your own self-hosted back end and use the [hubspot.fetch() API](/guides/crm/public-apps/fetching-data).
## Creating and configuring public apps

Using the CLI, you can create a project with a public app using any of the following methods:

- Follow the [quickstart guide](/guides/crm/public-apps/quickstart) to create an app from a boilerplate project template, which includes a card for contact records.
- [Migrate an existing public app](/guides/crm/public-apps/migrate-a-public-app-to-projects) to the developer projects framework. If an app hasn't been migrated yet, you can also copy it into a developer project by running `hs project clone-app`.
- [Manually create a public app](/guides/crm/public-apps/creating-public-apps#create-an-app) in a project.

Whichever method you choose, you'll handle app configuration in the app’s `public-app.json` file Learn more about [creating and configuring apps](/guides/crm/public-apps/creating-public-apps).

## UI extension development

When building UI extensions, check out the following resources to learn more about the utilities, functionalities, and components available to you:

- [UI extensions overview](/guides/crm/ui-extensions/overview)
- [Create UI extensions](/guides/crm/ui-extensions/create)
- [UI extensions SDK](/guides/crm/ui-extensions/sdk)
- [List of available UI components](/reference/ui-components/overview)
- [Sample UI extensions](/guides/crm/ui-extensions/sample-extensions/overview)

## Local development

While developing UI extensions, you can run a local development server to see your saved changes in real-time without needing to re-upload the project. The local development server is only available for the front end portion of the app, and assumes the backend is either running locally or is hosted. Because public apps require you to bring your own back end, there are a few differences for running local development for public apps, depending on your setup.

Learn more about [local development](/guides/crm/ui-extensions/local-development).

## Fetching data

While private apps can use serverless functions to fetch data, when you build UI extensions inside public apps, you need to bring your own REST-based backend and use the `hubspot.fetch()` API to fetch data. You'll specify the URLs that the app can request data from in the `allowedUrls` array in the `public-app.json` file.

Learn more about [fetching data with hubspot.fetch](/guides/crm/public-apps/fetching-data).

## Working with webhooks

Build webhooks into your app to subscribe to events happening in the account that the app is installed in. Webhooks in project-based public apps can use [generic webhook subscriptions](/guides/apps/public-apps/create-generic-webhook-subscriptions#parse-generic-webhook-payloads) to subscribe to events for a wide range of HubSpot objects. However, you can still subscribe to [classic webhook subscriptions](/guides/api/app-management/webhooks#webhook-subscriptions) by including a `legacyCrmObjects` or `HubEvents`, depending on the subscription type.

Webhook subscriptions are defined in the `webhooks.json` file within a `webhooks` folder in the same directory as your app (e.g., `src/app`). You'll also need to update your `public-app.json` configuration file to reference that file.

Learn more about using [webhooks in public apps](/guides/crm/public-apps/webhooks).

## App Marketplace listing

All apps built for the App Marketplace are subject to the [App Marketplace listing requirements](/guides/apps/marketplace/app-marketplace-listing-requirements), and certified apps are subject to the additional [App Marketplace certification requirements](/guides/apps/marketplace/certification-requirements). If you're adding a UI extension to an app intended for the App Marketplace, you'll need to adhere to some additional criteria.

### Compliance

- **Naming:** per the [App Partner Program branding guidelines](https://www.hubspot.com/partners/app/branding-guidelines):
  - Do not modify, imitate, or abbreviate any HubSpot brands or names (e.g., "HubSpot," "Hub," etc.) anywhere in the name of your UI extension.
  - Do not use a generic product name + any HubSpot brands or names (e.g., "UI extension for HubSpot").
  - Do not brand your UI extension using the word "inbound" in a way that would tie it to HubSpot's INBOUND event (e.g., "Inbound Sales UI extension").
- **Logos and icons:**
  - Per the [App Partner Program branding guidelines](https://www.hubspot.com/partners/app/branding-guidelines), do not use the HubSpot company logo or sprocket without permission.
  - Do not use company or brand logos other than your own as icons.
- **Sensitive data:**
  - Your app must not access, request, or use [sensitive data scopes](/reference/api/crm/sensitive-data).
  - Your UI extension must not display sensitive information, as defined in [HubSpot's Terms of Service](https://legal.hubspot.com/terms-of-service).

### Security and privacy

- Your app must have [advanced scope settings](/guides/crm/public-apps/creating-public-apps#app-configuration) turned on (`"advancedScopeSettingsEnabled": true` in `public-app.json`). All [required, conditionally required, and optional scopes](/guides/apps/public-apps/overview#configure-scopes) should be selected to prevent errors.
- Your app must use all of the scopes that it requests during installation. Scopes that are not used must be removed. If certain scopes only apply to a subset of your app's user base, they should be included as conditionally required or optional scopes.

### Reliability and performance

For linked assets such as images and JavaScript, avoid using absolute links. Instead, use relative links and include the assets in your files. Exceptions may only be made if you use a reputable CDN.

### Usability and accessibility

- **Buttons:**
  - [Forms](/reference/ui-components/standard-components/form) must include submit [Buttons](/reference/ui-components/standard-components/button).
  - Ensure destructive button styles denote a destructive behavior.
  - Include only one primary button per surface (app card, modal, or panel).
- **Text:**
  - It's recommended to not use underline formatting for text that's next to a hyperlink, as it will also appear clickable.
  - Do not use [Tags](/reference/ui-components/standard-components/tag) in place of Buttons or [Links](/reference/ui-components/standard-components/link).


The features described in this guide are in Early Access beta, separate from the _CRM development tools to build UI extensions with React as frontend_ beta for private app UI extension development.

[Request access to the UI extensions for public apps beta](https://developers.hubspot.com/build-app-cards)
# UI extensions for public apps quickstart (BETA)

Follow this tutorial to create a UI extension for a public app based on a HubSpot-provided template, which includes an app card for contact records. If you're looking to migrate an existing public app to the projects framework, check out the [migration guide](/guides/crm/public-apps/migrate-a-public-app-to-projects).

By the end of this tutorial, you will have:

- Configured your local environment for project development
- Created a project and public app locally using HubSpot's boilerplate Get Started project
- Installed the public app in a test account
- Added the app card to contact records

## Prerequisites

Before getting started, ensure you've also set up the following:

- Sign up for the UI extensions for public apps beta.
- Install the latest version of the CLI by running `npm i -g @hubspot/cli@latest`. The CLI will need to be version 6.1.0 or later.
- Create a [developer account](https://app.hubspot.com/signup-hubspot/developers).
- In your developer account, [create a test account](/getting-started/account-types#create-a-developer-test-account) that you can use to install and test the app.
- Because you'll be using OAuth, you'll need to set up a self-hosted backend environment to complete the OAuth process. For the purposes of this tutorial, it's recommended to set up the sample [OAuth Node.js app](https://github.com/HubSpot/oauth-quickstart-nodejs), which is configured to work with the example project you'll be creating.

## 1. Set up your local environment

Because projects are developed locally, you'll first need to set up your local environment:

- Install [Node.js](https://nodejs.org/en/download/) which enables HubSpot’s local development tools. Versions 18 or higher are supported. It's recommended to use a package manager like [Homebrew](https://brew.sh/) or [nvm](https://github.com/nvm-sh/nvm) to install Node.
- In the terminal, use the `cd` command to navigate to the directory where you'll be storing your project, app, and extension files.
- Install the latest version of the CLI by running `npm install -g @hubspot/cli@next`. The CLI will need to be version `5.2.1-beta.5` or later.
- Connect the CLI to your developer account:
  - If you haven't yet created a `hubspot.config.yml` file, run `hs init`, then follow the prompts to initialize the HubSpot configuration file to connect the CLI to your developer account.
  - If you've already set up the initial HubSpot configuration file (`hubspot.config.yml`), you can add your developer account to the list of authorized accounts by running `hs auth`. Follow the prompts to generate a personal access key in your account, then copy and paste it into the terminal.

## 2. Create a boilerplate project

To get started, HubSpot provides a boilerplate project template you can use by following the steps below.

- In your working directory, run `hs project create`.

```shell
hs project create
```

- Enter a **name** for the project, then confirm the **local destination** for the boilerplate project.
- When prompted to select a project template, select the **CRM getting started project with public apps (BETA)** template.
- With the boilerplate project created, navigate into the new directory by running `cd <project-directory>`.

```shell
cd <project-template-directory>
```

- Upload the project to your developer account by running `hs project upload`. This will create a new public app in your developer account which you'll then continue to update as a part of this guide.

```shell
hs project upload
```

## 3. Add your app's credentials to your backend

To find your app's client ID and secret:

- Navigate to your developer account.
- Click **Manage apps**, then click the **Get started with public apps** app.
- Click the **Auth** tab.
- Under _App credentials_, copy your app's **Client ID** and **Client Secret** and paste them into the environment configuration (e.g., entries in an `.env` file) for your backend.
This guide assumes you're using the [OAuth Node.js app](https://github.com/HubSpot/oauth-quickstart-nodejs), which is already configured to use `http://localhost:3000/oauth-callback` as the redirect URL.
## 4. Install the app in your developer test account

- In the left navigation bar, click **Apps**, then click the **Get started with public apps** app.
- Click the **Auth** tab.
- Next to _Sample install URL (OAuth)_, click **Copy full URL**, then open that URL in your browser.
- Follow the prompts to authorize and install the app in your developer test account.
- Navigate to your developer test account to confirm that it's been installed:
  - In the left sidebar of your developer account, click **Test accounts**.
  - Click the **name** of the test account you installed the app in.
  - In the top navigation bar, click the **Settings** icon.
  - In the left sidebar menu, navigate to **Integrations** > **Connected apps.** You should see the _Get started with public apps_ app listed on the _Connected apps_ page.

## 5. Add the card to the record page

The _Get started_ app comes with an example app card for contact record pages. After installing the app in your developer test account, you'll need to configure the app card to appear on your record pages by following the steps below.

- In the account where you installed the app, click the **Settings** icon in the main navigation bar.
- In the left sidebar menu, under _Data Management_, navigate to **Objects** > **Contacts**.
- Click the **Record customization** tab.
- Click **Default view** to customize the default contact record view.

  

- Click the **+** tab to create a new **Custom** tab. Then, click the **Custom** tab and click **Add cards**.

  

- In the right panel, under _Card types_, click **Apps**.

  

- Select the **checkbox** next to the example extension that's part of your public app.
- Click the **X** at the top of the right panel to close it.
- In the top right, click **Save and exit**.

With the card added to the contact record, navigate to a contact record to view the card:

- In the left sidebar, navigate to **CRM** > **Contacts**.
- Click the **name** of a contact.
- Click the **Custom** tab, then view your card.
With the card displaying on the contact record, you can make updates to the app or extension and re-upload to your account using `hs project upload`. However, for faster iteration, you can run a local development server, which will show your React file updates on save.

## 6. Run local development

When building a UI extension, you can run a local development server to iterate and test functionality in real-time without needing to re-upload the project. The local development server is only available for the frontend portion of the app, and assumes the backend is either running locally or is hosted.

Your app must be uploaded to your development account for the server to work. The local development server will only pick up changes made to React files. For changes to configuration files, you'll need to use `hs project upload`.
You should not use the production app for local development if it's installed in production accounts. Instead, work with a replica project to avoid making updates to the production app. You can clone an existing public app by running `hs project clone-app`.
To start a local development server:

- First install dependencies by navigating to `src/app/extensions` and running `npm install`.

```shell
cd src/app/extensions
npm install
```

- Run `hs project dev`.

```shell
hs project dev
```

- You'll then be prompted to select an existing develop test account or create a new one. For this tutorial, select the developer test account you previously used.

  
If you select a developer account that doesn't have the app installed, the terminal will prompt you to install the app.
- The local development server will start, and can be ended by pressing **q** in the terminal. In HubSpot, refresh the contact record page, which should update the card with a _Developing locally_ tag.

  

Changes to your React files should now get picked up automatically in the browser on save without needing to refresh the page.

To test this out, open the `extensions/ExampleCard.jsx` file and add the following `Text` component above the rest of the components.

```js
const Extension = () => {
  return (
    <>
      <Text>
        New text!
      </Text>
      <Text>
        Congrats! You just deployed your first App card. What's next? Here are
        some pointers to get you started:
      </Text>
      ...
```

After saving your changes, the card will automatically pick up the new component and display the text.

If you wanted to publish this change to your app, you would end the development server by pressing **q**, then run `hs project upload`. Because the app itself is stored in your app developer account, you’ll need to ensure you’re uploading to that account, not your test account. After your changes are built and deployed, you would see them reflected in the test account after refreshing the contact record page.
Changes made to your configuration files will not automatically get picked up by the local development server, and instead will need to be uploaded with `hs project upload`.
## Next steps

Now that you’ve successfully created, uploaded, installed, and started local development for your UI extension, check out the following resources to learn more:

- [Fetching data for public apps](/guides/crm/public-apps/fetching-data)
- [Local development for UI extensions](/guides/crm/ui-extensions/local-development)
- [UI extensions SDK reference](/guides/crm/ui-extensions/sdk)
- [Subscribing to events with webhooks](/guides/crm/public-apps/webhooks)
- [HubSpot’s sample UI extensions](/guides/crm/ui-extensions/sample-extensions/overview)
- [UI extension components](/reference/ui-components/overview)[](/reference/ui-components/overview)


The features described in this guide are in Early Access beta, separate from the _CRM development tools to build UI extensions with React as frontend_ beta for private app UI extension development.

[Request access to the UI extensions for public apps beta](https://developers.hubspot.com/build-app-cards)
# Working with webhooks (BETA)

Build webhooks into your public app to subscribe to events happening in the account that the app is installed in.

In this guide, learn about:

- [Defining webhook subscriptions](#defining-webhook-subscriptions)
- [Subscription types](#subscription-types)
- [Response payloads](#response-payloads)
- [Viewing webhook subscriptions in HubSpot](#view-webhook-subscriptions-in-hubspot)

## Defining webhook subscriptions

Webhook subscriptions are defined in the `webhooks.json` file within a `webhooks` folder in the same directory as your app (e.g., `src/app`). You'll also need to update your [public-app.json](/guides/crm/public-apps/creating-public-apps#app-configuration) configuration file to reference that file.
The `webhooks.json` file contains fields for defining the webhook's settings and event subscriptions.

```json
// Example webhooks.json
{
  "settings": {
    "targetUrl": "https://example.com/webhook",
    "maxConcurrentRequests": 10
  },
  "subscriptions": {
    "crmObjects": [
      {
        "subscriptionType": "object.propertyChange",
        "objectName": "contact",
        "propertyName": "firstname",
        "active": true
      },
      {
        "subscriptionType": "object.creation",
        "objectName": "contact",
        "active": true
      }
    ],
    "legacyCrmObjects": [
      {
        "subscriptionType": "contact.propertyChange",
        "propertyName": "lastname",
        "active": true
      },
      {
        "subscriptionType": "contact.deletion",
        "active": true
      }
    ],
    "hubEvents": [
      {
        "subscriptionType": "contact.privacyDeletion",
        "active": true
      }
    ]
  }
}
```

| Parameter | Type | Description |
| --- | --- | --- |
| `settings` | Object | An object that specifies your webhook settings, including `targetUrl` and `maxConcurrentRequests`. |
| `targetUrl` | String | The URL that webhooks will be sent to. Learn more about the [response payload](#response-payload) sent with the `POST` request. |
| `maxConcurrentRequests` | String | The maximum number of concurrent requests that will be sent. |
| `subscriptions` | Object | An object that specifies the webhook subscriptions. |
| `crmObjects` | Array | An array containing event subscription definitions. This is the standard array to include, and should be used for all events in the new format (`object.*`). Classic webhook subscription types should instead be included in `legacyCrmObjects` and `hubEvents` arrays, depending on the event. See [subscription types](#subscription-types) for more information. |
| `subscriptionType` | String | The type of event being subscribed to. Learn more about [subscription types](#subscription-types). |
| `objectName` | String | The CRM object being subscribed to. |
| `propertyName` | String | The property on the CRM object being subscribed to. |
| `active` | Boolean | Whether webhooks will be sent for this subscription. |
| `legacyCrmObjects` | Array | An array containing classic subscription types, such as `contact.creation` and `deal.deletion`. See the [webhooks API guide](/guides/api/app-management/webhooks#webhook-subscriptions) for more information. |
| `hubEvents` | Array | An array containing the classic subscription types `contact.privacyDeletion` and `conversation.*`. See the [webhooks API guide](/guides/api/app-management/webhooks#webhook-subscriptions) for more information. |

## Subscription types

Public apps built with projects use [generic webhook subscription](/guides/apps/public-apps/create-generic-webhook-subscriptions) syntax, using the format `object.*` rather than specifying the object name in the subscription type (e.g., `contact.*`).

| Subscription type  | Format                     |
| ------------------ | -------------------------- |
| Creation           | `object.creation`          |
| Deletion           | `object.deletion`          |
| Merge              | `object.merge`             |
| Restore            | `object.restore`           |
| Property change    | `object.propertyChange`    |
| Association change | `object.associationChange` |

To specify the type of CRM object you're subscribing to, include the `objectName` field in the subscription definition object using any of the supported objects in the table below. You'll need to include the object's corresponding scopes in your `public-app.json` file.

<table>
  <tbody>
    <tr>
      <td>
        <code>appointment</code>
      </td>
      <td>
        <code>fee</code>
      </td>
      <td>
        <code>quote_template</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>call</code>
      </td>
      <td>
        <code>feedback_submission</code>
      </td>
      <td>
        <code>task</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>cart</code>
      </td>
      <td>
        <code>goal_target</code>
      </td>
      <td>
        <code>tax</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>commerce_payment</code>
      </td>
      <td>
        <code>line_item</code>
      </td>
      <td>
        <code>ticket</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>communication</code>
      </td>
      <td>
        <code>listing</code>
      </td>
      <td>
        <code>partner_client</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>company</code>
      </td>
      <td>
        <code>meeting_event</code>
      </td>
      <td>
        <code>lead</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>contact</code>
      </td>
      <td>
        <code>note</code>
      </td>
      <td>
        <code>service</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>course</code>
      </td>
      <td>
        <code>order</code>
      </td>
      <td>
        <code>subscription</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>deal</code>
      </td>
      <td>
        <code>postal_mail</code>
      </td>
      <td>
        <code>invoice</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>discount</code>
      </td>
      <td>
        <code>product</code>
      </td>
      <td>
        <code>user</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>email</code>
      </td>
      <td>
        <code>quote</code>
      </td>
      <td>
        <code>partner_account</code>
      </td>
    </tr>
    <tr>
      <td>
        <code>engagement</code>
      </td>
      <td>&nbsp;</td>
      <td>&nbsp;</td>
    </tr>
  </tbody>
</table>

```json
// Example webhooks.json
{
  "settings": {
    "targetUrl": "https://example.com/webhook",
    "maxConcurrentRequests": 10
  },
  "subscriptions": {
    "crmObjects": [
      {
        "subscriptionType": "object.propertyChange",
        "objectName": "contact",
        "propertyName": "firstname",
        "active": true
      }
      ...
    ]
  }
}
```

If you need to subscribe to [classic subscription types](/guides/api/app-management/webhooks#webhook-subscriptions), you can store them in the `legacyCrmObjects` and `hubEvents` arrays instead, depending on the type of subscription.

```json
// Example webhooks.json
{
  "settings": {
    "targetUrl": "https://example.com/webhook",
    "maxConcurrentRequests": 10
  },
  "subscriptions": {
    "legacyCrmObjects": [
      {
        "subscriptionType": "contact.propertyChange",
        "propertyName": "lastname",
        "active": true
      },
      {
        "subscriptionType": "contact.deletion",
        "active": true
      }
    ],
    "hubEvents": [
      {
        "subscriptionType": "contact.privacyDeletion",
        "active": true
      }
    ]
  }
}
```

| Parameter | Type | Description |
| --- | --- | --- |
| `legacyCrmObjects` | Array | An array containing [classic subscription types](/guides/api/app-management/webhooks#webhook-subscriptions) (e.g., `contact.propertyChange`) |
| `hubEvents` | Array | An array that can contain the `contact.privacyDeletion` and `conversation.*` [classic subscription types](/guides/api/app-management/webhooks#webhook-subscriptions). |

## Response payloads

When an event that the app is subscribed to occurs, the `targetUrl` you specify in `webhooks.json` will receive a `POST` request containing JSON formatted data from HubSpot. All events will include the same base set of fields, with other fields being added depending on the event type. Learn more about [parsing webhook payloads for specific event types.](/guides/apps/public-apps/create-generic-webhook-subscriptions#parse-generic-webhook-payloads).

Below is an example payload for `propertyChange` event. This type of event contains all generic fields, plus the `propertyName` and `propertyValue` fields to show which property changed and the new value.

```json
// Example webhooks response payload
[
  {
    "appId": 3715530,
    "eventId": 100,
    "subscriptionId": 2764026,
    "portalId": 123456,
    "occurredAt": 1723651850844,
    "subscriptionType": "object.propertyChange",
    "attemptNumber": 0,
    "objectId": 987654,
    "changeSource": "CRM",
    "objectTypeId": "0-1",
    "propertyName": "firstname",
    "propertyValue": "sample-value",
    "isSensitive": false
  }
]
```

| Field | Description |
| --- | --- |
| `appId` | The ID of your public app. This can be helpful if you have multiple applications pointing to the same webhook URL. |
| `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 the event. |
| `portalId` | The [ID of the HubSpot account](https://knowledge.hubspot.com/account/manage-multiple-hubspot-accounts#check-your-current-account) where the event occurred. |
| `occurredAt` | When the event occurred as a unix timestamp (in milliseconds). |
| `subscriptionType` | The webhook subscription type. Can be one of the following:<ul><li>`object.creation`</li><li>`object.deletion`</li><li>`object.merge`</li><li>`object.restore`</li><li>`object.propertyChange`</li><li>`object.associationChange`</li></ul> |
| `attemptNumber` | Starting at `0`, which number attempt this is to notify your service of this event.<br /><br /> |
| `objectId` | The ID of the object that was created, changed, or deleted. For example, for a contact-related event, `objectId` would be the contact's ID. |
| `changeSource` | The source of the change. This can be any of the change sources that appear in contact property histories. |
| `objectTypeId` | The type of object that triggered the event. See the [full list of object type IDs](/guides/api/crm/understanding-the-crm#object-type-ids) for more information. |
| `propertyName` | The name of the property that was updated (for `object.propertyChange` events only). |
| `propertyValue` | The new property value that resulted from the change (for `object.propertyChange` events only). |
| `isSensitive` | Will be `true` if the property is a [sensitive data property](/reference/api/crm/sensitive-data). |
| `sourceId` | The ID of the source that triggered the event (e.g., a user ID if it was a user that updated the property data). |

The `object.associationChange` subscription will trigger for all associations, including custom association labels. Association change events will also trigger on both incoming and outgoing association changes. This means that an `object.associationChange` event defined for an `objectName` of `contact` will not only trigger on a `CONTACT_TO_DEAL` association change, but also on a `DEAL_TO_CONTACT` association change.

```json
// Example webhooks response payload
[
  {
    "eventId": 668521389,
    "subscriptionId": 621550,
    "portalId": 123456,
    "appId": 3715530,
    "occurredAt": 1715708228603,
    "subscriptionType": "object.associationChange",
    "attemptNumber": 0,
    "changeSource": "USER",
    "associationType": "CONTACT_TO_DEAL",
    "associationCategory": "HUBSPOT_DEFINED",
    "associationTypeId": 4,
    "fromObjectId": 3788,
    "fromObjectTypeId": "0-3",
    "toObjectId": 4658499728,
    "toObjectTypeId": "0-1",
    "associationRemoved": true,
    "isPrimaryAssociation": false,
    "sourceId": "userId:864745280"
  },
  {
    "eventId": 2975980077,
    "subscriptionId": 621550,
    "portalId": 885814039,
    "appId": 5553555,
    "occurredAt": 1715708228603,
    "subscriptionType": "object.associationChange",
    "attemptNumber": 0,
    "changeSource": "USER",
    "associationType": "DEAL_TO_CONTACT",
    "associationCategory": "HUBSPOT_DEFINED",
    "associationTypeId": 3,
    "fromObjectId": 4658499728,
    "fromObjectTypeId": "0-3",
    "toObjectId": 3788,
    "toObjectTypeId": "0-1",
    "associationRemoved": true,
    "isPrimaryAssociation": false,
    "sourceId": "userId:864745280"
  }
]
```

Learn more about [fields included in `associationChange` event payloads](/guides/apps/public-apps/create-generic-webhook-subscriptions#association-events).

## View webhook subscriptions in HubSpot

On the app settings page in HubSpot, you can view a list of each event subscription for each subscription in the app's `webhooks.json` file.

To view an app's webhook subscriptions in HubSpot:

- In the left sidebar of your developer account, navigate to **Apps**.

  

- Click the **name** of the app.
- In the left sidebar, under _Features_, click **Webhooks**.

  

- Under _Event subscriptions_, you can view each of the app's webhook subscriptions.
- To view more information about a subscription, including number of times triggered and number of errors, click the **name** of the subscription.

  - Click the numbers in the **Total count** and **Errors** columns to navigate to the webhooks monitoring tab.
  - Hover over a subscription type then click **Details** to open the details panel on the right. This panel includes a sample event response payload and a testing feature.

    


# Developer projects setup guide (BETA)

To develop HubSpot projects, you’ll first need to set up your local environment, including installing the HubSpot CLI and connecting it to your HubSpot account. You can also set up testing environments in your connected HubSpot account so that you can iterate in a siloed environment before deploying to production.

For private app development, you can set up [development sandboxes](#create-and-use-development-sandboxes) in your production account. For public app development, you can set up [test accounts](/getting-started/account-types#create-a-developer-test-account) in your developer account.

Below, learn how to connect your production account to the CLI then create a development sandbox within it. After setting up your local environment, you can proceed to [creating a project](/guides/crm/developer-projects/create-a-project). To learn more about the CLI commands you'll be using, check out the [project-specific CLI commands reference](/guides/crm/developer-projects/project-cli-commands).
For a complete guide to creating a UI extension, including these setup steps, check out the UI extensions quickstart guides:

- [UI extensions for private apps quickstart](/guides/crm/private-apps/quickstart)
- [UI extensions for public apps quickstart](/guides/crm/public-apps/quickstart)
## Install Node and the CLI

To set up your local environment, you'll first need to:

- Install [Node.js](https://nodejs.org/en/download) which enables HubSpot’s local development tools. Versions 18 and higher are supported. It's recommended to use a package manager like [Homebrew](https://brew.sh/) or [nvm](https://github.com/nvm-sh/nvm) to install Node.
- Install the HubSpot CLI globally by running `npm install -g @hubspot/cli@latest` in the terminal. This command will also update the CLI to the latest version if you’ve already installed it.

## Connect your HubSpot account

With the CLI installed, you can now connect it to the account you'll be uploading to. If you're developing a UI extension for a private app, you'll connect the CLI to the production account where the app will be installed. If you're developing a UI extension for a public app, you'll instead connect the CLI to the developer account that will host the app.

- In the terminal, navigate to the directory where you’ll be working using the `cd` command.

```shell
cd path/to/working/directory
```

- If you haven't connected an account to the CLI before, run `hs init`. If you've connected an account to the CLI before but want to connect another account, you can run `hs auth` instead.

```shell
hs init
```

- Press **Enter** to open the personal access key page in your browser.
- In the browser, select the **account** you want to deploy to.
  - For private app development, select an existing **standard production** HubSpot account or **developer sandbox account**.
  - For public app development, select your **developer account**.
- Click **Continue with this account**. You’ll then be redirected to the personal access key page of the account.
- If you haven’t generated a personal access key for the selected account yet, you’ll need to first generate the key by selecting the scopes you want to include for it.
  - For private app development, select the **Developer projects**, **Sandboxes**, and **Serverless functions** checkboxes. Then click **Generate personal access key**.
  - For projects that include a public app, select the **Developer projects**, **Developer test accounts**, and **File manager** checkboxes. Then click **Generate personal access key**.
- To copy/paste the access key, click **Show** to reveal the key, then click **Copy**.
Your account's personal access key must include the following minimum scopes to avoid errors during local development:

- For private app development, your production account personal access key must include the following scopes: `developer.projects.write`, `developer.app_functions.read`, `developer.app_functions.write`, `developer.sandboxes.read`, `developer.sandboxes.write`, `sandboxes.read`, `sandboxes.write`.
- For public app development, your developer account personal access key must include the following scopes: `developer.projects.write`, `developer.test_accounts.read`, `developer.test_accounts.write`, `files`.

When connecting the CLI to an existing sandbox account, you can ignore the sandbox-related scopes above. Personal access keys in sandbox accounts don't have access to those scopes because you cannot create a sandbox within a sandbox.
- Paste the copied key into the terminal, then press **Enter**.
- Enter a unique name for the account, which can used when running CLI commands. Then, press **Enter**.
- Set the account as your default by pressing **Enter**.

The CLI will display a success message confirming that the **`hubspot.config.yml`** file was created, or updated, and that your newly connected account was set as the default.

- The `hubspot.config.yml` file stores your authorized accounts, and can be updated by running the **`hs auth`** command. At any time, you can view all the currently connected accounts by running `hs accounts list`.
- When you run commands to interact with the account, such as building and deploying, HubSpot will use the account that’s set as the default in the file. When developing on multiple accounts, you can change the default account by running `hs accounts use accountName`. You can also interact with a specific account by adding the following flag to the end of a command: `--account=accountName`.

Below, learn more about developing private apps in sandbox accounts.

## Create and use development sandboxes
Development sandboxes are only available in accounts with an _Enterprise_ subscription.
After following the steps above to connect a production account to the CLI for private app development, you can create a development sandbox within it to setup a lightweight testing environment. This enables you to develop your apps and extensions in a siloed environment before deploying to a production account.

Before proceeding, review the following development sandbox limits:

- A production account can have only two development sandboxes at a time.
- CRM object definitions are synced from the production account to the development sandbox at the time of sandbox creation.
- You cannot create a sandbox within another sandbox.

### Create a development sandbox

To set up a development sandbox account:

- Because development sandboxes are created within the `defaultPortal` in your `hubspot.config.yml` file, first confirm that your production account is connected and set as the default:

  - In the terminal, run `hs accounts list`.
  - In your list of connected accounts, confirm that your production account is listed as the default account.

    

  - If your production account is not the default, run `hs accounts use` and select your production account.

- After confirming your production account is the default, run `hs sandbox create`.
- You'll then be prompted to select a type of sandbox to create. Select **Development sandbox**, then press **Enter**.
- Enter a `name` for the sandbox account, then press **Enter**.
- All CRM object definitions will be copied from production to the development sandbox.
- You can use the [import tool](https://knowledge.hubspot.com/import-and-export/import-objects) to import production object record data, or manually create sample data for testing.

- The CLI will then begin the sandbox setup process. Once the sandbox is fully set up and synced, you'll see a _Sandbox sync complete_ confirmation.

With your development sandbox created, it will appear under the associated production account when running `hs accounts list`.

If you want to set the development sandbox as your default account, run `hs accounts use`, then select the **sandbox**. To deploy to your sandbox or production account, you can either run `hs accounts use` to set the default account, or manually select the account when uploading by running `hs project upload --account=<name-of-account>`.
Development sandboxes are designed to be early proof of concept environments. It is recommended to [delete](#delete-a-development-sandbox) and [create](#create-a-development-sandbox) a new Development Sandbox using the CLI. This ensures development sandboxes always have an exact mirror of the production account’s CRM object definitions when beginning new projects.

After setting up your development sandbox, learn how to quickly create a UI extension by jumping to [section three of the UI extensions quickstart guide](/guides/crm/private-apps/quickstart#start-local-development). Or, learn more about [projects](/guides/crm/developer-projects/create-a-project), [private apps](/guides/crm/private-apps/creating-private-apps), and [UI extensions](/guides/crm/ui-extensions/create).
Learn more about the [CLI commands](/guides/crm/developer-projects/project-cli-commands#sandbox-commands) you can use to interact with both standard and development sandboxes.
### View a development sandbox in HubSpot

By default, all super admin users are synced to the development sandbox during creation. Super admins can give other users access by [adding them as users to the development sandbox](https://knowledge.hubspot.com/account-management/add-and-remove-users).

To access the development sandbox account in HubSpot:

- In your HubSpot account, navigate to **CRM Development** in the main navigation bar.
- In the left sidebar menu, select **Sandboxes**.
- Click the **Development** tab, where your new sandbox will be listed along with its name, create date, and the user who created it.
- To navigate to the sandbox account, click the **development sandbox name**.

Once a user has been granted access to a development sandbox, they can access it by clicking the **Profile picture** in the top right of HubSpot, then clicking the **Account selection menu** and selecting the account.
### Delete a development sandbox

- To delete a development sandbox using the CLI, run `hs sandbox delete`, then follow the prompts.
- To delete a development sandbox in HubSpot:
  - In your HubSpot account, navigate to **CRM Development** in the main navigation bar.
  - In the left sidebar menu, select **Sandboxes**.
  - Click the **Development** tab.
  - Hover over the development sandbox, then click **Delete**.


# Create UI extensions with React (BETA)

After creating a [project](/guides/crm/developer-projects/create-a-project) and a [public app](/guides/crm/public-apps/creating-public-apps) or [private app](/guides/crm/private-apps/creating-private-apps) within it, you can create a UI extension to customize and extend HubSpot's CRM record and help desk UI. In this guide, you'll learn how UI extensions work and how to build them. To learn more about configuring UI extensions, check out the [UI extensions SDK reference](/guides/crm/ui-extensions/sdk).

You can also [follow the quickstart guide](/guides/crm/private-apps/quickstart) to build and deploy an example UI extension to your account. Or, check out HubSpot's other [sample projects](https://github.com/HubSpot/ui-extensions-examples).

In this guide:

- [Prerequisites](#prerequisites)
- [Set up extension files](#set-up-extension-files)
- [Start local development](#start-local-development)

## Prerequisites

Before getting started, ensure you've also set up the following:

- [Set up your local environment](/guides/crm/setup) for project development.
- Created a [project](/guides/crm/developer-projects/create-a-project) with a [private app](/guides/crm/private-apps/creating-private-apps) or [public app](https://developers.hubspot.com/docs/guides/crm/public-apps/creating-public-apps) in it.

If you haven't created a UI extension before, you may want to start with the [private app](/guides/crm/private-apps/quickstart) or [public app](/guides/crm/public-apps/quickstart) quickstart guide or view [HubSpot's example UI extensions](/guides/crm/ui-extensions/sample-extensions/overview).

## Set up extension files

Within your project's app directory, create an `/extensions` directory with the following files:

- `example-card.json`: the card configuration.
- `Example.jsx`: the React file that serves as the front-end.
- `package.json:` metadata about the extension's front-end. This file is required and can be used to include dependencies for your React front-end.

In the `extensions` directory, you'll also need to install the [HubSpot UI extensions npm package](https://www.npmjs.com/package/@hubspot/ui-extensions) by running `npm i` `@hubspot/ui-extensions`.

### example-card.json

The configuration file for the UI extension's app card.

```json
// Example extension config file

{
  "type": "crm-card",
  "data": {
    "title": "Example Card",
    "location": "crm.record.tab",
    "uid": "unique-extension-name",
    "module": {
      "file": "Example.jsx"
    },
    "objectTypes": [{ "name": "contacts" }]
  }
}
```

| Field | Type | Description |
| --- | --- | --- |
| `type` | String | The type of extension. Must be `crm-card`. |
| `title` | String | The name of the card. |
| `location` | String | `location` (string): where the card appears in HubSpot's UI. Learn more about [extension location](#extension-location).<br /><ul><li>`crm.record.tab`: places the card on a tab of the middle pane of CRM records.</li><li>`crm.record.sidebar`: places the card in the right sidebar of CRM records.</li><li>`crm.preview`: places the card in the right side preview panel that you can access from record pages, index pages, board views, and lists pages.</li><li>`helpdesk.sidebar`: places the card in the ticket sidebars within help desk. This includes the ticket preview panel on the help desk home page and the right sidebar of the ticket view in help desk. This location is separate from the ticket CRM record page.</li></ul> |
| `uid` | String | The extension's unique identifier. This can be any string, but should meaningfully identify the extension. HubSpot will identify the extension by this ID so that you can change the extension's title without removing historical or stateful data, such as the card's position on the CRM record. |
| `module` | Object | An object containing the `file` field, which contains the the location of the app card's front end React code. |
| `objectTypes` | Array | Defines which types of CRM object records the extension will appear on. This will also enable you to pull data from those records when using the `fetchCrmObjectProperties` method. Learn more about [compatible objects](#compatible-objects). |

#### Extension location

You can configure which part of the HubSpot UI to customize using the `location` property in the extension's [JSON config file](#example-card-json). The follow locations are available:

- `crm.record.tab`: places the extension in the middle column of CRM record pages, either in one of HubSpot's default tabs or in a custom tab. If you've customized the middle column previously, you'll need to [customize the middle column view](https://knowledge.hubspot.com/object-settings/customize-records) to make any newly created extensions visible.

  

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

  

- `helpdesk.sidebar`: places the card in the ticket sidebars within help desk. This includes both the ticket preview panel on the help desk home page and the right sidebar of the ticket view in help desk. To add a card to this location, you'll need to [configure your help desk settings](https://knowledge.hubspot.com/help-desk/customize-the-right-sidebar-of-help-desk) to include the card.
When creating an extension for this location, you'll also need to ensure that the [app's JSON configuration file](/guides/crm/intro/create-private-apps-with-projects) includes `tickets` in the `scopes` array, and that the [card's JSON configuration file](/guides/crm/ui-extensions/create#set-up-extension-files) includes `tickets` in the `objectTypes` field.
Help desk home page:
Help desk ticket view:
#### Supported objects

You can create extensions for both [standard object](/guides/api/crm/understanding-the-crm) and custom object records. In the [app card's JSON configuration file](#example-card-json), you'll define this within the `objectTypes` array.

When building an extension for custom objects, you'll reference the object as `p_objectName` (case sensitive). To get this value, make a `GET` request to the [custom object schema API](/guides/api/crm/objects/custom-objects), then look for the `fullyQualifiedName` in the response. Take the `fullyQualifiedName`, then remove the HubID number, and use the resulting value for the configuration file.

```json
// example card.json

"objectTypes": [
 {
  "name": "p_Cats"
 }
]
```

For example, for a custom object with the `fullyQualifiedName` of `p123456_Cats`, the correct value to use for the configuration file would be `p_Cats`.

### Example.jsx

The React front-end file. Note that this example code is for a UI extension powered by a private app, as it includes a serverless function. For a public app, you would not include a serverless function. Instead, you would provide your own custom back-end.

```js
// Example React front-end

import React, { useState } from 'react';
import {
  Button,
  Text,
  Input,
  Stack,
  hubspot,
} from '@hubspot/ui-extensions';

hubspot.extend(({ context, runServerlessFunction, actions }) => (
  <Extension
    context={context}
// runServerlessFunction is for private apps only
    runServerless={runServerlessFunction}
    sendAlert={actions.addAlert}
  />
));

const Extension = ({ context, runServerless, sendAlert }) => {
  const [text, setText] = useState('');

  const run = () => {
    runServerless({ name: 'myFunc', parameters: { text: text } }).then((resp) =>
      sendAlert({ message: resp.response })
    );
  };

  return (
    <>
      <Text>
        <Text format={{ fontWeight: 'bold' }}>
          Your first UI Extension is ready!
        </Text>
        Congratulations {context.user.firstName}! You just deployed your first
        HubSpot UI extension. This example demonstrates how you would send
        parameters from your React front-end to the serverless function and get
        response back.
      </Text>
      <Stack>
        <Input
          name="text"
          label="Send to serverless"
          onInput={(t) => setText(t)}
        />
        <Button type="submit" onClick={run}>
          Click me
        </Button>
      </Stack>
    &lt;/>
  );
};
```

### package.json

Metadata about the extension's front-end. This file is required and can be used to include dependencies for your React front-end.

```json
// Example package.json

{
  "name": "example-extension",
  "version": "0.1.0",
  "description": "",
  "license": "MIT",
  "main": "Example.jsx",
  "scripts": {
    "dev": "hs-ui-extensions-dev-server dev",
    "build": "hs-ui-extensions-dev-server build"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/HubSpot/ui-extensions-react-examples"
  },
  "dependencies": {
    "@hubspot/ui-extensions": "latest",
    "react": "^18.2.0"
  }
}
```

## Start local development

With your project files created locally, you can now run `hs project dev` to upload the files to HubSpot, then run `hs project dev` to start a local development server. start a local development server to view the extension in HubSpot. The local development server will pick up changes saved to your React files without needing to refresh the page or re-upload the project.

Learn more about [local development for UI extensions](/guides/crm/ui-extensions/local-development).

```shell
hs project dev
```

## Adding cards to the UI

To view an app card in HubSpot, you'll need to add it to the UI in its specified location. For example, to add a card to the contact record view:

- Log in to your HubSpot account.
- In your HubSpot account, navigate to **Contacts** > **Contacts**. Then, click the **name** of a contact to view its record.
- At the top of the contact record, click **Customize tabs**. A new tab will open showing the record editor sidebar.

  

- In the right sidebar, click **Default view** to edit the default contact record view.
- For the purposes of this tutorial, click the **\+ plus icon tab** at the top of the editor to add a new tab.

  

- In the dialog box, enter a **name** for your new tab, then click **Done**.
- With the new tab added, click the **Add cards** dropdown menu.
- In the right panel, under _Card types_, click **Apps**.

  

- Select the **checkbox** next to the example extension that's part of your public app.
- Click the **X** at the top of the right panel to close it.
- In the top right, click **Save and exit**.
- Navigate back to the contact record, then refresh the page. You should now see your new tab, which will contain your new card. With the local development server running, you'll see a _Developing locally_ tag displayed at the top of the card.

.


# Local development for UI extensions (BETA)

When building a UI extension, you can run a local development server to iterate and test functionality in real-time without needing to re-upload the project. The local development server is only available for the frontend portion of the app, and assumes the back-end is either running locally or is hosted.

Your app must be uploaded to your development account for the server to work. The local development server will only pick up changes made to React files. For changes to configuration files, you'll need to use `hs project upload`.

## Prerequisites

To run a local development server, ensure that you meet the following prerequisites.

- Your project must be already uploaded to your account (`hs project upload`).
- You must install the necessary dependencies in your `extensions` directory. If you haven't done so yet, navigate into the `/src/app/extensions` directory and run `npm install`.
- You must be running v6.1.0 of the HubSpot CLI or above. You can check your version by running `hs --version`, and update by running `npm i -g @hubspot/cli@latest`.

```shell
cd src/app/extensions
npm install
```

## Start a local development server

Before starting local development, keep in mind the following for public apps:

- If you're building a public app that's installed in production accounts, you should not use the production app for local development. Instead, work with a duplicate of the app to avoid making updates to the production app. You can clone an existing public app by running `hs project clone-app`.
- If you have a locally running back-end, you can set up a proxy to remap `hubspot.fetch()` requests made during local development. This proxy is configured through a `local.json` file in your project. Learn more about [proxying requests to a locally running back-end](/guides/crm/public-apps/fetching-data#proxying-requests-to-a-locally-running-back-end).

To start a local development server:

- In the terminal, run `hs project dev` command in a project directory.

```shell
hs project dev
```

- You'll then be prompted to select the **account** to run the server in. This should be the account that the app is installed in.

  - If you're building a public app, you'll only be able to select from your connected developer test accounts.

    

  - If you're building a private app, you can select a production account or one of its development sandboxes, or you can create a new development sandbox.
When creating a new development sandbox, if you receive the error `The personal access key you provided doesn't include sandbox permissions`, you'll need to deactivate the account's Personal Access Key, then create a new one with sandbox permissions. To do so, run `hs auth`, then follow the prompts to select your account. Then, click **Deactivate** next to the personal access key, and generate a new one with the proper scopes.
- If your project has multiple extensions, you'll be prompted to select which extension to run. You can run multiple extensions from the same app, but not multiple extensions across multiple apps.

After selecting the account and extension, the local development server will then start. To view the locally running version of your UI extension:

- In your browser, navigate to where the app's UI extension is located, or refresh the page if it's already loaded.
- The extension will be updated with a _Developing locally_ tag, which indicates that changes to your React files will now be picked up automatically on save without needing to refresh. Changes made to configuration files will not be picked up by the local development server, and will instead need to be uploaded with `hs project upload`.

  

When you're ready to push your changes to HubSpot, end the server by pressing **q** in the terminal, then run `hs project upload`. If your project is not enabled for [auto-deploy](/guides/crm/developer-projects/create-a-project#view-the-project-in-hubspot), you can then deploy it after successful build by running `hs project deploy`.

## Authenticating serverless function requests

You can authenticate private app serverless function requests during local development using either the private app access token or secrets. To authenticate requests using a secret, add the secret to a `.env` file within the `.functions` directory, then reference it by name:

```shell
SECRET=my-secret-value
```

The method for authenticating requests with a private app access token during local development depends on the version of the HubSpot CLI you're running:

- If you're running HubSpot CLI version 7.0.0 or above, private app access tokens are automatically available for authentication as long as your personal access key has the `developer.app_functions.write` and `developer.app_functions.read` scopes.
- If you're running HubSpot CLI version 6.x or below, you'll need to add the token to a `.env` file within the `.functions` directory, similar to using secrets. You can find the [private app access token in the app's settings in HubSpot](/guides/crm/private-apps/creating-private-apps#view-the-app-in-hubspot).

```shell
PRIVATE_APP_ACCESS_TOKEN=pat-na1-******
SECRET=my-secret-value
```

Learn more about [serverless functions](/guides/crm/private-apps/serverless-functions).


# UI Extensions overview (BETA)

HubSpot UI extensions enable you to customize HubSpot’s UI to suit your needs. UI extensions can be created for CRM records and the ticket panels within help desk, and they can interact with both HubSpot and external data using a [private](/guides/crm/private-apps/creating-private-apps) or [public app](/guides/crm/public-apps/creating-public-apps) included in the project.

Below, learn more about how UI extensions work, including how they fetch data, along with best practices.

## How UI extensions work

UI extensions are built using React as a primary front-end framework. For UI extensions in private apps, the back-end is provided by HubSpot using serverless functions for operations such as fetching data. For UI extensions in public apps, you'll need to provide a custom back-end to handle OAuth authentication.

Extensions are built locally using the HubSpot CLI, which you'll use to run a HubSpot local development server for the front-end and deploy back-end changes to the HubSpot account. Extensions are powered by the [UI extensions SDK,](/guides/crm/ui-extensions/sdk) which offers a number of utilities, including UI components to create the visual elements of the extension. The UI extensions SDK is published as an [npm package](https://www.npmjs.com/package/@hubspot/ui-extensions/v/next), which you'll include in the `package.json` file within the `extensions` directory.

The UI extensions SDK enables you to:

- Register an extension with HubSpot
- Run serverless functions (private apps only)
- Add actions to your extension, such as opening an iframe in a modal, adding success and failure alerts, and fetching CRM properties

- Build the extension's UI with components
- Fetch user and account data[](#fetch-user-and-account-data)

Learn about [adding functionality to your extension through the SDK](/guides/crm/ui-extensions/create#add-ui-extension-functionality).
While an _Enterprise_ account is needed to create UI extensions for private apps, a paid seat is not required to view UI extensions or custom tabs on CRM records. Any user in the account will be able to view and use UI extensions once uploaded to the account.
## Structure and schema

Within the project structure, UI extensions are stored in a `/src/app/extensions` directory. The extension then uses [UI components](/reference/ui-components/overview) to render a UI and display information retrieved by the app’s serverless function. UI components are provided by the UI Extension SDK and can be customized through their included fields. Because you’re using React, you can also create your own component building blocks by packaging these components and importing them as needed.

At a high level, a React-based UI extension consists of:

- **A back-end:** for [private apps](/guides/crm/private-apps/creating-private-apps), the back-end is provided for you through [serverless functions](/guides/crm/private-apps/serverless-functions). The `app.functions` directory contains the serverless function JavaScript file and a JSON configuration. You can also add a `package.json` file to include any needed dependencies. The serverless function sends and fetches data that your React components can later use. For [public apps](/guides/crm/public-apps/creating-public-apps), this directory is not needed, as you'll provide your own back-end.
- **React front-end:** an extensions directory containing `.jsx` or `.tsx` files to render components, along with the app card’s `.json` configuration and a required `package.json` that includes any needed dependencies.
As shown in the screenshot above, the front-end and back-end directories both include their own `package.json` file. You can use these files to include dependencies as needed.
## Location and object support

UI extensions can be created for a variety of locations and CRM objects in HubSpot. An extension can only appear in one location, but can be appear in that location for multiple CRM objects.

### Locations

UI extensions can be built for the following locations:

- The middle column of CRM record pages

  

- The right sidebar of CRM record pages

  

- The CRM record preview panel that appears on the right side throughout HubSpot

  

- The ticket sidebars within the help desk tool (both the preview sidebar and the help desk ticket record view)

  

Learn more about [extension locations](/guides/crm/ui-extensions/create#extension-location).

### CRM objects

UI extensions can be built for the following CRM objects:

- Contacts
- Companies
- Deals
- Tickets
- Custom objects

In addition, the following CRM objects are supported if you've enabled the corresponding [data model template (BETA)](https://knowledge.hubspot.com/data-management/data-model-templates):

- Appointments and services (Healthcare template)
- Courses (Education template)
- Listings (Real estate template)

Learn more about [compatible objects](/guides/crm/ui-extensions/create#compatible-objects).

## Working with data

You can fetch data in multiple ways, depending on the data source:

- To fetch third-party data, you can make API requests authenticated by secrets.
- To fetch HubSpot data, you can:
  - use the `fetchCrmObjectProperties` action to fetch data from the currently displaying CRM record.
  - use HubSpot's API endpoints to fetch data outside of the currently displaying CRM record.
  - use GraphQL to query CRM data directly.

To fetch data with a private app, you'll use [serverless functions](/guides/crm/private-apps/serverless-functions). To fetch data with a public app, you'll use the [hubspot.fetch API](/guides/crm/public-apps/fetching-data).

### Fetch data via API call

- For private apps:
  - You can fetch data from external APIs using an API client, such as Axios or other third-party clients, in your serverless functions. To authenticate these requests, learn how to [create and use secrets in serverless functions](/guides/crm/private-apps/serverless-functions#including-secrets-in-a-function). To see an example of using a serverless function to fetch data from a third-party source, check out the [mapbox-api code sample extension](https://github.com/HubSpot/ui-extensions-examples/tree/main/mapbox-api).
  - You can fetch data from HubSPot's APIs using serverless functions, but instead of authenticating with a secret, you'll use the private app's access token. Learn more about [authenticating HubSpot API calls with private app access tokens](/guides/crm/private-apps/serverless-functions#authenticate-hubspot-api-calls).
- For public apps, you can fetch data using the [hubspot.fetch API](/guides/crm/public-apps/fetching-data). You'll need to provide authentication through your self-hosted back-end.

### Fetch HubSpot CRM data

To fetch data from the HubSpot account, such as displaying property data from the record you’re viewing, you’ll pass the `fetchCrmObjectProperties` method to the extension via `hubspot.extend()` in your React files. This method automatically handles authentication, so you don't need to include a private app access token. Learn more about [the fetchCrmObjectProperties method](/guides/crm/ui-extensions/create#fetch-crm-property-data).

```js
hubspot.extend(({ actions }) => (
  <HelloWorld fetchProperties={actions.fetchCrmObjectProperties} />
));

const HelloWorld = ({ fetchProperties }) => {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  useEffect(() => {
    fetchProperties(['firstname', 'lastname']).then((properties) => {
      setFirstName(properties.firstname);
      setLastName(properties.lastname);
    });
  }, [fetchProperties]);

  return (
    <Text>
      Hello {firstName} {lastName}
    </Text>
  );
};
```

In addition to using `fetchCrmObjectProperties`, [CRM data components](/reference/ui-components/crm-data-components/overview) can fetch and visualize HubSpot data out of the box. You can also use GraphQL to query CRM data through the `/collector/graphql` endpoint. Learn more about [querying CRM data using GraphQL](/guides/cms/content/data-driven-content/graphql/query-hubspot-data-using-graphql).

## Best practices

### Using serverless functions in private apps

For security, the React front-end cannot fetch data directly with APIs. For private apps, you can fetch data on the back-end using serverless functions. Then, on the front-end you'll use the `hubspot.serverless` API to call the serverless function and send your JSON payload as parameters. Learn more about [serverless functions](/guides/crm/private-apps/serverless-functions).

Because UI extensions are split between front-end and back-end, you can call multiple serverless functions from the same card. Or, you can reuse the same serverless function to run different operations and pass them as needed to the front-end.

If your serverless function requires secrets, learn more about [managing secrets for deployed serverless functions and local development](/guides/crm/private-apps/serverless-functions#managing-secrets).

## Limitations

To render UI extensions, HubSpot uses [sandboxed iframes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox) for isolation, [web workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) for untrusted code execution, and [Shopify's remote-ui library](https://shopify.engineering/remote-rendering-ui-extensibility) for UI abstraction. This enables you to build with familiar tools like React and JavaScript, and `remote-ui` translates that into specific components to the host via `postmessage`. This process is essentially serializing a React element tree and sending it through `postmessage` for the host to evaluate. This results in benefits, such as:

- Helping to protect both you and HubSpot with a sound and resilient solution for isolated code execution, while ensuring a consistent in-app user experience through the use of HubSpot's component library.
- Providing you with a productive and delightful developer experience by leveraging mainstream frameworks like React.

However, it also separates the UI extensions from typical web applications built with React. Below, learn more about some of differences you can expect when developing React-based UI extensions.

### File path character limit

A project cannot contain file paths that exceed 500 characters. If your project includes a file whose path exceeds that limit, you'll receive the following error in the console when uploading your project:

`"[ERROR] File paths must not exceed 500 characters. Shorten the following file path, and then try again. {Relevant erroring file path}."`

### Non-HubSpot component libraries

When building UI extensions on HubSpot, you can only use the [components](/reference/ui-components/overview) provided through the [UI extensions SDK](/guides/crm/ui-extensions/create#add-ui-extension-functionality). Each component has a set of parameters that you can use for customization, but these components cannot be customized beyond those parameters.

### Accessing DOM elements

Because UI extensions are rendered through sandboxed iframes, they cannot directly access the DOM. This restriction means that common methods of DOM manipulation and event listening, such as `document.getElementById` or `document.addEventListener`, are unavailable within the iframe's local script context. However, some components provide callback functions that you can use for certain events, like clicks, focus, and input.

#### Examples of unsupported hooks

- React Router's `useNavigate()` hook will not function as expected within a UI extension. This hook manipulates the browser's window object to change the URL, which is not allowed within a sandboxed iframe.
- The `useForm()` hook from react-hook-form is not supported because it relies on creating event listeners directly on DOM elements for form validation and submission, which is not allowed within a sandboxed iframe.

### Custom styles on HubSpot components

Because you can't access the DOM and components don't pass the `style` prop to the renderer, you can only style components with the provided component props.

### Making client-side HTTP requests

You cannot make client-side HTTP requests through UI extensions, meaning browser features like `fetch` and `XMLHttpRequest` will not work, nor will libraries like Axios, which is built around those features. Instead, requests should be made through the serverless function, executed by HubSpot behind the scenes.

### Using cookies to store session information

Requests made within the sandbox do not contain cookies, so rather than trying to store session information with cookies, you can look to the `context` object, which is passed to the extension component via `hubspot.extend`. This object contains information related to the authenticated user and HubSpot account. Learn more about [fetching account and user data](/guides/crm/ui-extensions/create#fetch-account-and-user-information).


# Deals summary sample project tutorial (BETA)

To better understand how to build UI extensions and get the most out of CRM data, extension components, and project development, HubSpot provides a set of [sample UI extensions](/guides/crm/ui-extensions/sample-extensions/overview) featuring a variety of functionalities, including this deal summary sample extension. These sample projects can server as inspiration, reference, or a starting point for your own UI extensions.

By the end of this tutorial, you will have:

- Uploaded the deal summary sample project to your account.
- Created two deal records to supply the custom card with example data.
- Customized the card with a component to display calculated deal data.

For the purposes of this tutorial, it's recommended to complete these steps in a [development sandbox](/guides/crm/setup#create-and-use-development-sandboxes) account to keep this example data separate from the production account.
If you haven't created a UI extension yet, it's recommended to [follow the quickstart guide](/guides/crm/private-apps/quickstart) first.
## Update your CLI and authenticate your account

To get started, you'll first need to ensure that your CLI is on the latest version that your account is authenticated. If this is your first time setting up your local environment, check out the [projects setup guide](/guides/crm/setup).

- Update to the latest CLI version by running `npm install -g @hubspot/cli@next`.
- If you haven't yet connected your production account to the CLI, run `hs init` to create the necessary `hubspot.config.yml` file. You can either create this file in your working directory or in one of its parent directories. When working locally, the CLI will use whichever config file is closest, meaning you can also have multiple config files depending on your preferred workflow.

## Clone the sample project and upload to your account

With your local environment set up, you'll now download the sample project and upload it to your account. You'll then be able to view the extension's custom card on any contact record in your account.

- Clone the [sample app](https://github.com/HubSpot/ui-extensions-examples/tree/main/deals-summary) to your working directory.
- Upload the sample app to your account by running `hs project upload`, then follow the CLI prompts to complete the upload.
- With the project uploaded, log in to your HubSpot account.
- In your HubSpot account, navigate to **Contacts** > **Contacts**.
- Click the **name** of any contact record. If you're in a new account, you can click the **Brian Halligan** sample contact, or click **Create contact** in the upper right to create a new test contact.
- On the contact record, click the **Custom** tab at the top of the record. On this tab, you'll see the uploaded sample project card titled _Deals summary_. Because you haven't yet created any deals associated with the contact, the card will show no data.

  
If the project uploaded successfully but you're not seeing the custom card on the contact record, you may need to click **Customize record** to add the card to the _Custom_ tab first.
Next, you'll create two deals associated with the contact to populate the card with data.

## Create two deals

If your test contact has no deals associated with it, you'll need to create two deals to fill the custom card with data.

- In the _Deals_ card in the right sidebar of the contact record, click **\+ Add**.

  

- In the right panel, give the deal a **Deal name** and **amount**. Then, click **Create and add another** at the bottom of the panel.
- Repeat the same step as above to give the second deal a **Deal name** and **amount**. Then, click **Create** at the bottom of the panel.

Because you created these deals from the contact record, they'll automatically be associated with the contact. If you now refresh the page, the card will populate with the aggregated deal data. Next, you'll start the local development server and update your project with authentication so that you can fetch more deal data using HubSpot's API.

## Start local development

By starting a local development server, you'll be able to update the card's frontend and see your saved changes without needing to upload first. And by adding the private app access token to the project, you'll enable the serverless function to make authenticated requests to the [HubSpot deals API](/guides/api/crm/objects/deals). Learn more about [running the local development server](/guides/crm/private-apps/quickstart#start-local-development) and [managing secrets](/guides/crm/private-apps/serverless-functions#managing-secrets).

- Run `npm install` to install dependencies needed for running the local development server.
- Inside the `src/app/app.functions` folder, create a new file named `.env` to store your private app access token.
- Retrieve the private app's access token:

  - In your HubSpot account, navigate to **CRM Development**.
  - In the left sidebar menu, navigate to **Private apps**.
  - Click **Deals summary app** to navigate to the app's overview page.
  - Click the **Auth** tab.
  - In the _Access token_ section, click **Show token**. Then, click **Copy**.

    

  - Paste the token into your `.env` file using the following format:

```shell
PRIVATE_APP_ACCESS_TOKEN=your-access-token
```

- Save the file.
- Start the local development server by running `hs project dev`, then following the prompts to select the account to develop in.
- Return the contact record in HubSpot and refresh the browser. You should now see an icon on the card that says _DEVELOPING LOCALLY_.

  

Next, you'll update your local extension files to fetch deal data with the API, calculate average deal margin, and add a new component to the card.

## Add the average margin component

You'll now update the card with a new component to display the calculated average deal margin. You'll first update the card's backend (serverless function), then the frontend React files.

### Update the serverless function

- In your project files, open the `get-data.js` file. This serverless function is using the HubSpot API client to fetch associated deal data through the `getAssociatedDeals` function.
- Add the following function to the bottom of the file, below the `calculateTotalAmounts` function.

```js
function calculateAverageAmount(deals) {
  const totalCount = deals.length;

  const amounts = deals.map((deal) => parseFloat(deal.properties.amount));
  const totalDeals = amounts.reduce((sum, amount) => sum + amount, 0);

  const average = Math.ceil(totalDeals / totalCount);
  return -Math.round(-average);
}
```

- To store the result of this function, update `exports.main` with a new variable, then add that variable to the existing `sendResponse`.

```js
const avgAmount = calculateAverageAmount(deals);

sendResponse({ dealsCount: deals.length, totalAmount, avgAmount });
```

### Update the React frontend

- Open the `DealsSummaryExampleCard.jsx` file, then add the following state variable to define the initial state of the average amount.

```js
const [avgAmount, setAvgAmount] = useState(0);
```

- Update the state within the `useEffect` function.

```js
setAvgAmount(serverlessResponse.response.avgAmount);
```

- Lastly, add a new [`StatisticsItem` component](/reference/ui-components/overview#statistics) to display the averaged amount in the card.

```js
<StatisticsItem label="AVG MARGIN" number={avgAmount}>
  <Text>Average margin</Text>
</StatisticsItem>
```

After making the above changes, the `DealsSummaryExampleCard.jsx` file should contain the following code:

```js
import React, { useState, useEffect } from 'react';
import {
  Alert,
  LoadingSpinner,
  Statistics,
  StatisticsItem,
  Text,
  Flex,
} from '@hubspot/ui-extensions';
import { hubspot } from '@hubspot/ui-extensions';

// Define the extension to be run within the Hubspot CRM
hubspot.extend(() => <DealsSummary />);

// Define the Extension component
const DealsSummary = () => {
  const [loading, setLoading] = useState(true);
  const [errorMessage, setErrorMessage] = useState('');
  const [dealsCount, setDealsCount] = useState(0);
  const [totalAmount, setTotalAmount] = useState(0);

  useEffect(() => {
    // Request statistics data from serverless function
    hubspot
      .serverless('get-data', {
        propertiesToSend: ['hs_object_id'],
      })
      .then((response) => {
        setDealsCount(response.dealsCount);
        setTotalAmount(response.totalAmount);
      })
      .catch((error) => {
        setErrorMessage(error.message);
      })
      .finally(() => {
        setLoading(false);
      });
  }, []);

  if (loading) {
    // If loading, show a spinner
    return <LoadingSpinner />;
  }
  if (errorMessage) {
    // If there's an error, show an alert
    return (
      <Alert title="Unable to get deals data" variant="error">
        {errorMessage}
      </Alert>
    );
  }
  return (
    <Flex direction={'column'} gap={'lg'}>
      <Text variant="microcopy">
        This example shows you how you can view a high-level summary of data
        from associated deals.
      </Text>
      <Statistics>
        <StatisticsItem label="Open deals" number={dealsCount}>
          <Text>Total number of deals contact is associated with</Text>
        </StatisticsItem>
        <StatisticsItem label="Unit price" number={totalAmount}>
          <Text>High End</Text>
        </StatisticsItem>
      </Statistics>
    </Flex>
  );
};
```

With your changes saved, you should now see the new component in the card displaying the calculation.
- To quit the local development server and upload your finished work, press `q` in the terminal, then run `hs project upload`.


# Sample UI extensions (BETA)

Check out HubSpot's sample UI extensions to get familiar with what's possible when building UI extensions. These sample projects can be used as inspiration, reference, or starting points for your own projects.
- Unless otherwise noted, these sample UI extensions are built for private apps, which require a _**Sales Hub**_ or _**Service Hub**_ _Enterprise_ subscription to install in a standard HubSpot account.
- A paid subscription is <u>not</u> required to install sample apps in a developer test account. All of these sample extensions can be used as a inspiration for a public app built in a free developer account.
- [Bidirectional property refresh](#bidirectional-property-refresh)
- [Build a multi-step flow](#build-a-multi-step-flow)
- [Create a deals summary](#create-a-deals-summary)
- [Custom logger example](#custom-logger-example)
- [Display an iframe modal](#display-an-iframe-modal)
- [Duplicate contact](#duplicate-contact)
- [Generate multiple quotes](#generate-multiple-quotes)
- [Create line and bar charts](#create-line-and-bar-charts)
- [Manage layouts: Flex and Box](#manage-layouts-flex-and-box)
- [Overlay example](#overlay-example)
- [Use CRM data components](#use-crm-data-components)
- [View nearby companies: Mapbox API](#view-nearby-companies-mapbox-api)

## Bidirectional property refresh

<LinkWithTracking
  href="https://github.com/HubSpot/ui-extensions-examples/tree/main/bi-directional-property-refresh"
  linkText="View the source code in GitHub"
/>

Provides bidirectional refresh of contact property changes between a custom card and the CRM record page.

**Example use case:** a salesperson in your account makes frequent changes to a contact's properties and associated fields on a custom card and requires a seamless editing experience to avoid manual page refreshes.

**What's included:** a _Refresh properties between custom card and CRM page_ card that displays the latest `firstname` and `lastname` property values when changed on the CRM record page, as well as a lifecycle stage dropdown menu that you can update on the card and immediately see the change reflected on the CRM record page.

**What you'll learn:**

- How you can use the `fetchCrmObjectProperties` and `onCrmPropertiesUpdate` actions to listen to changes on the CRM record page and fetch the latest values.
- How you can ensure data in the custom card stays up to date with the `refreshObjectProperties` action.

## Build a multi-step flow

<LinkWithTracking
  href="https://github.com/HubSpot/ui-extensions-examples/tree/main/multi-step-flow"
  linkText="View the source code in GitHub"
/>

Send a meal from a local restaurant to one of your contacts. This example extension includes code for both public and private app implementations.

**Example use case:** with one of their agency partners working overtime to close an upgrade deal, an account manager wants a way to order them dinner from the CRM as a thank you.

**What's included:** an example card for contact records featuring search, data validation, custom components, and more.
**What you'll learn:**

- Searching a table, including pagination.
- Implementing realtime form validation.
- Fetching data asynchronously with HubSpot serverless functions.
- Including empty, error, and loading states.
- Using the [Panel component](/reference/ui-components/standard-components/panel) to display a form.
- Getting current contact properties.
- Getting current user properties.
- Triggering alerts in the CRM outside of the card 's boundaries.

## Create a deals summary

<LinkWithTracking
  href="https://github.com/HubSpot/ui-extensions-examples/tree/main/deals-summary"
  linkText="View the source code in GitHub"
/>

View a high-level summary of data from associated deals. Follow the [tutorial](/guides/crm/ui-extensions/sample-extensions/deals-summary-tutorial) (also included in the project's [README.md](https://github.com/HubSpot/ui-extensions-examples/blob/main/deals-summary/README.md) file) to walk through how to add the extension to your account, create two example deal records to display data from, then customize the extension with additional components.

**Example use case:** a salesperson needs a way to quickly view important deal information from their contact records so that they can plan, prioritize, and report on their efforts.

**What's included:** an example card for contact records that aggregates and displays associated CRM record data.

****

**What you'll learn:** how to surface data from associated CRM records and how to customize extensions with additional components.

## Custom logger example

<LinkWithTracking
  href="https://github.com/HubSpot/ui-extensions-examples/tree/main/custom-logger-example"
  linkText="View the source code in GitHub"
/>

Explore the various `logger` API methods by sending custom logs from deployed extensions. When used in local development mode, logs will be sent to the browser console. In cases where the extensions fail to load, a trace ID will be generated which you can then use to trace the log within the [app's logs in HubSpot](/guides/crm/private-apps/creating-private-apps#logs). Learn more about the [custom logger API](/guides/crm/ui-extensions/sdk#send-custom-log-messages-for-debugging).

**What's included:**

- **Middle column card:** an example card for the middle column of contact records, which includes custom logging for serverless function success/failure, logging different types of information, and logging for extension failure.

  

- **Sidebar card:** an example card for the right sidebar of contact records, which includes custom logging for serverless function success/failure, logging different types of information, and logging for extension failure.
**What you'll learn:** how to use the [custom logger API](/guides/crm/ui-extensions/sdk#send-custom-log-messages-for-debugging) to enable more in-depth troubleshooting of UI extensions.

## Display an iframe modal

<LinkWithTracking
  href="https://github.com/HubSpot/ui-extensions-examples/tree/main/display-iframe-modal"
  linkText="View the source code in GitHub"
/>

Open a popup iframe modal on button click to embed external content on CRM records. Learn more about [opening iframe modals in UI extensions](/guides/crm/ui-extensions/sdk#open-an-iframe-in-a-modal).

**Example use case:** you want to embed a section of your company's ERP so that sales reps can check inventory levels when putting together potential deals.

**What's included:** an example card for contact records that includes descriptive text and a button that loads Wikipedia in an iframe modal.

****

**What you'll learn:** how to launch a modal that contains an iframe for displaying external content.

## Duplicate contact

<LinkWithTracking
  href="https://github.com/HubSpot/ui-extensions-examples/tree/main/duplicate-contact"
  linkText="View the source code in GitHub"
/>

Duplicate a contact along with some of its properties and associated deals and companies.

**Example use case:** after an in-person consultation, a marketer needs a way to quickly create several contact records for a new group of clients who share similar data.

**What's included:** an example card for contact records featuring data display and form submission.
**What you'll learn:**

- Fetching data asynchronously using HubSpot serverless functions.
- Using loading and error states.
- Making GraphQL calls inside serverless functions.

## Generate multiple quotes

<LinkWithTracking
  href="https://github.com/HubSpot/ui-extensions-examples/tree/main/generate-quotes"
  linkText="View the source code in GitHub"
/>

Match a customer with a service based on specified criteria, then generate a sales quote. The extension is built on top of the HubSpot quotes tool and uses the [quotes API](/guides/api/crm/commerce/quotes).

**Example use case:** a fictional shuttle bus rental company with several service options needs a way to match customers with the most appropriate service, then generate a quote for them.

**What's included:** a _Shuttle bus quotes sample_ card for deal records featuring a multi-part form and quote generator.
**What you'll learn:**

- Building multi-page forms.
- Fetching data asynchronously using HubSpot serverless functions.
- Customized sales quote generation.

## Create line and bar charts

<LinkWithTracking
  href="https://github.com/HubSpot/ui-extensions-examples/tree/main/charts-example"
  linkText="View the source code in GitHub"
/>

Render two example charts based on customer purchase history and revenue distribution over time. This example extension includes code for both public and private app implementations.

**Example use case:** you want to analyze revenue data associated with a contact so you can reach out to them with other related products they might be interested in.

**What's included:** a `LineChart` component and `BarChart` component that render based on randomly generated test data.
**What you'll learn:**

- Configuring line and bar chart display options.
- Formatting test data to render correctly in the context of the two chart types.

## Manage layouts: Flex and Box

<LinkWithTracking
  href="https://github.com/HubSpot/ui-extensions-examples/tree/main/flex-and-box"
  linkText="View the source code in GitHub"
/>

Learn how the `Flex` and `Box` components can be used to [manage extension layout](/reference/ui-components/manage-ui-extension-layout).

**Example use case:** you want to customize the structure of your card and its various components to better organize information and improve UX.

**What's included:**

- **Flex playground card:** a card for deal records where you can experiment with various `Flex` props to better understand how they work together.
- **Flex and box example card:** a real estate listing card for deal records that uses `Flex` and `Box` to arrange information on each listing. This card does not include any real data handling functionality. The example data is hardcoded for component demonstration purposes only.
**What you'll learn:**

- Using the `Flex` and `Box` [layout components](/reference/ui-components/manage-ui-extension-layout).
- Improving `Form` component arrangement.
- Optimizing the space of your cards through a variety of components.
- Using `Tile` components to group information.
- Including multiple extensions in one project.

## Overlay example

<LinkWithTracking
  href="https://github.com/HubSpot/ui-extensions-examples/tree/main/overlay-example"
  linkText="View the source code in GitHub"
/>

Explore the types of extension overlays available, including all variants available for [Modal](/reference/ui-components/standard-components/modal) and [Panel](/reference/ui-components/standard-components/panel).

**What's included:** a card for contact records that lists all available `Modal` and `Panel` variants in an interactive table.
**What you'll learn: how to launch and close `Modal` and `Panel` components from an extension.**

## Use CRM data components

<LinkWithTracking
  href="https://github.com/HubSpot/ui-extensions-examples/tree/main/crm-data-components"
  linkText="View the source code in GitHub"
/>

View a contact's associated deal information in a table, then navigate to the deal record to view and modify its progress in the sales pipeline.

**Example use case:** a salesperson needs a way to quickly view a contact's associated deal information, then navigate to the deal record to confirm the deal's progress and close it.

**What's included:**

- **Association Table Card:** a card for contact records to display high-level associated deal information in a table.
- **Stage Tracker Card:** a card for deal records to view and update pipeline stage progress.
**What you'll learn:**

- Using [CRM data components](/reference/ui-components/overview#crm-data-components), which are pre-configured to display CRM data.
- Using the `Flex` [layout component](/reference/ui-components/manage-ui-extension-layout).
- Making API calls to the HubSpot API using serverless functions.
- Fetching properties for the currently displaying record.

## View nearby companies: Mapbox API

<LinkWithTracking
  href="https://github.com/HubSpot/ui-extensions-examples/tree/main/mapbox-api"
  linkText="View the source code in GitHub"
/>

Find and display companies that are located near the currently displaying company record.

**Example use case:** a salesperson will be traveling to visit client and wants to know if there are other companies they can visit along the way.

**What's included:**

- **Top value companies within radius:** a card for company records to show the companies nearest to the currently displaying record.

  

- **Nearest companies to currency company record:** a card for company records to search for nearby companies by mile radius.

  

**What you'll learn:**

- Including multiple UI extensions in one project using one private app.
- Working with third-party data.


# UI extensions SDK (BETA)

The UI extensions SDK is the foundation for [building UI extensions in HubSpot](/guides/crm/ui-extensions/create), providing an assortment of methods and utilities that enable you to:

- Access account, extension, and user context
- Run serverless functions
- Perform actions
- Log custom messages for debugging
- Render the UI through UI components

Below, learn more about configuring your UI extension to access context and perform actions. For documentation on the UI components included in the SDK, check out the [UI components reference documentation](/reference/ui-components/overview).

## Registering the extension

UI extensions, like any React front-end, are written as React components. However, unlike typical React components, you must register your UI extension with HubSpot by including `hubspot.extend()` inside the component file instead of exporting it. This is not required when you create a sub-component. For cleaner code, reuse and include them inside your extension.

The `hubspot.extend()` function receives the following arguments:

- `context`: provides account, extension, and user context to the extension.
- `runServerlessFunction`: enables serverless functions to run in the extension when including a private app ([deprecated](#run-serverless-functions)).
- `actions`: makes actions available to the extension.

```js
hubspot.extend(({ context, actions }) => (
  <Extension context={context} sendAlert={actions.addAlert} />
));
```

The provided arguments can then be passed to the extension component as props.

```js
// Define the extension to be run within Hubspot
hubspot.extend(({ context, actions }) => (
  <Extension
    context={context}
    sendAlert={actions.addAlert}
  />
));
// Define the Extension component, taking in context, and sendAlert as props
const Extension = ({ context, sendAlert }) => {
  ...
};
```

If your extension doesn't need to access context, run serverless functions, or perform actions, you don't need to provide these arguments to `hubspot.extend()` or the extension component.

```js
hubspot.extend(() => <Extension />);
const Extension = () => {
  ...
};
```

## Access context data

The `context` object, passed to the extension component via `hubspot.extend()`, contains data related to the authenticated user and HubSpot account, along with data about where the extension was loaded. It has the following fields:

| Field | Type | Description |
| --- | --- | --- |
| `location` | `'crm.record.tab'` &#124; `'crm.record.sidebar'` &#124; `crm.preview` &#124; `helpdesk.sidebar` | The UI extension's location. |
| `crm.objectId` | Number | The ID of the CRM record (e.g., contact ID). |
| `crm.objectTypeId` | String | The ID of the CRM record's object type (e.g., `0-1`). See the [full list of object IDs](/guides/api/crm/understanding-the-crm#object-type-ids) for reference. |
| `extension.appId` | Number | The extension's app ID. |
| `extension.appName` | String | The name of the extension's app. |
| `extension.cardTitle` | String | The extension's title. |
| `portal.id` | Number | The ID of the HubSpot account. |
| `portal.timezone` | String | The account's timezone. |
| `user.email` | String | The user's email address. |
| `user.emails` | String[] | All of the user's associated email addresses. |
| `user.firstName` | String | The user's first name. |
| `user.id` | Number | The user's ID. |
| `user.locale` | String | The user's locale. |

## Run serverless functions

For UI extensions powered by private apps, you can use the `hubspot.serverless()` API to call app functions in your extension.
  <strong> Please note:</strong> the `hubspot.serverless()` API was introduced
  as of `@hubspot/ui-extensions@0.8.5`. While the previous method of using
  `runServerlessFunction` is still supported, the new API is more intuitive and
  efficient, as it avoids unnecessary prop-drilling in sub-components and
  provides responses with resolved promises regardless of the outcome. For
  posterity, you can find documentation for `runServerlessFunction` in the [next
  section](#runserverlessfunction-deprecated).
`hubspot.serverless()` expects a string containing the name of the function to call (as defined in `serverless.json`), and can receive an object containing `propertiesToSend` and `parameters`.
```js
// Example React front-end file
import { hubspot, Button } from '@hubspot/ui-extensions';

hubspot.extend(() => <Extension />);

const Extension = () => {
  const handleSubmit = () => {
    hubspot
      .serverless('my-function-name', {
        propertiesToSend: ['hs_object_id'],
        parameters: { extra: 'data' },
      })
      .then((response) => {
        // handle response, which is the value returned from the function on success
      })
      .catch((error) => {
        // handle error, which is an Error object if the function failed to execute
      });
  };
  return <Button onClick={handleSubmit} label="Click me" />;
};
```
```json
{
  "appFunctions": {
    "my-function-name": {
      "file": "function.js",
      "secrets": []
    }
  }
}
```
| Parameter | Type | Description |
| --- | --- | --- |
| `propertiesToSend` | Array | An array of properties to send the serverless function from the front-end. Property values are pulled from the currently displaying CRM record. This enables retrieving property values on the server-side when the function is called. |
| `parameters` | Object | Additional key-value pairs to supply to the function. |

On success, `hubspot.serverless()` will return `Promise<any>`. On failure, it will reject the promise.
Serverless functions have a response limit of 15 seconds. Functions that take longer to execute will fail.
Alternatively you can call the function with async/await syntax:

```js
import { hubspot, Button } from '@hubspot/ui-extensions';

hubspot.extend(() => <Extension />);

const Extension = () => {
  const handleSubmit = async () => {
    try {
      const response = await hubspot.serverless('my-function-name', {
        propertiesToSend: ['hs_object_id'],
        parameters: { extra: 'data' },
      });
      // handle response, which is the value returned from the function on success
    } catch (error) {
      // handle error, which is an Error object if the function failed to execute
    }
  };
  return <Button onClick={handleSubmit} label="Click me" />;
};
```

Learn more about [fetching data with serverless functions](/guides/crm/private-apps/serverless-functions).

#### runServerlessFunction (deprecated)

The `runServerlessFunction` argument enables the front-end React code to run a serverless function defined within the project.
  <strong>Please note:</strong> as of `@hubspot/ui-extensions@0.8.5`, this
  method has been deprecated in favor of `hubspot.serverless()`. UI extensions
  using this method will continue to function.
To make the serverless function available to the extension, you first pass it as an argument in `hubspot.extend()`, then as a prop in the `Extension` component. You can then call the function by `name` (as defined in `serverless.json`) and include additional options.

```js
// Define the extension to be run within the Hubspot CRM
hubspot.extend(({ runServerlessFunction }) => (
  <Extension
    runServerless={runServerlessFunction}
  />
));

// Define the Extension component, taking in runServerless as a prop
const Extension = ({ runServerless }) => {

  // Call serverless function to execute with parameters.
  // The `myFunc` function name is configured inside `serverless.json`
  const handleClick = async () => {
    const { response } = await runServerless({
      name: "myFunc",
      propertiesToSend: ['hs_object_id'],
      parameters: { 'key':'value' }
    });
  };

  return (
    // UI components
  );
};
```

| Parameter | Type | Description |
| --- | --- | --- |
| `name` | String | The name of the serverless function to execute, as specified in `serverless.json`. |
| `propertiesToSend` | Array | An array of properties to send the serverless function from the front-end. Property values are pulled from the currently displaying CRM record. This enables retrieving property values on the server-side when the function is called. |
| `parameters` | Object | Additional key-value pairs to supply to the function. |

The status returned will result in a resolved promise regardless of the outcome. Success is returned as `{status: 'SUCCESS', response: ...}`, and failure is returned as `{status: 'ERROR', message: ...}`. The status is automatically determined by HubSpot and cannot be set manually using `sendResponse()`.

To trigger an error state, you'll need to build it into the serverless function by catching the error with `try ... catch` blocks. On the React front-end side, you'll need to check for `status` in the response, treating `SUCCESS` and `ERROR` appropriately. To ensure a good user experience in HubSpot, you can use the [ErrorState component](/reference/ui-components/standard-components/error-state) to communicate error messages with next steps.
Functions have a response limit of 15 seconds. Functions that take longer to execute will fail.
## Actions

Below are the actions that the SDK enables you to perform. Note that some UI components include a set of actions separate from the SDK actions below, such as the [CRM action components](/reference/ui-components/crm-action-components/overview).

- [Display alert banners](#display-alert-banners)
- [Fetch CRM property data](#fetch-crm-property-data)
- [Refresh CRM record properties](#refresh-crm-record-properties)
- [Listen for property updates](#listen-for-property-updates)
- [Open overlays](#open-overlays)
- [Open an iframe in a modal](#open-an-iframe-in-a-modal)
- [Copy text to clipboard](#copy-text-to-clipboard)
- [Upload files](#upload-files)

### Display alert banners

Use the `addAlert` method to send alert banners as a feedback for any actions to indicate success or failure. `addAlert` is a part of the `actions` object that can be passed to extension via `hubspot.extend`. If you instead want to render an alert within a card, check out the [`Alert` component](/reference/ui-components/standard-components/alert).
For example, the code below is a simplified version of the [CRM data components example extension](/guides/crm/ui-extensions/sample-extensions/overview#use-crm-data-components). Note that the `addAlert` action is passed into `hubspot.extend()` and the `Extension` component, then is triggered when the serverless function successfully executes.

```js
// Snippet from app/extensions/HelloWorld.jsx

hubspot.extend(({ context, actions }) => (
  <Extension
    context={context}
    addAlert={actions.addAlert}
  />
));

const Extension = ({ context, addAlert }) => {

  const handleStageChange = useCallback(
    (newStage: string) => {
      hubspot
        .serverless('updateDeal', {
          parameters: {
            dealId: dealId!,
            dealStage: newStage
          }
        })
        .then(response => {
          addAlert({
            type: 'success',
            message: 'Deal stage updated successfully'
          });
          setStage(newStage);
        })
        .catch(error => {
          setError(error.message || 'An error occurred');
        });
    },
    [dealId, addAlert]
  );

return (
// components
);
```

| Prop | Type | Description |
| --- | --- | --- |
| `title` | String | The bolded text of the alert. |
| `message` | String | The main alert text. |
| `variant` | `'info'` (default) &#124; `'tip'` &#124; `'success'` &#124; `'warning'` &#124; `'danger'` | The color of the alert.<ul><li><code>info</code>: a blue alert to provide general information.</li><li><code>success</code>: a green alert indicating a positive outcome.</li><li><code>warning</code>: a yellow alert indicating caution.</li><li><code>danger</code>: a red alert indicating a negative outcome.</li><li><code>tip</code>: a white alert to provide guidance.</li></ul> |

### Fetch CRM property data

There are three ways to fetch CRM property data:

- `fetchCrmObjectProperties`, which can be included in your React files to fetch property data client side at extension load time. This method is described below.
- `propertiesToSend`, which can be included in your serverless functions to fetch property data on the back-end at function invocation time. See the [Run serverless functions section](#run-serverless-functions) for more information.
- Use GraphQL to query CRM data through the `/collector/graphql` endpoint. Learn more about [querying CRM data using GraphQL](/guides/cms/content/data-driven-content/graphql/query-hubspot-data-using-graphql). To see an example of using GraphQL to fetch data, check out [HubSpot's contact duplicator sample project](/guides/crm/ui-extensions/sample-extensions/overview#contact-duplicator).
To make GraphQL requests, your app must include the following scopes:

- `collector.graphql_schema.read`
- `collector.graphql_query.execute`
**fetchCrmObjectProperties**

Using the `fetchCrmObjectProperties` method, you can get property values from the currently displaying CRM record without having to use HubSpot's APIs. This method is a part of the `actions` object that can be passed to the extension via `hubspot.extend`. You'll first need to add the object to `objectTypes` inside the card's `.json` config file. The objects you specify in `objectTypes` will also set which CRM objects will display the extension.

```js
hubspot.extend(({ actions }) => (
  <HelloWorld fetchProperties={actions.fetchCrmObjectProperties} />
));

const HelloWorld = ({ fetchProperties }) => {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  useEffect(() => {
    fetchProperties(['firstname', 'lastname']).then((properties) => {
      setFirstName(properties.firstname);
      setLastName(properties.lastname);
    });
  }, [fetchProperties]);

  return (
    <Text>
      Hello {firstName} {lastName}
    </Text>
  );
};
```

You can specify individual properties or fetch all properties with an asterisk:

```js
fetchCrmObjectProperties('*').then((properties) => console.log(properties));
```

The response for `fetchCrmObjectProperties` is formatted as:

```js
// example fetchCrmObjectProperties response
{
 "property1Name": "property1Value",
 "property2Name": "property2Value"
}
```

### Refresh properties on the CRM record

Use `refreshObjectProperties` to refresh the property data on the CRM record, and any CRM data components on the record without needing to refresh the page. This includes [cards added to the record through HubSpot's UI](https://knowledge.hubspot.com/object-settings/customize-records#manage-cards-in-the-middle-column). This method will work for the CRM objects that you include in the extension's `.json` file in the `objectTypes` array.
This method will <u>not</u> refresh property values in app cards that are fetched using HubSpot's APIs. Only HubSpot's built-in property fields and properties in CRM data components will be refreshed.
```js
import React, { useState } from 'react';
import {
  Divider,
  Button,
  Input,
  Flex,
  hubspot
} from '@hubspot/ui-extensions';

hubspot.extend(({ actions }) => (
  <Extension
    refreshObjectProperties={actions.refreshObjectProperties}
  />
));

const Extension = ({
  refreshObjectProperties,
}) => {

// Your extension logic goes here
// Refresh all properties of the object on the page
        refreshObjectProperties();
      }
    });
  };

return (
// Your extension body
)
```

### Listen for property updates

Use `onCrmPropertiesUpdate` to subscribe to changes made to properties on the CRM record and run functions based on those changes. This only includes changes made from within the HubSpot UI, not property updates from outside the UI, such as via APIs. This action is intended to be used like a React hook.

The full API for this method is as follows:

```js
export type onCrmPropertiesUpdateAction = (
 properties: string[] | '*',
 callback: (
   properties: Record<string, string>,
   error?: { message: string }
   ) => void
) => void;
```

As an example, the following function subscribes to updates made to the contact's first and last name properties, then logs those properties to the console.

```js
onCrmPropertiesUpdate(['firstname', 'lastname'], (properties) =>
  console.log(properties)
);
```

You can subscribe to all properties by using an asterisk.

```js
onCrmPropertiesUpdate('*', (properties) => console.log(properties));
```

To handle potential errors, pass the `error` argument to the callback.

```js
onCrmPropertiesUpdate(['firstname','lastname'], (properties, error) => {
   if(error) {
    console.log(error.message}
   }
   else {
     console.log(properties)
   }
})
```

### Open overlays
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.
To add another layer of UI to your extension, you can include overlays using the [Modal](/reference/ui-components/standard-components/modal) and [Panel](/reference/ui-components/standard-components/panel) components. To see an example of using overlays, check out [HubSpot's Overlay example project](https://github.com/HubSpot/ui-extensions-examples/tree/main/overlay-example).

- `Modal`: a pop-up dialog box best suited for short messages and action confirmations. A `'danger'` variant is included for destructive actions, such as deleting a contact.
- `Panel`: a slide-out sidebar best suited for longer, compartmentalized tasks that users might need to perform, such as multi-step forms. Includes a `'modal'` variant to obscure page content outside of the panel to focus the user on the panel task.

To add either type of overlay to your extension:

- Add the `overlay` prop to 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.
- Add the `Modal` or `Panel` component into the `overlay` prop.

By default, overlays include a close button in the top right, as shown below.
You can add a secondary closing mechanism by including a `Button`, `LoadingButton`, `Link`, `Tag`, or `Image` component within `overlay` that triggers the `closeOverlay` action in an `onClick` event. To use this action, you'll need to include the [actions argument](/guides/crm/ui-extensions/sdk#registering-the-extension) in `hubspot.extend()`.
- Only one `Modal` can be open at a time per extension. Opening a `Modal` when another one is already opened will cause the first one to close.
- A `Modal` can be opened from a `Panel`, but a `Panel` can't be opened from a `Modal`.
Below are examples of a panel overlay and a modal 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>
    </>
  );
};
```
```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>
    </>
  );
};
```
### Open an iframe in a modal

Similar to `addAlert` and `fetchCrmObjectProperties`, you can pass `openIframeModal` to the extension through the `actions` object. This action includes a callback, which you can use to run a function when the iframe modal is closed. The callback doesn't receive any parameters.

Learn more by checking out HubSpot's [Display an iframe modal sample project](/guides/crm/ui-extensions/sample-extensions/overview#display-an-iframe-modal).

```js
export type OpenIframeModalAction = (
  action: OpenIframeActionPayload,
  onClose?: () => void
) => void;
```

`openIframeModal` takes the following payload:

```js
interface OpenIframeActionPayload {
  uri: string;
  height: number;
  width: number;
  title?: string;
  flush?: boolean;
}
```

For example, the following code would result in an extension that opens an iframe on button click. The iframe is configured to contain the Wikipedia homepage with a height and width of 1000px and no padding. Upon closing the modal, a message will be logged to the console.

```js
import { Link, Button, Text, Box, Flex, hubspot } from '@hubspot/ui-extensions';

// Define the extension to be run within the Hubspot CRM
hubspot.extend(
  (
    { actions } // serverless function is not required for simply displaying an iframe
  ) => <Extension openIframe={actions.openIframeModal} />
);

// Define the Extension component, taking in openIframe as a prop
const Extension = ({ openIframe }) => {
  const handleClick = () => {
    openIframe(
      {
        uri: 'https://wikipedia.org/', // this is a relative link. Some links will be blocked since they don't allow iframing
        height: 1000,
        width: 1000,
        title: 'Wikipedia in an iframe',
        flush: true,
      },
      () => console.log('This message will display upon closing the modal.')
    );
  };

  return (
    <>
      <Flex direction="column" align="start" gap="medium">
        <Text>
          Clicking the button will open a modal dialog with an iframe that
          displays the content at the provided URL. Get more info on how to do
          this .
          <Link href="https://developers.hubspot.com/docs/platform/create-ui-extensions#open-an-iframe-in-a-modal">
            here
          </Link>
        </Text>

        <Box>
          <Button type="submit" onClick={handleClick}>
            Click me
          </Button>
        </Box>
      </Flex>
    </>
  );
};
```

### Copy text to clipboard

Use the `copyTextToClipboard` action to copy text to your clipboard. This action can be accessed through the actions argument (`actions.copyTextToClipboard`) and returns a promise that resolves once the system clipboard has been updated. Its functionality is provided by the [Clipboard: writeText() method](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText) and follows the same requirements.

This action only works after the user has interacted with the page after loading ([transient activation](https://developer.mozilla.org/en-US/docs/Web/Security/User_activation#transient_activation)).

```js
import React from 'react';
import { Button, Flex, hubspot, TextArea } from '@hubspot/ui-extensions';

hubspot.extend(({ actions }) => <Extension actions={actions} />);

function Extension({ actions }) {
  const textToCopy = `Copy me!`;

  // Use copy action on event handler
  async function handleOnClick() {
    try {
      // The function is async, make sure to await it.
      await actions.copyTextToClipboard(textToCopy);
      actions.addAlert({
        type: 'success',
        message: 'Text copied to clipboard.',
      });
    } catch (error) {
      // User error handling. copyTextToClipboard can fail with a `notAllowed` error.
      console.log(error);
      actions.addAlert({
        type: 'warning',
        message: "Couldn't copy text.",
      });
    }
  }

  return (
    <Flex direction="column" gap="md">
      <TextArea label="Text" value={textToCopy} />
      <Button onClick={handleOnClick}>Copy text</Button>
    </Flex>
  );
}
```

This action should be run by explicit user interaction, otherwise the action will fail by running before the page has rendered. For example, the following implementation would fail:

```js
function CopyButtonBadExample({ actions }) {
  const textToCopyWithoutUserPermission = `Please don't try this, it will fail`;

  useEffect(() => {
    /**
     * Don't run copyTextToClipboard without explicit interaction.
     * This will fail because the action will run before the page
     * has rendered.
     */
    async function badStuff() {
      try {
        await actions.copyTextToClipboard(textToCopyWithoutUserPermission);
        actions.addAlert({
          type: 'success',
          message: 'text copied to clipboard',
        });
      } catch (error) {
        console.log(error);
        actions.addAlert({
          type: 'warning',
          message: "can't copy value",
        });
      }
    }

    badStuff();
  }, []);
```

### Upload files

While there is no [UI component](/reference/ui-components/overview) for uploading files, there are a few ways you can upload files:

- [Create a custom file type property](https://knowledge.hubspot.com/properties/create-and-edit-properties), then use a [CRM property list component](/reference/ui-components/crm-data-components/crm-property-list) to display and manage the property from CRM records. You can upload up to 10 files per file property, and file uploaded via file properties have the same [size and type limitations](https://knowledge.hubspot.com/files/supported-file-types) as files uploaded to the file manager.
- Include an [iframe modal](#open-an-iframe-in-a-modal) in the extension that loads an upload page, then upload files through the iframe.

## Send custom log messages for debugging

Using `logger` methods, you can send custom log messages to HubSpot for more in-depth troubleshooting of deployed extensions. Custom log messages will appear in the [app's logs in HubSpot](/guides/crm/private-apps/creating-private-apps#logs), searchable by trace ID. Check out [HubSpot's custom logger sample project](/guides/crm/ui-extensions/sample-extensions/overview#custom-logger-example) to see an implementation example.

The following methods are available:

- `logger.info`
- `logger.debug`
- `logger.warn`
- `logger.error`

Each method accepts a single string argument.

For example, the following extension code includes few different log messages to help better identify where an error has occurred:

```js
import { logger } from '@hubspot/ui-extensions';

logger.debug('Information before my extension');

hubspot.extend(({ runServerlessFunction }) => {
  logger.warn('Logging a warning inside extension');

  useEffect(() => {
    runServerlessFunction('customers')
      .then((result) => {
        if (result.status === 'ERROR') {
          logger.error(`Customer fetch failed`);
          return;
        }

        logger.info(JSON.stringify(result));
      })
      .catch((error) => {
        logger.error(`Customer fetch failed: ${error.message}`);
      });
  }, []);

  return <Text>Hello</Text>;
});
```

When an extension fails to load on a CRM record, an error message will display. This error message will contain a trace ID, which you can copy.
Using that trace ID, you can then locate the custom log messages [within the private app's logs](/guides/crm/private-apps/creating-private-apps#log-traces).

### Notes and limitations

- Custom log messages are not sent while in local development mode. They are logged to the browser console instead.
- All logs are sent as batches with a maximum of 100 logs per batch.
- Each HubSpot account is rate limited to 1,000 logs per minute. After exceeding that limit, all logging is stopped until the page is reloaded.
- The logger will queue a maximum of 10,000 pending messages. Any subsequent logs will be dropped until the queue is below the maximum.
- Queued logs are processed at a rate of five seconds per log batch.
- Queued logs are dropped when the page or is refreshed or closed.