🟢 HubSpot Developer Practical Textbook — 2026 Edition Developer Edition
Chapter 4  ·  Custom Objects & Schema Design

custom objects &
schema design

Define business data in HubSpot that cannot be represented using standard objects as custom objects. Learn practical concepts of object design, property type selection, association design, and Enterprise-specific functions.

Enterprise plan required
Schemas API v3
Time required: Approximately 90 minutes
4-1 What is a custom object?

Understand the limitations of standard objects and identify issues that can be solved with custom objects.

📦 Standard objects

Fixed objects built into HubSpot.

Contacts / Companies / Deals / Tickets / Products / Line Items / Quotes etc. Although suitable for most businesses, industry-specific data structures have limitations.

✨ Custom objects

An object that can be freely defined by the developer.

"Properties", "Vehicles", "Subscriptions", "Events", "Qualifications", etc. You can add unique data structures to HubSpot CRM that suit your industry and business. Enterprise Required

💡 Typical examples where custom objects are needed

Real estate industry:“Property” object → Manage by linking with Contacts (owner/tenant)
Car sales:“Vehicle” object → Association with Deals (business negotiations) and Contacts (owner)
SaaS:“Subscription” object → Lifecycle management by linking with Contacts/Deals
education:"Course" "Attendance history" object → Track learning progress of Contacts
Manufacturing:“Equipment” and “Maintenance record” objects → Support management in conjunction with Tickets

Limitations: custom object is Enterprise planAvailable only in Maximum per portal 10 piecesYou can create custom objects for (may vary by plan). Each object has a maximum 1,000 piecesproperties can be defined.
4-2 Concept of schema design

Design object structures, properties, and associations before implementation.

🏗 Design steps

① Identify the entity you want to manage → ② Check if it can be replaced with a standard object → ③ Define the name and properties of the custom object → ④ Design the association with the standard object → ⑤ Implementation with API

Design example: SaaS product schema

standard
contacts
──────
custom
subscriptions
──────
custom
usage_logs
standard
companies
──────
custom
subscriptions
──────
standard
deals

Subscriptions (custom) is the central object that bridges contacts, companies, and deals

Design criteriaUse standard objectsUse custom objects
Nature of data Close to people, companies, business negotiations, and support Industry-specific entities (properties, vehicles, contracts, etc.)
number of properties Existing properties are sufficient Requires 10 or more dedicated properties
reporting requirements Available with standard reports Requires custom object-specific aggregation
Workflow collaboration Can be handled with standard workflow I want to use a custom object as a trigger
4-3 Create a custom object with Schemas API

Define and create schemas for custom objects using the API.

Node.js — Create custom object "Subscription"
import { Client } from '@hubspot/api-client'; const client = new Client({ accessToken: process.env.HUBSPOT_ACCESS_TOKEN }); const schema = await client.crm.schemas.coreApi.create({ name: 'subscriptions', //API name (lowercase letters/underscore) labels: { singular: 'Subscription', // singular label plural: 'Subscriptions', // plural label }, primaryDisplayProperty: 'plan_name', // Properties to display in the record list secondaryDisplayProperties: ['status', 'mrr'], properties: [ { name: 'plan_name', label: 'Plan name', type: 'string', fieldType: 'text', groupName: 'subscriptioninformation', }, { name: 'status', label: 'status', type: 'enumeration', fieldType: 'select', groupName: 'subscriptioninformation', options: [ { label: 'active', value: 'active', displayOrder: 1, hidden: false }, { label: 'Trial', value: 'trial', displayOrder: 2, hidden: false }, { label: 'Cancelled', value: 'cancelled', displayOrder: 3, hidden: false }, { label: 'pause', value: 'paused', displayOrder: 4, hidden: false }, ], }, { name: 'mrr', label: 'MRR (Monthly Revenue)', type: 'number', fieldType: 'number', groupName: 'subscriptioninformation', }, { name: 'start_date', label: 'Contract start date', type: 'date', fieldType: 'date', groupName: 'subscriptioninformation', }, { name: 'renewal_date', label: 'Updated date', type: 'date', fieldType: 'date', groupName: 'subscriptioninformation', }, { name: 'external_id', label: 'External system ID', type: 'string', fieldType: 'text', groupName: 'subscriptioninformation', hasUniqueValue: true, // unique constraint }, ], associatedObjects: ['CONTACT', 'COMPANY', 'DEAL'], }); console.log(`Schema created: ${schema.objectTypeId}`); // objectTypeId will be used later in API calls (e.g. "2-12345678")
About objectTypeId: After creating the custom object,objectTypeId(example:2-12345678) is returned. Use this ID in subsequent API calls. /crm/v3/objects/2-12345678 Access it by including it in the path like this.
4-4 Property type details and selection criteria

Organize property type/fieldType combinations and use cases for each type.

typefieldTypestored valueuse case
string text text Name, memo, URL, code, etc.
string textarea long text Explanation/Remarks/Memo column
number number numerical value Amount/Score/Quantity/Coordinates
enumeration select one choice Status/Rank/Category
enumeration radio one choice Yes/No/Priority (emphasis on visibility)
enumeration checkbox Semicolon separated multiple values Tags/Function flags/Multiple selection
date date YYYY-MM-DD Contract date, deadline, birthday
datetime date Unix milliseconds Timestamp/Last login
bool booleancheckbox true / false Flag/Valid/Invalid/Consent
phone_number phonenumber phone number string Telephone number (compatible with international format)
⚠ Handling of enumeration (checkbox) values: The checkbox type separates multiple selected values ​​with a semicolon (;) delimited string. example:"feature_a;feature_b;feature_c" If you want to filter by Search API CONTAINS_TOKEN Use operators.
Node.js — Add properties later
// Add property to existing custom object await client.crm.properties.coreApi.create( '2-12345678', // objectTypeId { name: 'seat_count', label: 'Number of seats', type: 'number', fieldType: 'number', groupName: 'subscriptioninformation', description: 'Number of contracted users (number of seats)', } ); // Create property group (group related properties) await client.crm.propertyGroups.coreApi.create( '2-12345678', { name: 'billing_info', displayOrder: 2, label: 'Billing information', } );
4-5 Association design

Define relationships between custom objects and standard/other custom objects.

🔗 Key points for association design

Associations between custom objects When defining a schema using the Schemas APIor set it to later Associations Schema API You can add it with . Both "one-to-many" and "many-to-many" relationships can be expressed. association labelBy using , you can differentiate the type of relationship between the same objects, such as ``primary person'' and ``secondary person person.''

Node.js — Creating custom association labels
// Add your own label between custom object → Contacts // Example: Distinguish between the "main person in charge" and "billing person" of Subscription const assocDef = await client.crm.schemas.associationsApi.create( 'subscriptions', // fromObjectType 'contacts', // toObjectType { label: 'Main contact', name: 'primary_contact', category: 'USER_DEFINED', } ); console.log(`Association type ID: ${assocDef.typeId}`); // Create the opposite direction as well (Contact → Subscription) await client.crm.schemas.associationsApi.create( 'contacts', 'subscriptions', { label: 'Responsible subscription', name: 'managed_subscription', category: 'USER_DEFINED', } );
Node.js — Association using custom association labels
// Associate subscriptions and contacts with custom labels await client.crm.associations.v4.basicApi.create( 'subscriptions', subscriptionId, 'contacts', contactId, [ { associationCategory: 'USER_DEFINED', associationTypeId: assocDef.typeId, // typeId created above }, ] ); // Get related contacts with labels const related = await client.crm.associations.v4.basicApi.getPage( 'subscriptions', subscriptionId, 'contacts' ); // You can check the label with result.associationTypes
4-6 CRUD operations for custom objects

Work with custom object records using the same API patterns as standard objects.

Node.js — Create, retrieve, update, and delete records
const OBJECT_TYPE = '2-12345678'; // or 'subscriptions' (name is also ok) // ── Create ────────────────────────────── const record = await client.crm.objects.basicApi.create(OBJECT_TYPE, { properties: { plan_name: 'Pro plan', status: 'active', mrr: '49800', start_date: '2026-04-01', renewal_date: '2027-03-31', external_id: 'sub_001', }, associations: [ { to: { id: contactId }, types: [{ associationCategory: 'USER_DEFINED', associationTypeId: primaryTypeId }], }, ], }); // ── Get ────────────────────────────── const fetched = await client.crm.objects.basicApi.getById( OBJECT_TYPE, record.id, ['plan_name', 'status', 'mrr', 'renewal_date'] ); // ── Update ────────────────────────────── await client.crm.objects.basicApi.update(OBJECT_TYPE, record.id, { properties: { status: 'cancelled', mrr: '0', }, }); // ── Filter by Search API ──────────── const activeSubscriptions = await client.crm.objects.searchApi.doSearch(OBJECT_TYPE, { filterGroups: [{ filters: [{ propertyName: 'status', operator: 'EQ', value: 'active', }], }], properties: ['plan_name', 'mrr', 'renewal_date'], sorts: [{ propertyName: 'renewal_date', direction: 'ASCENDING' }], limit: 100, });
Node.js — Process custom objects in bulk with Batch API
// Sync all subscriptions from external DB (upsert pattern) async function upsertSubscriptions(externalData) { const CHUNK = 100; for (let i = 0; i < externalData.length; i += CHUNK) { const chunk = externalData.slice(i, i + CHUNK); await client.crm.objects.batchApi.upsert(OBJECT_TYPE, { inputs: chunk.map(item => ({ idProperty: 'external_id', // upsert with unique property properties: { external_id: item.id, plan_name: item.planName, status: item.status, mrr: String(item.mrr), renewal_date: item.renewalDate, }, })), }); await new Promise(r => setTimeout(r, 200)); } }
Upsert API: batchApi.upsert() specifiedunique propertyidProperty) updates if there is a record that matches the value, If not, create a new one. Very useful for periodic synchronization with external systems. hasUniqueValue: true properties only idProperty can be specified.
4-7 Obtain/update/delete schema

Manage the schema of custom objects you have created.

Node.js — Schema management operations
// Get all custom object schemas in the portal const allSchemas = await client.crm.schemas.coreApi.getAll(); console.log(allSchemas.results.map(s => ({ name: s.name, objectTypeId: s.objectTypeId, properties: s.properties.length, }))); // Get details of a specific schema const schema = await client.crm.schemas.coreApi.getById('subscriptions'); // Update schema labels await client.crm.schemas.coreApi.update('subscriptions', { labels: { singular: 'Subscription', plural: 'Subscription list', }, primaryDisplayProperty: 'plan_name', }); // Delete schema (can be deleted only if there are 0 records) await client.crm.schemas.coreApi.archive('subscriptions');
⚠ Note on schema deletion: To delete a custom object's schema, delete that object's schema.Delete all records firstMust be. Additionally, deleted schemas cannot be restored. Always test in a sandbox before deleting in production.
4-8 Practical example: Periodic synchronization architecture with external DB

A design pattern that syncs subscription data from external services like Stripe to HubSpot custom objects.

🏛 Architecture Overview

① Stripe Webhook → Receive subscription change event
② Node.js server(or Serverless Function) → Handle events
③ HubSpot Upsert API → Custom object subscriptions update
④ Associations API → Link with Contacts/Deals
⑤ HubSpot Workflow → Notification/task creation triggered by status change

Node.js — Stripe → HubSpot sync handler
import Stripe from 'stripe'; import { Client } from '@hubspot/api-client'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); const hubspot = new Client({ accessToken: process.env.HUBSPOT_ACCESS_TOKEN }); async function handleSubscriptionUpdated(stripeSubscription) { const { id: externalId, status, items, current_period_end, customer: customerId, } = stripeSubscription; const mrr = items.data.reduce((sum, item) => sum + (item.price.unit_amount * item.quantity / 100), 0 ); // Upsert HubSpot custom object const result = await hubspot.crm.objects.batchApi.upsert('subscriptions', { inputs: [{ idProperty: 'external_id', properties: { external_id: externalId, status: status === 'active' ? 'active' : status === 'trialing' ? 'trial' : 'cancelled', mrr: String(mrr), renewal_date: new Date(current_period_end * 1000).toISOString().split('T')[0], }, }], }); // Find and associate HubSpot contacts by Stripe CustomerID const contacts = await hubspot.crm.contacts.searchApi.doSearch({ filterGroups: [{ filters: [{ propertyName: 'stripe_customer_id', operator: 'EQ', value: customerId }], }], properties: ['email'], limit: 1, }); if (contacts.results.length > 0) { const subscriptionId = result.results[0].id; const contactId = contacts.results[0].id; await hubspot.crm.associations.v4.basicApi.create( 'subscriptions', subscriptionId, 'contacts', contactId, [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 1 }] ); } }
4-9 Summary of this chapter

Please review before proceeding to the next chapter (CMS Hub Development—Themes & Templates).

✅ Chapter 4 Checklist

  • Determine when a custom object is required (where standard objects are insufficient)
  • Understood the 5 steps of schema design (identification → confirmation → definition → association → implementation)
  • You can create custom objects with the Schemas API
  • I understood how to use objectTypeId (2-XXXXXXXXformat)
  • You can select a combination of property type (string/number/enumeration/date/bool) and fieldType
  • You can set unique constraints using hasUniqueValue
  • Create and apply custom association labels
  • CRUD/Search/Batch operations can be performed on custom objects
  • You can implement differential synchronization with external systems using the Upsert API.
  • Sync HubSpot custom objects with external services like Stripe via webhooks
About the next chapter (Chapter 5): The development side of CMS Hub—Learn about themes and templates. HubSpot theme structure/HubL template language practice/ We will systematically explain CMS development for front-end developers, including global partial responsive design.