# HubSpot Client

Manage contacts, deals, companies, and interact with HubSpot's CRM platform.

## Methods

| Method                                   | Description                                         |
| ---------------------------------------- | --------------------------------------------------- |
| `apiRequest(options, schema, metadata?)` | Make any HubSpot API request with schema validation |

## Usage

### Create a Contact

```typescript
import { api, z, hubspot } from "@superblocksteam/sdk-api";

// Integration ID from the integrations panel
const PROD_HUBSPOT = "a1b2c3d4-5678-90ab-cdef-hubspot00001";

const ContactSchema = z.object({
  id: z.string(),
  properties: z
    .object({
      email: z.string().optional(),
      firstname: z.string().optional(),
      lastname: z.string().optional(),
      phone: z.string().optional(),
      company: z.string().optional(),
    })
    .passthrough(),
  createdAt: z.string(),
  updatedAt: z.string(),
});

export default api({
  integrations: {
    hubspot: hubspot(PROD_HUBSPOT),
  },
  name: "HubSpotExample",
  input: z.object({
    email: z.string().email(),
    firstName: z.string(),
    lastName: z.string(),
  }),
  output: z.object({
    contactId: z.string(),
  }),
  async run(ctx, { email, firstName, lastName }) {
    const result = await ctx.integrations.hubspot.apiRequest(
      {
        method: "POST",
        path: "/crm/v3/objects/contacts",
        body: {
          properties: {
            email: email,
            firstname: firstName,
            lastname: lastName,
          },
        },
      },
      { response: ContactSchema },
    );

    return { contactId: result.id };
  },
});
```

### Get a Contact

```typescript
const contact = await ctx.integrations.hubspot.apiRequest(
  {
    method: "GET",
    path: `/crm/v3/objects/contacts/${contactId}`,
    params: {
      properties: "email,firstname,lastname,phone,company,lifecyclestage",
    },
  },
  { response: ContactSchema },
);
```

### Search Contacts

```typescript
const SearchResponseSchema = z.object({
  total: z.number(),
  results: z.array(ContactSchema),
  paging: z
    .object({
      next: z
        .object({
          after: z.string(),
        })
        .optional(),
    })
    .optional(),
});

const result = await ctx.integrations.hubspot.apiRequest(
  {
    method: "POST",
    path: "/crm/v3/objects/contacts/search",
    body: {
      filterGroups: [
        {
          filters: [
            {
              propertyName: "email",
              operator: "CONTAINS_TOKEN",
              value: "@example.com",
            },
          ],
        },
      ],
      sorts: [{ propertyName: "createdate", direction: "DESCENDING" }],
      properties: ["email", "firstname", "lastname"],
      limit: 25,
    },
  },
  { response: SearchResponseSchema },
);
```

### Create a Deal

```typescript
const DealSchema = z.object({
  id: z.string(),
  properties: z
    .object({
      dealname: z.string().optional(),
      amount: z.string().optional(),
      dealstage: z.string().optional(),
      closedate: z.string().optional(),
      pipeline: z.string().optional(),
    })
    .passthrough(),
  createdAt: z.string(),
  updatedAt: z.string(),
});

const deal = await ctx.integrations.hubspot.apiRequest(
  {
    method: "POST",
    path: "/crm/v3/objects/deals",
    body: {
      properties: {
        dealname: "Enterprise Contract",
        amount: "50000",
        dealstage: "qualifiedtobuy",
        closedate: "2024-12-31",
        pipeline: "default",
      },
    },
  },
  { response: DealSchema },
);
```

### List Deals

```typescript
const ListDealsResponseSchema = z.object({
  results: z.array(DealSchema),
  paging: z
    .object({
      next: z.object({ after: z.string() }).optional(),
    })
    .optional(),
});

const deals = await ctx.integrations.hubspot.apiRequest(
  {
    method: "GET",
    path: "/crm/v3/objects/deals",
    params: {
      limit: 50,
      properties: "dealname,amount,dealstage,closedate",
    },
  },
  { response: ListDealsResponseSchema },
);
```

### Create a Company

```typescript
const CompanySchema = z.object({
  id: z.string(),
  properties: z
    .object({
      name: z.string().optional(),
      domain: z.string().optional(),
      industry: z.string().optional(),
      phone: z.string().optional(),
    })
    .passthrough(),
  createdAt: z.string(),
  updatedAt: z.string(),
});

const company = await ctx.integrations.hubspot.apiRequest(
  {
    method: "POST",
    path: "/crm/v3/objects/companies",
    body: {
      properties: {
        name: "Acme Corp",
        domain: "acme.com",
        industry: "Technology",
        phone: "+1-555-123-4567",
      },
    },
  },
  { response: CompanySchema },
);
```

### Associate Objects

```typescript
// Associate a contact with a company
await ctx.integrations.hubspot.apiRequest(
  {
    method: "PUT",
    path: `/crm/v3/objects/contacts/${contactId}/associations/companies/${companyId}/contact_to_company`,
  },
  { response: z.object({}).optional() },
);

// Associate a deal with a contact
await ctx.integrations.hubspot.apiRequest(
  {
    method: "PUT",
    path: `/crm/v3/objects/deals/${dealId}/associations/contacts/${contactId}/deal_to_contact`,
  },
  { response: z.object({}).optional() },
);
```

### Update Contact Properties

```typescript
const updatedContact = await ctx.integrations.hubspot.apiRequest(
  {
    method: "PATCH",
    path: `/crm/v3/objects/contacts/${contactId}`,
    body: {
      properties: {
        phone: "+1-555-987-6543",
        lifecyclestage: "customer",
        hs_lead_status: "QUALIFIED",
      },
    },
  },
  { response: ContactSchema },
);
```

### Get Contact Engagements

```typescript
const EngagementsResponseSchema = z.object({
  results: z.array(
    z.object({
      id: z.string(),
      properties: z.record(z.unknown()),
    }),
  ),
});

const engagements = await ctx.integrations.hubspot.apiRequest(
  {
    method: "GET",
    path: `/crm/v3/objects/contacts/${contactId}/associations/notes`,
  },
  { response: EngagementsResponseSchema },
);
```

## Trace Metadata

All methods accept an optional `metadata` parameter as the last argument for diagnostics labeling. See the [root SDK README](../../../README.md#trace-metadata) for details.

## Common Pitfalls

### No Specialized Methods

```typescript
// WRONG - These methods do not exist
await hubspot.createContact({ ... });
await hubspot.getDeals();

// CORRECT - Use apiRequest
await ctx.integrations.hubspot.apiRequest(
  { method: "POST", path: "/crm/v3/objects/contacts", body: { ... } },
  { response: ContactSchema }
);
```

### Property Names are Lowercase

HubSpot properties use lowercase with underscores:

```typescript
// WRONG - CamelCase
const body = {
  properties: {
    firstName: "John",
    lastName: "Doe",
  },
};

// CORRECT - lowercase
const body = {
  properties: {
    firstname: "John",
    lastname: "Doe",
  },
};
```

### Amounts as Strings

Deal amounts are strings, not numbers:

```typescript
// WRONG
const body = { properties: { amount: 50000 } };

// CORRECT
const body = { properties: { amount: "50000" } };
```

### Search Filter Operators

```typescript
// Available operators
const operators = [
  "EQ", // Equals
  "NEQ", // Not equals
  "LT", // Less than
  "LTE", // Less than or equal
  "GT", // Greater than
  "GTE", // Greater than or equal
  "CONTAINS_TOKEN", // Contains (for text)
  "NOT_CONTAINS_TOKEN",
  "HAS_PROPERTY", // Property exists
  "NOT_HAS_PROPERTY",
];

// Example search
const body = {
  filterGroups: [
    {
      filters: [
        { propertyName: "amount", operator: "GTE", value: "10000" },
        { propertyName: "dealstage", operator: "EQ", value: "closedwon" },
      ],
    },
  ],
};
```

### Association Types

Association types follow a naming pattern:

```typescript
// Contact to Company
const type = "contact_to_company";

// Deal to Contact
const type = "deal_to_contact";

// Company to Deal
const type = "company_to_deal";
```

### Pagination

HubSpot uses cursor-based pagination:

```typescript
async function getAllContacts(hubspot: HubSpotClient) {
  const allContacts: Contact[] = [];
  let after: string | undefined;

  while (true) {
    const result = await ctx.integrations.hubspot.apiRequest(
      {
        method: "GET",
        path: "/crm/v3/objects/contacts",
        params: {
          limit: 100,
          ...(after && { after }),
        },
      },
      { response: ListContactsResponseSchema },
    );

    allContacts.push(...result.results);
    if (!result.paging?.next?.after) break;
    after = result.paging.next.after;
  }

  return allContacts;
}
```

### Date Format

Dates use ISO 8601 or Unix timestamp (milliseconds):

```typescript
const body = {
  properties: {
    closedate: "2024-12-31", // ISO date
    // Or Unix timestamp in milliseconds
    closedate: "1735689600000",
  },
};
```

## Error Handling

```typescript
import { RestApiValidationError } from "@superblocksteam/sdk-api";

try {
  const result = await ctx.integrations.hubspot.apiRequest(
    { method: "POST", path: "/crm/v3/objects/contacts", body: { ... } },
    { response: ContactSchema }
  );
} catch (error) {
  if (error instanceof RestApiValidationError) {
    console.error("Validation failed:", error.details.zodError);
  }
}
```

## API Reference

- [HubSpot API Documentation](https://developers.hubspot.com/docs/api/overview)
- [Contacts](https://developers.hubspot.com/docs/api/crm/contacts)
- [Deals](https://developers.hubspot.com/docs/api/crm/deals)
- [Companies](https://developers.hubspot.com/docs/api/crm/companies)
- [Search](https://developers.hubspot.com/docs/api/crm/search)
