Best Practice for Fetching Custom Object Associations in UI Extensions (hubspot.fetch vs. Serverless

Hi HubSpot Developer Team,

I’m building a custom UI card for a custom object page and I need some clarification on the best practice for fetching associated data.

My goal is to display a table of line items. The association path is: Custom Object → Associated Deal → Associated Line Items.

My understanding is that direct API calls from a UI card are blocked by the Content Security Policy (CSP), and the required method for fetching data is to use a serverless function. When I try to call the API directly for my custom object, I get errors as expected.

However, I’m confused because I have another component on a standard Deal page that successfully fetches associated line items directly from the frontend using hubspot.fetch() with a relative API path. This approach works perfectly without a serverless function.

This has made it difficult to understand when a serverless function is truly necessary.

Here is the code that works on a standard Deal card, which is causing my confusion:

JavaScript

import React, { useEffect, useState } from 'react';
import { hubspot, Card, CrmAssociationTable } from '@hubspot/ui-extensions';

const DealsTimelineCard = ({ context }) => {
 const [loading, setLoading] = useState(false);
 const [lineItems, setLineItems] = useState([]);

 useEffect(() => {
 const loadProductsForDeal = async () => {
 try {
 setLoading(true);
 const dealId = context?.crm?.objectId;
 if (!dealId) {
 setLoading(false);
 return;
 }

 // This hubspot.fetch() call works directly from the frontend
 const assocRes = await hubspot.fetch(`/crm/v4/objects/deals/${dealId}/associations/line_items`);
 const assocJson = await assocRes.json();
 const ids = (assocJson?.results || []).map(r => r.toObjectId).filter(Boolean);

 if (!ids.length) {
 setLineItems([]);
 setLoading(false);
 return;
 }

 // This subsequent hubspot.fetch() also works
 const batchRes = await hubspot.fetch(`/crm/v3/objects/line_items/batch/read`, {
 method: 'POST',
 headers: { 'content-type': 'application/json' },
 body: JSON.stringify({
 properties: ['name', 'quantity', 'price', 'amount'],
 inputs: ids.map(id => ({ id }))
 })
 });
 const batchJson = await batchRes.json();
 setLineItems(batchJson?.results || []);

 } catch (e) {
 // Handle error
 } finally {
 setLoading(false);
 }
 };
 loadProductsForDeal();
 }, [context]);

 return (
 <Card>
 <CrmAssociationTable
 objectTypeId="0-7" // Line Item Object Type ID
 propertyColumns={['name', 'quantity', 'price']}
 searchable={true}
 pagination={true}
 />
 </Card>
 );
};

hubspot.extend(({ context }) => <DealsTimelineCard context={context} />);

## My Questions

Could you please clarify the proper method for my use case?

  1. Why does hubspot.fetch() work for fetching associations from a standard Deal object but not for a custom object?
  2. Is there a special, limited proxy for standard objects that allows this? If so, what are its exact capabilities and limitations?
  3. For fetching data related to a custom object, is using a serverless function the only correct and officially supported method?

Understanding the intended architecture here would be incredibly helpful. Thank you!

Hello,

I am not an employee of HubSpot, but one thing I would suggest looking at is a couple of things:

- Make sure that your scopes include crm.objects.custom.read and crm.objects.associations.read

- Make sure that you are calling objects by their object type (2-{id}). Deal calls used the proxy correctly; your custom object attempt might be using an absolute URL or used an incorrect object type identifier. Switch to hubspot.fetch(‘/crm/…’) with the right custom object type.

Here is some documentation on it: Fetching data for UI extensions - HubSpot docs

Let me know if this helps, or if you have any other questions!


:check_mark: Was I able to help answer your question? Help the community by marking it as a solution.

Brandon Woodruff
Senior Software Developer @ PearagonStill have questions? Reach out at brandon@pearagon.com