# Tool.hubspot Integration.5ebd51c72e954598

> Build reliable HubSpot ↔ Airtable sync workflows. Covers API selection, rate limits, sync architecture, and n8n implementation patterns.

- Skill: `ai45lab/tool-hubspot-integration-5ebd51c72e954598` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add ai45lab/tool-hubspot-integration-5ebd51c72e954598`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ai45lab/tool-hubspot-integration-5ebd51c72e954598/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: ai45lab (https://skillmd.com/u/ai45lab)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ai45lab/tool-hubspot-integration-5ebd51c72e954598

---

Use this skill when the OpenART registry selects `tool.hubspot_integration.5ebd51c72e954598` for the current task.

# HubSpot Integration Patterns

Build reliable HubSpot ↔ Airtable sync workflows. This skill covers API selection, rate limits, sync architecture, and n8n implementation.

## Quick Reference

| Task | API | Endpoint Pattern |
|------|-----|------------------|
| CRUD objects | v3 | `POST /crm/v3/objects/{type}` |
| Search/filter | v3 | `POST /crm/v3/objects/{type}/search` |
| Batch upsert | v3 | `POST /crm/v3/objects/{type}/batch/upsert` |
| Associations (all) | v4 | `POST /crm/v4/associations/{from}/{to}/batch/read` |
| Labeled associations | v4 | `PUT /crm/v4/objects/{type}/{id}/associations/{toType}/{toId}` |

## API Selection

### Use v3 for Objects + Search
```
/crm/v3/objects/contacts          # CRUD
/crm/v3/objects/contacts/search   # Filter + paginate
/crm/v3/objects/contacts/batch/upsert  # Bulk writes
```

### Use v4 for Associations
v3 associations only return the **primary** associated company. v4 returns **all** associations with labels.

```
/crm/v4/associations/contacts/companies/batch/read  # Batch read (1000 inputs)
/crm/v4/objects/contact/{id}/associations/company/{companyId}  # Labeled association
```

## Authentication

Use **Private App tokens** (Bearer auth). API keys are deprecated.

```
Authorization: Bearer {ACCESS_TOKEN}
```

**Required scopes for sync:**
- `crm.objects.contacts.read` / `.write`
- `crm.objects.companies.read` / `.write`
- `crm.objects.deals.read` / `.write`
- `crm.schemas.custom.read`

## Rate Limits

### Tiered Limits (Private Apps)

| Tier | Burst (per 10s) | Daily |
|------|-----------------|-------|
| Free/Starter | 100 | 250,000 |
| Professional | 190 | 625,000 |
| Enterprise | 190 | 1,000,000 |

### Search API: Separate Limit
**5 requests/second per token** — no rate limit headers returned.

### Associations API (v4)
- Burst: 100-150/10s depending on tier
- Batch read: 1,000 inputs max
- Batch create: 2,000 inputs max

### Rate Limit Headers
```
X-HubSpot-RateLimit-Remaining      # Requests left in 10s window
X-HubSpot-RateLimit-Daily-Remaining # Requests left today
Retry-After                        # Seconds to wait after 429
```

**Backoff Strategy:**
1. If `Retry-After` header exists, wait that duration
2. Otherwise, if `X-HubSpot-RateLimit-Remaining < 10`, wait 3 seconds
3. On 429 without headers, wait 10 seconds (TEN_SECONDLY_ROLLING policy)

## Sync Architecture

### Recommended: Hybrid Pattern
- **Webhooks** for immediacy (deletions, merges, critical updates)
- **Polling with watermark** for completeness (incremental sync)

### Watermark Incremental Sync (Read Path)

```javascript
// Store watermark: last_sync_timestamp (epoch ms)
{
  "filterGroups": [{
    "filters": [{
      "propertyName": "lastmodifieddate",  // Note: NOT hs_lastmodifieddate for contacts
      "operator": "GT",
      "value": "1674758400000"
    }]
  }],
  "sorts": [{ "propertyName": "lastmodifieddate", "direction": "ASCENDING" }],
  "limit": 200,
  "after": "cursor_from_previous_page",
  "properties": ["email", "firstname", "lastname", "hs_object_id"]
}
```

**Critical:** Use overlap window (watermark minus 2-5 minutes) to avoid missing records with identical timestamps.

### Write Path (Airtable → HubSpot)
Use batch upsert with `idProperty` for idempotent writes:

```javascript
POST /crm/v3/objects/contacts/batch/upsert?idProperty=email
{
  "inputs": [
    { "id": "user@example.com", "properties": { "firstname": "James" } }
  ]
}
```

## Avoiding N+1 Queries

**Bad:** Fetch 100 contacts, then 100 individual association calls = 101 requests

**Good:** Batch read associations:
```javascript
POST /crm/v4/associations/contacts/companies/batch/read
{ "inputs": [{ "id": "101" }, { "id": "102" }, ...] }  // up to 1000
```

## Bi-Directional Loop Prevention

Prevent infinite sync loops with:

1. **Field-level diffing:** Only write if incoming value differs from existing
2. **"Last Updated By" filter:** Exclude changes made by integration user
   ```
   hs_updated_by_user_id NOT_EQ {integration_user_id}
   ```

## Reference Files

- **[references/gotchas.md](references/gotchas.md)** — Merge handling, deletions, property names, data formats
- **[references/n8n-patterns.md](references/n8n-patterns.md)** — Pagination loops, webhook patterns, rate limiting
- **[references/api-reference.md](references/api-reference.md)** — Code examples for search, batch ops, associations

