HubSpot REST API – Creating Custom Objects and Properties

Share This Post

As HubSpot partners, we’re always digging into the myriad features available on the platform. One of the most powerful features in HubSpot is the ability to extend Out-of-the-Box (OOTB) objects with custom properties and manage them with the HubSpot REST API. Perhaps even more powerful is the ability to create custom object types, each with their own custom properties, that can be tailored to specific business needs.

While you can add custom properties to OOTB objects using the HubSpot UI, we find it’s better to do this programmatically using the HubSpot REST API. Why?

  • We work in several HubSpot portals throughout a project lifecycle (e.g., dev, test, production) so manually managing properties between portals can be challenging.
  • We like to be able to script creation (and tear down) of properties and objects in case something goes sideways and we need to start over.
  • To make our scripts easier to maintain, we like to provide a custom name for properties. Specifying custom names is easier via the API than the HubSpot UI.
  • Scripts can be treated like code and managed in our code repositories so we know we’ll have version history, in case something goes wrong.

In addition to custom properties on HubSpot entities like contacts and companies, many of our projects utilize at least one custom object. Currently, it is not possible to create custom objects from the HubSpot UI. As such, we also use the REST API to manage these objects.

Whether you’re building a HubSpot app or implementing HubSpot for a specific client’s CRM needs, you’re likely going to utilize properties and custom objects. In this post, we’re going to provide some tips and tricks for creating and managing properties and custom objects through the HubSpot REST API.

A Few General Notes

Before we get into the nitty gritty, a few points that are worth mentioning:

  • All examples below use the HubSpot REST API. If you’re unfamiliar with it, look at HubSpot’s very extensive API documentation.
  • To authenticate against the HubSpot REST API, you’ll need an API key. You can get details on that here.
  • We’re using curl to make our API calls since it’s easy for us to script curl calls. Feel free to use any client you prefer (e.g. Postman).
  • HubSpot allows you to create “property groups” that can help visually separate properties in the UI. These are optional but highly recommended if you’re creating more than a few custom properties per entity type (e.g. Contacts, Companies, etc.).
  • We try our best to consistently use naming conventions – prefixing all properties, property groups and objects according to a company-wide standard. Why do we do this? Several reasons:
    • If we have a well-defined convention, we can leverage that when writing scripts (e.g. “delete all of our property groups that start with ‘bfg_property_group’”).
    • Using a convention will help us avoid potential clashes with 3rd-party HubSpot integrations.
    • Like-named properties are just easier to spot when using the API and/or the HubSpot UI.

Property Groups

HubSpot allows for the creation of named buckets called “property groups”. Groups are an easy way to organize your properties within the HubSpot UI.

Custom Object Property Group - Custom Objects and Properties in HubSpot via the REST API

Each HubSpot entity’s API endpoint has a “/groups” path for creating/editing/deleting property groups for that entity. Here are some examples:

				
					https://api.hubapi.com/crm/v3/properties/companies/groups?hapikey=YOUR_HUBSPOT_API_KEY

https://api.hubapi.com/crm/v3/properties/contacts/groups?hapikey=YOUR_HUBSPOT_API_KEY

				
			

Creating Property Groups

Property groups can be easily created via the HubSpot API. Here are a few pointers for doing this:

  • Like custom properties, property groups can have a name specified (these must be unique) using the “name” parameter in the payload sent to the API. Think of this as the property group’s unique ID.
  • The “label” parameter is the “friendly” name that a user would see in the UI.
  • The “displayOrder” parameter indicates where, in a list of other property groups for that entity, the property group would be displayed. If you use “-1”, HubSpot will (seemingly) display that group in an order of its choosing.
  • For more details on property groups, refer to the “properties” section of the HubSpot API documentation.

Here’s an example showing the creation of a new Contact property group called “Our Custom Contact Properties”:

				
					curl --location --request POST 'https://api.hubapi.com/crm/v3/properties/contacts/groups?hapikey=YOUR_HUBSPOT_API_KEY' --header 'Content-Type: application/json' --data-raw '{ "label" : "Our Custom Contact Properties", "name" : "bfg_property_group_custom_contact_props", "displayOrder" : "-1" }’;
				
			

Deleting Property Groups

Property groups can also be deleted (technically, HubSpot archives them and saves them for 90 days) via the API. Here’s an example showing the deletion of the group we created above:

				
					curl --location --request DELETE 'https://api.hubapi.com/crm/v3/properties/contacts/groups/bfg_property_group_custom_contact_props?hapikey=YOUR_HUBSPOT_API_KEY'
				
			

Note: You *must* delete or move (either using the API or HubSpot UI) any properties that have been assigned to this group before attempting to delete. If you don’t do this, the response you’ll get back from the API will be along the lines of:

				
					{
    "status": "error",
    "message": "There was a problem with the request.",
    "correlationId": "XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
}

				
			

Properties

Like property groups, properties can be created/updated/deleted using the API.

Here’s an example for retrieving the full set of properties for the contact entity:

				
					curl --location --request GET 'https://api.hubapi.com/crm/v3/properties/contacts?hapikey=YOUR_HUBSPOT_API_KEY'
				
			

Interesting note – there appears to be no way to get all properties for an entity for a particular property group, despite the group name being returned when querying all properties.

Creating Properties

Creating a custom property involves a POST to the appropriate entity endpoint (e.g. “/properties/contacts/“). Additionally, the following options can be passed as parameters in the payload:

  • name – This is the unique name/ID you’ll give the property (e.g. “bfg_contact_crm_favorite_food”).
  • groupName – The unique ID of the property group to which the property should be associated. This is required (e.g. “bfg_property_group_custom_contact_props”).
  • hasUniqueValue – Boolean that indicates whether the value for this property must be unique across records (imagine a unique ID that you’d want to store for each contact). Default value is “false”.
  • label– What HubSpot users will see when they’re viewing a contact record.
  • type – The data type of the property (e.g. string, number, date – for a complete list of valid types refer to HubSpot’s Developer Docs).
  • fieldType – The type of frontend control that will be used to represent this field in the HubSpot UI (e.g. textarea, dropdown, checkbox – for a complete list of valid field types refer to HubSpot’s Developer Docs).

Here’s an example showing the creation of a new Contact property called “Our Custom Contact Properties”:

				
					curl --location --request POST 'https://api.hubapi.com/crm/v3/properties/contacts?hapikey=YOUR_HUBSPOT_API_KEY' --header 'Content-Type: application/json' --data-raw '{    "groupName" : "bfg_property_group_custom_contact_props",    "displayOrder" : "-1",    "hasUniqueValue" : "false",    "hidden" : "false",    "name" : "bfg_contact_crm_favorite_food",    "type" : "string",    "fieldType" : "text", "label" : "Favorite Food",    "description" : "This contact'\''s favorite food in the entire world"}';
				
			

When creating properties, consider the following:

  • You can update an existing property using PATCH. In this scenario, the name of the property is provided in the URL path and the parameters you want to change in the payload of the API call. You do not need to specify all parameters, as you do in the POST.
  • You cannot change the name of a property once it’s been created.
  • For boolean properties, these need to be represented as enumerations with the fieldType of “booleancheckbox”. Additionally, an “options” array needs to be passed in the payload:
				
					curl --location --request POST 'https://api.hubapi.com/crm/v3/properties/contacts?hapikey=YOUR_HUBSPOT_API_KEY' --header 'Content-Type: application/json' --data-raw '{    "groupName" : "bfg_property_group_custom_contact_props",    "displayOrder" : "-1",    "hasUniqueValue" : "false",    "hidden" : "false",    "name" : "bfg_contact_crm_is_vegetarian",    "type" : "enumeration",    "fieldType" : "booleancheckbox",    "label" : "Is Vegetarian?",    "description" : "Is this contact a vegetarian?",    "options" : [ {    "value" : "TRUE",    "label" : "Yes",    "description" : "",    "displayOrder" : 0},{    "value" : "FALSE",    "label" : "No",    "description" : "",    "displayOrder" : 1}]       }’;
				
			
  • For a property that needs to be displayed using a single-select dropdown, the type will also be “enumeration” but the fieldType will be “select”.
  • If you try to create a property using an invalid property group name, the HubSpot API will return an error. It will not automatically create the property group!

Deleting Properties

You can delete a custom property by making a DELETE call to “/properties/<entity type>/<property name>”. For example:

				
					curl --location --request DELETE 'https://api.hubapi.com/crm/v3/properties/contact/bfg_contact_crm_favorite_food_2?hapikey=YOUR_HUBSPOT_API_KEY' \
--header 'Content-Type: application/json&rsquo;

				
			

There are a few things to know regarding deletion of properties:

  • When deleting, you’re really archiving the property and any record values that have been associated with that property. HubSpot will keep this property and values around for up to 90 days.
  • If you recreate the property using the original property definition within that 90-day period, HubSpot will effectively restore the property along with any previously assigned property values for records.
  • When you archive a property successfully via the API, HubSpot will simply return an HTTP status code of 204, with no additional indication that the property was archived.

Custom Objects

We’ve talked through custom properties on OOTB entities like contacts and companies. But what if you need to store additional data in HubSpot that doesn’t “fit” into one of these entities? Well, if you’re using HubSpot Sales Hub Enterprise, you’ll have the ability to create your own custom objects to suit your needs. Like OOTB entities, custom objects can have properties that you define. Additionally, custom objects can be associated with other object types (both OOTB types and custom objects).

Why would you want to use a custom object? As an example, we migrated one of our clients to HubSpot from an ERP system. Since they’re a B2B merchant, some of their buyers had multiple addresses on their contact records in their ERP. In HubSpot, a contact can only have one address (something we really hope HubSpot fixes in the future). When syncing a contact from their ERP into HubSpot, we wanted to retain the entire address book. So, we created a custom address object along with all the properties that you’d expect – street 1, street 2, city, zip, etc. We were then able to set up an association between this custom address object and the contact record (we also set up an association with the contact’s company record if it existed).

Since the HubSpot UI natively supports displaying custom objects, we didn’t need to do anything special once the custom object was defined. The properties we added to the object appeared on the entity sheets just like OOTB entities.

Creating Custom Objects with the HubSpot REST API

Creation of a custom object is only possible via the API. To do this, you’ll want to leverage the “/schemas” path (full documentation here).

When creating a new custom object, you’ll POST to the /schemas endpoint. The payload will need to include the following parameters:

  • name – This is the unique name you’ll give the custom object. (e.g. “bfg_custom_object_address”). Note: HubSpot will prefix the name with an internal ID comprised of your portal’s ID, which may cause confusion. More on that in a bit.
  • labels – This is an object with two elements:
    • singular – what label should be used in the HubSpot UI when referring to a single instance of this custom object (e.g. “Address”)
    • plural – what label should be used in the HubSpot UI when referring to multiple instances of this custom object (e.g. “Addresses”).
  • metaType – This is always set to “PORTAL_SPECIFIC” for custom objects created via the API.
  • primaryDisplayProperty – The property on the object that will be displayed on things like the contact information sheet in the HubSpot UI. The value for this should match the unique name of one of your properties. Note: only one value can be specified here.
  • secondaryDisplayProperties – An array of property names. These will be displayed under the primary display property on information sheets in the HubSpot UI. Note: you can indicate a maximum of two properties in this array (we think it would be nice if HubSpot allowed more).
  • requiredProperties – An array of property names that will be required when a new instance of the custom object is created in the HubSpot UI.
  • properties – An array of property definitions. The schema for these definitions matches the schema described above under “Properties” (<Link – anchor link to above>).
  • associatedObjects – An array of OOTB entity types to which a custom object instance can be associated. For example, [“CONTACT”, “COMPANY”] will ensure that the object instance is tied to the contact and/or company record when created. Note: the values specified in this array will dictate where the object sheet is shown in the HubSpot UI.

Here’s an example of creating a new custom address object:

				
					curl --location --request POST 'https://api.hubapi.com/crm/v3/schemas?hapikey=YOUR_HUBSPOT_API_KEY' --header 'Content-Type: application/json' --data-raw '{    "name" : "bfg_custom_object_address",    "labels" : { "singular" : "Address", "plural" : "Addresses" },    "metaType" : "PORTAL_SPECIFIC",    "primaryDisplayProperty" : "bfg_address_erp_street_1",    "secondaryDisplayProperties" : [ "bfg_address_erp_is_default" ],    "requiredProperties" : [ "bfg_address_erp_street_1","bfg_address_erp_city","bfg_address_erp_zip_postal_code","bfg_address_erp_country","bfg_address_erp_billing_or_shipping" ],    "properties" : [  {    "groupName" : "bfg_property_group_address",    "displayOrder" : "-1",    "hasUniqueValue" : "false",    "hidden" : "true",    "name" : "bfg_address_erp_name",    "type" : "string",    "fieldType" : "text",    "label" : "Address Name",    "description" : "Internal name for this address in the ERP",    "options" : [ ]        }, {    "groupName" : "bfg_property_group_address",    "displayOrder" : "-1",    "hasUniqueValue" : "false",    "hidden" : "false",    "name" : "bfg_address_erp_street_1",    "type" : "string",    "fieldType" : "text",    "label" : "Street/P.O. Box",    "description" : "Street address 1",    "options" : [ ]        }, {    "groupName" : "bfg_property_group_address",    "displayOrder" : "-1",    "hasUniqueValue" : "false",    "hidden" : "false",    "name" : "bfg_address_erp_street_2",    "type" : "string",    "fieldType" : "text",    "label" : "Suite/Apt./Unit",    "description" : "Street address 2",    "options" : [ ]        }, {    "groupName" : "bfg_property_group_address",    "displayOrder" : "-1",    "hasUniqueValue" : "false",    "hidden" : "false",    "name" : "bfg_address_erp_city",    "type" : "string",    "fieldType" : "text",    "label" : "City",    "description" : "City for this address",    "options" : [ ]        }, {    "groupName" : "bfg_property_group_address",    "displayOrder" : "-1",    "hasUniqueValue" : "false",    "hidden" : "false",    "name" : "bfg_address_erp_county",    "type" : "string",    "fieldType" : "text",    "label" : "County",    "description" : "County for this address",    "options" : [ ]        }, {    "groupName" : "bfg_property_group_address",    "displayOrder" : "-1",    "hasUniqueValue" : "false",    "hidden" : "false",    "name" : "bfg_address_erp_zip_postal_code",    "type" : "string",    "fieldType" : "text",    "label" : "Postal Code",    "description" : "Zip/Postal code this for address",    "options" : [ ]        }, {    "groupName" : "bfg_property_group_address",    "displayOrder" : "-1",    "hasUniqueValue" : "false",    "hidden" : "false",    "name" : "bfg_address_erp_country",    "type" : "string",    "fieldType" : "text",    "label" : "Country",    "description" : "Country for this address",    "options" : [ ]        }, {    "groupName" : "bfg_property_group_address",    "displayOrder" : "-1",    "hasUniqueValue" : "false",    "hidden" : "false",    "name" : "bfg_address_erp_telephone",    "type" : "string",    "fieldType" : "text",    "label" : "Telephone",    "description" : "Telephone number associated with this address",    "options" : [ ]        }, {    "groupName" : "bfg_property_group_address",    "displayOrder" : "-1",    "hasUniqueValue" : "false",    "hidden" : "false",    "name" : "bfg_address_erp_is_default",    "type" : "enumeration",    "fieldType" : "booleancheckbox",    "label" : "Is Default?",    "description" : "Indicates whether or not this address is considered a default",    "options" : [ {    "value" : "TRUE",    "label" : "Yes",    "description" : "",    "displayOrder" : 0},{    "value" : "FALSE",    "label" : "No",    "description" : "",    "displayOrder" : 1}]        }, {    "groupName" : "bfg_property_group_address",    "displayOrder" : "-1",    "hasUniqueValue" : "false",    "hidden" : "false",    "name" : "bfg_address_erp_billing_or_shipping",    "type" : "enumeration",    "fieldType" : "booleancheckbox",    "label" : "Billing or Shipping?",    "description" : "Indicates whether or not this is a billing or shipping address",    "options" : [ {    "value" : "1",    "label" : "Billing",    "description" : "Address used when processing a payment on a sales order",    "displayOrder" : 0},{    "value" : "2",    "label" : "Shipping",    "description" : "Address used when shipping a sales order",    "displayOrder" : 1}]        }, {    "groupName" : "bfg_property_group_address",    "displayOrder" : "-1",    "hasUniqueValue" : "false",    "hidden" : "false",    "name" : "bfg_address_erp_is_dropship",    "type" : "enumeration",    "fieldType" : "booleancheckbox",    "label" : "Is Dropship?",    "description" : "Indicates whether or not this is a dropship.",    "options" : [ {    "value" : "TRUE",    "label" : "Yes",    "description" : "",    "displayOrder" : 0},{    "value" : "FALSE",    "label" : "No",    "description" : "",    "displayOrder" : 1}]        }, {    "groupName" : "bfg_property_group_address",    "displayOrder" : "-1",    "hasUniqueValue" : "false",    "hidden" : "false",    "name" : "bfg_address_erp_tax_code",    "type" : "enumeration",    "fieldType" : "select",    "label" : "Tax Code",    "description" : "Tax Code associated with this address",    "options" : [ {    "value" : "1",    "label" : "Exempt",    "description" : "VatStatus = N",    "displayOrder" : 1},{    "value" : "2",    "label" : "Tax Liable",    "description" : "VatStatus = Y",    "displayOrder" : 2},{    "value" : "3",    "label" : "Something Else",    "description" : "",    "displayOrder" : 4}]        }, {    "groupName" : "bfg_property_group_address",    "displayOrder" : "-1",    "hasUniqueValue" : "false",    "hidden" : "false",    "name" : "bfg_address_erp_care_of",    "type" : "string",    "fieldType" : "text",    "label" : "C/O",    "description" : "",    "options" : [ ]        }, {    "groupName" : "bfg_property_group_address",    "displayOrder" : "-1",    "hasUniqueValue" : "false",    "hidden" : "true",    "name" : "bfg_address_erp_web_customer_address_id",    "type" : "number",    "fieldType" : "number",    "label" : "Web Customer Address ID",    "description" : "Internal ID for this address. Note: this property should not be changed in HubSpot",    "options" : [ ]        } ],        "associatedObjects" : [ "CONTACT","COMPANY"]     }';
				
			

Here’s the formatted JSON from the payload above:

				
					{
    "name": "bfg_custom_object_address",
    "labels":
    {
        "singular": "Address",
        "plural": "Addresses"
    },
    "metaType": "PORTAL_SPECIFIC",
    "primaryDisplayProperty": "bfg_address_erp_street_1",
    "secondaryDisplayProperties": ["bfg_address_erp_is_default"],
    "requiredProperties": ["bfg_address_erp_street_1", "bfg_address_erp_city", "bfg_address_erp_zip_postal_code", "bfg_address_erp_country", "bfg_address_erp_billing_or_shipping"],
    "properties": [
    {
        "groupName": "bfg_property_group_address",
        "displayOrder": "-1",
        "hasUniqueValue": "false",
        "hidden": "true",
        "name": "bfg_address_erp_name",
        "type": "string",
        "fieldType": "text",
        "label": "Address Name",
        "description": "Internal name for this address in the ERP",
        "options": []
    },
    {
        "groupName": "bfg_property_group_address",
        "displayOrder": "-1",
        "hasUniqueValue": "false",
        "hidden": "false",
        "name": "bfg_address_erp_street_1",
        "type": "string",
        "fieldType": "text",
        "label": "Street/P.O. Box",
        "description": "Street address 1",
        "options": []
    },
    {
        "groupName": "bfg_property_group_address",
        "displayOrder": "-1",
        "hasUniqueValue": "false",
        "hidden": "false",
        "name": "bfg_address_erp_street_2",
        "type": "string",
        "fieldType": "text",
        "label": "Suite/Apt./Unit",
        "description": "Street address 2",
        "options": []
    },
    {
        "groupName": "bfg_property_group_address",
        "displayOrder": "-1",
        "hasUniqueValue": "false",
        "hidden": "false",
        "name": "bfg_address_erp_city",
        "type": "string",
        "fieldType": "text",
        "label": "City",
        "description": "City for this address",
        "options": []
    },
    {
        "groupName": "bfg_property_group_address",
        "displayOrder": "-1",
        "hasUniqueValue": "false",
        "hidden": "false",
        "name": "bfg_address_erp_county",
        "type": "string",
        "fieldType": "text",
        "label": "County",
        "description": "County for this address",
        "options": []
    },
    {
        "groupName": "bfg_property_group_address",
        "displayOrder": "-1",
        "hasUniqueValue": "false",
        "hidden": "false",
        "name": "bfg_address_erp_zip_postal_code",
        "type": "string",
        "fieldType": "text",
        "label": "Postal Code",
        "description": "Zip/Postal code this for address",
        "options": []
    },
    {
        "groupName": "bfg_property_group_address",
        "displayOrder": "-1",
        "hasUniqueValue": "false",
        "hidden": "false",
        "name": "bfg_address_erp_country",
        "type": "string",
        "fieldType": "text",
        "label": "Country",
        "description": "Country for this address",
        "options": []
    },
    {
        "groupName": "bfg_property_group_address",
        "displayOrder": "-1",
        "hasUniqueValue": "false",
        "hidden": "false",
        "name": "bfg_address_erp_telephone",
        "type": "string",
        "fieldType": "text",
        "label": "Telephone",
        "description": "Telephone number associated with this address",
        "options": []
    },
    {
        "groupName": "bfg_property_group_address",
        "displayOrder": "-1",
        "hasUniqueValue": "false",
        "hidden": "false",
        "name": "bfg_address_erp_is_default",
        "type": "enumeration",
        "fieldType": "booleancheckbox",
        "label": "Is Default?",
        "description": "Indicates whether or not this address is considered a default",
        "options": [
        {
            "value": "TRUE",
            "label": "Yes",
            "description": "",
            "displayOrder": 0
        },
        {
            "value": "FALSE",
            "label": "No",
            "description": "",
            "displayOrder": 1
        }]
    },
    {
        "groupName": "bfg_property_group_address",
        "displayOrder": "-1",
        "hasUniqueValue": "false",
        "hidden": "false",
        "name": "bfg_address_erp_billing_or_shipping",
        "type": "enumeration",
        "fieldType": "booleancheckbox",
        "label": "Billing or Shipping?",
        "description": "Indicates whether or not this is a billing or shipping address",
        "options": [
        {
            "value": "1",
            "label": "Billing",
            "description": "Address used when processing a payment on a sales order",
            "displayOrder": 0
        },
        {
            "value": "2",
            "label": "Shipping",
            "description": "Address used when shipping a sales order",
            "displayOrder": 1
        }]
    },
    {
        "groupName": "bfg_property_group_address",
        "displayOrder": "-1",
        "hasUniqueValue": "false",
        "hidden": "false",
        "name": "bfg_address_erp_is_dropship",
        "type": "enumeration",
        "fieldType": "booleancheckbox",
        "label": "Is Dropship?",
        "description": "Indicates whether or not this is a dropship.",
        "options": [
        {
            "value": "TRUE",
            "label": "Yes",
            "description": "",
            "displayOrder": 0
        },
        {
            "value": "FALSE",
            "label": "No",
            "description": "",
            "displayOrder": 1
        }]
    },
    {
        "groupName": "bfg_property_group_address",
        "displayOrder": "-1",
        "hasUniqueValue": "false",
        "hidden": "false",
        "name": "bfg_address_erp_tax_code",
        "type": "enumeration",
        "fieldType": "select",
        "label": "Tax Code",
        "description": "Tax Code associated with this address",
        "options": [
        {
            "value": "1",
            "label": "Exempt",
            "description": "VatStatus = N",
            "displayOrder": 1
        },
        {
            "value": "2",
            "label": "Tax Liable",
            "description": "VatStatus = Y",
            "displayOrder": 2
        },
        {
            "value": "3",
            "label": "Something Else",
            "description": "",
            "displayOrder": 4
        }]
    },
    {
        "groupName": "bfg_property_group_address",
        "displayOrder": "-1",
        "hasUniqueValue": "false",
        "hidden": "false",
        "name": "bfg_address_erp_care_of",
        "type": "string",
        "fieldType": "text",
        "label": "C/O",
        "description": "",
        "options": []
    },
    {
        "groupName": "bfg_property_group_address",
        "displayOrder": "-1",
        "hasUniqueValue": "false",
        "hidden": "true",
        "name": "bfg_address_erp_web_customer_address_id",
        "type": "number",
        "fieldType": "number",
        "label": "Web Customer Address ID",
        "description": "Internal ID for this address. Note: this property should not be changed in HubSpot",
        "options": []
    }],
    "associatedObjects": ["CONTACT", "COMPANY"]
}

				
			

Once the custom object is created, it will appear in the HubSpot UI under “Settings”->”Custom Objects”:

Custom Objects in HubSpot Admin - Custom Objects and Properties in HubSpot via the REST API

Properties for the custom object will appear under the “Settings”->”Properties” section, just like OOTB entity properties:

Custom Properties in HubSpot Admin - Custom Objects and Properties in HubSpot via the REST API

Finally, if you’ve associated the custom object with a HubSpot entity, the UI will show that object’s section on the right-hand nav when viewing an entity record:

Custom Object Sheet in HubSpot UI - Custom Objects and Properties in HubSpot via the REST API

Adding a new custom object record will show the user the property fields defined in the object definition:

Create Custom Object Instance Example - Custom Objects and Properties in HubSpot via the REST API

What if you’d like to add more properties to an existing custom object? HubSpot provides a PATCH endpoint on “/schemas” but, believe it or not, that’s not how you’d add new properties to an existing object. (Note: you can actually call this endpoint with a single item in the “properties” array that would represent a new property and HubSpot will happily respond to the request. However, the property will not be added).

Instead, you can use the same mechanism mentioned earlier for adding properties. The difference? Instead of using something like “/contacts” or “/companies”, you’ll use the unique ID for the custom object. Here’s an example where we add a “Street 3” property to our custom address object – note the custom object’s unique ID in the path:

				
					curl --location --request POST 'https://api.hubapi.com/crm/v3/properties/p20606099_bfg_custom_object_address?hapikey=YOUR_HUBSPOT_API_KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
            "groupName": "bfg_property_group_address",
            "displayOrder": "-1",
            "hasUniqueValue": "false",
            "hidden": "false",
            "name": "bfg_address_erp_street_3",
            "type": "string",
            "fieldType": "text",
            "label": "Street Address 3",
            "description": "The 3rd street line. Needed in some cases",
            "options": []
        }'

				
			
Custom Property Street 3 - Custom Objects and Properties in HubSpot via the REST API

Deleting Custom Objects with the HubSpot REST API

Like properties, deletion of custom objects is an archive operation. HubSpot will hang on to the object and any instances of that object for some period.

Deleting custom object definitions via the API isn’t particularly straightforward. We’ve found that you need to follow these steps to truly delete (not just archive) an object:

  • Delete all existing instances of the custom object. For example, if we added an address instance to a contact, we’d want to remove that instance before we can remove the custom object definition.
  • Perform a “Soft” delete on the custom object. To do this, you’ll make a DELETE call to the “/schemas” endpoint with your custom object’s unique ID (note: this is NOT the same as the unique name. Remember, HubSpot prefixes your custom object’s name with its own internal ID). Here’s an example of what this looks like:
				
					curl --location --request DELETE 'https://api.hubapi.com/crm/v3/schemas/p20606099_bfg_custom_object_address?hapikey=YOUR_HUBSPOT_API_KEY' 
				
			
  • Once the object has been soft deleted, you’ll want to archive it. You can make the same call to the same endpoint as above. However, you’ll need to pass a special “archived=true” query string parameter/value to the call:
				
					curl --location --request DELETE 'https://api.hubapi.com/crm/v3/schemas/p20606099_bfg_custom_object_address?hapikey=YOUR_HUBSPOT_API_KEY&amp;archived=true' 
				
			
  • Finally, if you ever want to be able to create this custom object again using the same unique name that you originally used (e.g. during development as you’re refining your object definition), you’ll need to purge the schema from HubSpot:
				
					curl --location --request DELETE 'https://api.hubapi.com/crm/v3/schemas/p20606099_bfg_custom_object_address/purge?hapikey=YOUR_HUBSPOT_API_KEY' 
				
			

More About Custom Objects

When we first started working with custom objects, it took us a few attempts to really understand how HubSpot is managing things behind-the-scenes. Here are some things we learned along the way:

  • As mentioned above, HubSpot will assign a prefix to your custom object name. This prefix is in the format of “p_”. If you’re trying to update a custom object and it doesn’t seem to be working, make sure you’re using this unique ID *not* your custom object name (we think it would be nice if HubSpot allowed this but assume there’s a good technical reason they do not).
  • During development, we had some issues really purging a custom object definition. This can be challenging as you’re constantly tweaking the definition as you discover additional properties, configurations, etc. you want on the custom object. We’d suggest starting off with a custom object name that can easily be adjusted as you build out your object definition – “bfg_custom_object_address_iteration_1”, “bfg_custom_object_address_iteration_2”, etc. This way, you can just keep creating new object definitions until you get it right, without worrying about deleting existing definitions first. When you’re really comfortable that you’ve nailed down the definition, use the “final” name for the object when you create it for the last time – e.g. “bfg_custom_object_address”.
  • In the property definitions for custom objects that you pass via the HubSpot REST API, one of the elements is “hidden”. If the value for this is true, the HubSpot UI should hide that property from the user (imagine this property is a unique ID from another system – it’s not something you’d want a user to edit). While the UI doesn’t show the property, it will show a yellow box on the custom object “Add” screen that says, “Invalid property: ”. Users can still save the custom object without a value for this field so this is just a minor annoyance. We haven’t found a way to prevent this message from appearing for hidden fields.
Create Custom Object Instance Common Issue - Custom Objects and Properties in HubSpot via the REST API

Wrapping Up

HubSpot’s extensibility really shines when it comes to object and properties. When OOTB objects and properties aren’t meeting your needs, you can easily model data as you see fit (you can even use custom objects in HubSpot custom code).

The HubSpot REST API helps to make all this possible. Through scripting with tools like curl, your team can create/update/tear down properties and objects programmatically such that the process can be done in seconds and repeated in any given HubSpot portal.

Whether you’re just getting started with HubSpot and its API or you’re a seasoned developer, we’d love to get your input on your experiences working with properties and objects in the HubSpot platform. Feel free to contact us!

More To Explore

It’s a Matter of Trust

The combination of pandemic and moving to a rural community has increased the amount of shopping I do online, but even before those events I found myself depending more and

Thoughts from SAMA

I was recently able to attend the San Antonio Manufacturers Association Trade Show & Conference (SAMA). Like all live events I have had the pleasure to attend this year, the