---
title: "HubSpot"
description: "Stream HubSpot CRM objects into Streamkap using a HubSpot Private App access token"
---

{/* Ships text-only, matching the dynamodb / s3 / webhook-salesforce source-page template.
    Four screenshots would enrich it, if anyone is capturing:
      1. HubSpot Private App, Scopes tab — the CRM read scopes checked
      2. HubSpot Private App, Auth tab — the access-token reveal/copy control
      3. Streamkap create-source, Auth tab — Name and Access Token fields
      4. Streamkap create-source, Settings tab — the resources multi-select, 8 objects
    Carried over from SCREENSHOTS_NEEDED.md, which main retired as a placeholder file. */}

The HubSpot source pulls CRM objects — contacts, deals, companies, and more — out of your HubSpot portal and streams them into Kafka topics, one topic per object. Records land flat in your destination like any other Streamkap source, so every existing destination consumes them unchanged.

:::info
This connector is in **Beta**. Behaviors and defaults may change before general availability.
:::

:::note
HubSpot is an [API source](/api-sources). It polls the HubSpot API on a schedule rather than streaming a database log, and it delivers to a destination with **Send to destination** instead of a pipeline you build by hand.
:::

## How It Works

1. **Authentication** — Streamkap calls the HubSpot API with a **Private App access token** you provide (see [Prerequisites](#prerequisites)).
2. **Incremental sync** — On the first run each selected object is read in full. After that, the connector polls on a schedule and fetches only records that changed since the last poll, tracking a per-object cursor.
3. **Deletes** — A periodic sweep detects records archived in HubSpot and emits a tombstone (`__deleted = true`) so downstream sinks flip the row to deleted; a slower daily reconciliation pass also catches deletes that leave no archived record, such as merges and permanent purges (see [Deletes and tombstones](#deletes-and-tombstones)).
4. **Rate limiting** — The connector paces its requests to stay under your portal's HubSpot API limits (see [Behavior & Limits](#behavior--limits)).

Each object you select produces a topic named `source_<id>.hubspot.<object>` — for example `source_<id>.hubspot.contacts`.

## Prerequisites

* A HubSpot account with permission to create a **Private App**.
* A **Private App access token** with the CRM read scopes for the objects you want to sync.
* A Streamkap workspace with permission to create source connectors.

### Required scopes

Streamkap reads each resource you select through HubSpot's CRM API, so the Private App needs the **read** scope HubSpot requires for that object. Streamkap never writes to your portal, and it needs no scope for anything you have not selected.

Scope names, and the account tier each is available on, are HubSpot's own and change independently of Streamkap — take them from [HubSpot's scope reference](https://developers.hubspot.com/docs/guides/apps/authentication/scopes). Three mappings are worth knowing when you look them up:

* **`pipelines`** syncs **deal** pipeline definitions, so it needs deal read access — not a scope of its own.
* **`owners`** is read from HubSpot's owners API, not from the CRM objects API.
* Streamkap also reads each selected object's **property list**, which is how it picks up your custom and newly-added properties. On the portals we have tested, the object's read scope already covers this. If it does not on yours, **Test connection** says so and names the object.

:::tip
You do not have to get this right by inspection. Select your resources in Streamkap and click **Test connection**: it names every resource the token cannot read, so you can go back to the Private App and add exactly those scopes.
:::

:::note
**No paid HubSpot plan is required.** Every standard resource Streamkap offers works on a free or entry-tier portal. Only [custom objects](#custom-objects) need a higher HubSpot tier, and they are entirely optional — see the one optional scope below.
:::

#### Optional: `crm.schemas.custom.read`

Grant this **only if you want to sync [custom objects](#custom-objects)**. It is what lets Streamkap list your portal's custom object definitions so it can offer them in the **Resources** picker.

Without it, the Settings tab shows a note that it could not read your custom-object definitions, and everything else keeps working normally — the standard resources you selected are unaffected. If your portal has no custom objects (they need a higher HubSpot tier), there is nothing to grant and nothing to fix; you can ignore the note.

:::warning
Grant a scope for **every** object you select in Streamkap. If a scope is missing, syncing pauses for **that object only** — the rest keep streaming — and the connector shows **Broken** with a message naming the paused object. It recovers automatically once you grant the scope (or deselect the object). See [Per-object permissions](#per-object-permissions).
:::

## HubSpot Setup

### 1. Create a Private App

1. In HubSpot, go to **Settings → Integrations → Private Apps**.
2. Click **Create a private app**, give it a name (for example `Streamkap`), and open the **Scopes** tab.
3. Select the read scopes for the objects you want to sync (see [Required scopes](#required-scopes)).
4. Click **Create app** and confirm.

### 2. Copy the access token

1. Open the app and go to the **Auth** tab.
2. Under **Access token**, click **Show token**, then **Copy**.
3. Keep this token handy for the Streamkap setup below — treat it like a password.

:::info
HubSpot calls this credential a **Private App access token**. Some HubSpot surfaces refer to it as a **service token**, and older portals label it a **legacy Private App token** — they are the same value. Streamkap accepts any of them.
:::

## Streamkap Setup

Follow these steps to configure your new connector:

### 1. Create the Source

* Navigate to [Add Connectors](https://app.streamkap.com/connectors/add?tab=Sources).
* Choose **HubSpot**.

### 2. Connection Settings (Auth tab)

* **Name**: A name for your connector (for example `hubspot-prod`).
* **Access Token**: Paste the HubSpot Private App access token from [HubSpot Setup](#hubspot-setup). It is stored encrypted at rest and never shown again.

Optionally click **Test connection**. This runs a read-only check against HubSpot — nothing is created or written — and reports one of:

* **Connection verified**, with the HubSpot account the token belongs to. Worth a glance: it confirms you pasted the token for the portal you meant.
* **Connection failed** — the token is invalid, revoked, or its Private App was deleted.
* **Some resources need attention** — the token works, but one or more of the objects you selected cannot be read: usually a missing read scope, or a [custom object](#custom-objects) whose qualified name is not in this portal. The result names each object it could not read, so you know exactly what to fix in the Private App.

Testing is optional. Streamkap validates the token again when you save, whether or not you tested.

Click **Next**.

### 3. Settings tab

* **Resources**: Select the HubSpot objects to sync. The default is **contacts** and **deals**. See [Supported Resources](#supported-resources) for the full list.
* **Backfill start date** *(optional)*: The earliest record modification time to sync, as an ISO-8601 date or datetime — `2026-01-01`, or `2026-01-01T00:00:00Z`. A value with no timezone is read as UTC. Leave it empty to sync all history. See [Backfill start date](#backfill-start-date).
* **Sync all properties**: On by default, and best left on. It syncs every property an object exposes, including your custom properties and any you add in HubSpot later. Turn it off to pick individual properties instead — see [Properties and custom fields](#properties-and-custom-fields).

The panel beside the form lists the Kafka topics your selection will produce — one topic per resource.

### 4. Review and create

The last step of the wizard is **Review**. It lists everything you entered, with the access token masked. Click **Create**.

Streamkap validates the token against HubSpot before the source is created — an invalid or under-scoped token is rejected here with an actionable message, before anything is saved.

:::note
Each HubSpot account can back only one source per workspace. If you try to create a second source for an account you have already connected, the save is rejected with "A source for this vendor account already exists in this workspace." The connection test warns you before you get that far — it flags an already-connected account without failing the test itself.
:::

## Editing the Source

An existing HubSpot source has **Status**, **Auth**, **Settings**, **Destinations** and **Config History** tabs, plus a **Logs** link. There is no Schema tab: an API source has no table schema to include or exclude, and it does not expose a schema snapshot.

### Rotating the access token

The token is write-only. It is never displayed again, so the **Auth** tab shows it as stored rather than as a value. Leave it untouched and Streamkap keeps the credential it already holds; click **Replace** and type a new one to rotate it.

Saving a replacement token takes effect immediately: it clears an authentication failure and any per-object scope suspensions in one step, without waiting for the automatic retry.

**Test connection** works here too, and tests the *stored* token — you do not need to re-enter it just to check that it still works. This is the quickest way to confirm a `Broken` source is a credential problem rather than a scope one.

### Custom objects

Once a source exists, the **Settings** tab discovers your portal's custom objects and adds them to the **Resources** picker alongside the eight standard ones. They are listed by their fully-qualified name (`p<portal-id>_<name>`, or `2-<object-type-id>`).

Custom objects are not offered on the create form: discovery needs the saved token to ask HubSpot what your portal has. So create the source with the standard resources you want, then add custom objects on the **Settings** tab.

Custom objects are a higher-tier HubSpot feature, and syncing them needs two scopes the standard resources do not: `crm.schemas.custom.read` to list the definitions and `crm.objects.custom.read` to read the records. Both are optional — a portal with no custom objects needs neither.

:::note
If the Settings tab says it could not read your custom-object definitions, the token is missing `crm.schemas.custom.read`. Nothing else is affected: the resources you already selected keep syncing, and you can still add a custom object by typing its fully-qualified name into **Resources**.
:::

## Sending Topics to a Destination

HubSpot topics reach a destination without you building a pipeline by hand. Streamkap creates and maintains the pipeline for you — the Pipelines list labels it **Auto-managed**.

You can start from whichever end you are already looking at:

* **From the source** — open its **Destinations** tab, click **Send topics**, pick a destination.
* **From the Topics page** — select the topics you want and choose **Send to destination**.
* **From the destination** — open it and click **Add topics**.

To stop delivering a topic, remove it from the source's **Destinations** tab — **Detach all** clears a destination in one go. Removing the last topic removes the destination from the source entirely.

:::note
HubSpot sources do not appear in the **Create pipeline** source picker. That is deliberate — use **Send to destination** instead, and Streamkap manages the pipeline. See [API sources](/api-sources).
:::

:::note
You cannot delete a source that is still delivering. Stop sending its topics from the **Destinations** tab first.
:::

## Supported Resources

Select any of the eight standard CRM objects:

| Resource     | HubSpot object                     |
| ------------ | ---------------------------------- |
| `contacts`   | Contacts                           |
| `deals`      | Deals                              |
| `companies`  | Companies                          |
| `tickets`    | Tickets                            |
| `line_items` | Line items                         |
| `products`   | Products                           |
| `owners`     | Owners                             |
| `pipelines`  | **Deal** pipeline definitions. Ticket pipelines are not synced. |

### Not yet supported

The following are **deferred** and cannot be selected yet. Requesting one is rejected at validation with a message saying the resource is "deferred pending pilot demand — contact support to enable them":

* **Associations** (object-to-object relationships)
* **Quotes**
* **Engagements** (notes, calls, emails, meetings)
* **Property history**

Need one of these? [Contact Streamkap support](mailto:support@streamkap.com) — pilot demand drives the roadmap.

## Behavior & Limits

### Incremental sync

After the initial full read of each object, every poll fetches only records that changed since the last cursor position. **A poll where nothing changed in HubSpot produces zero new records** — this is expected, not a fault.

### Backfill start date

By default the first sync of an object reads **all** of its history. When you don't want years of records you will never query, set a **Backfill start date** on the **Settings** tab: the first sync starts from that instant instead, and records last modified before it are never fetched.

The value bounds the **first** sync of each object and nothing else. Once an object has synced, the connector resumes from its own cursor and the backfill start date no longer applies to it — raising it later hides nothing, and lowering it reaches no further back. An object you add to **Resources** afterwards is new, so its first sync uses whatever the value is at that point.

:::note
Changing the backfill start date on a running source does not re-sync or re-bound anything already syncing. To read an object again from a new bound, remove it from **Resources** and add it back — that makes its next poll a first sync.
:::

:::note
The bound applies to the objects synced incrementally on a modification timestamp — the standard CRM objects and your [custom objects](#custom-objects). `owners` and `pipelines` are read as a whole list each poll and are not date-bounded, so they always sync in full.
:::

### Properties and custom fields

**Sync all properties** is on by default, and it is what keeps a HubSpot source in step with your portal. With it on, Streamkap reads each object's live property list on every poll and syncs **every** property that object exposes — the standard ones, your custom properties, and any property added since the last sync. Add a property to Contacts this afternoon and it starts flowing on the next sync, with nothing to reconfigure in Streamkap.

Turn it off and a **Properties** picker appears, listing every property your selected objects expose. The source then syncs exactly what you choose there and nothing else. The reason to do that is a destination table whose consumers depend on its current columns and must not widen — or simply a portal with hundreds of properties where you only want a handful.

Two things are always synced whatever you pick: each object's last-modified property, which is how the source knows what changed, and `hs_object_id`, which identifies the record. A selection that leaves them out would break incremental syncing, so they are added back for you.

Property names in the picker are HubSpot's **internal** names, not the labels shown in the HubSpot UI — the picker shows both so you can tell them apart. A name only has to exist on one of your selected objects; syncing `dealname` alongside Contacts and Deals gives you the column on Deals and no column on Contacts, which is what HubSpot itself does. If you type a name that exists on none of them, Streamkap rejects it when you save rather than letting you believe it synced, because HubSpot silently ignores a property it does not recognise.

The picker only appears on a source you have already created: reading your portal's property list needs your credentials, and syncing everything is the default, so a new source never has to answer the question.

:::note
[Custom objects](#custom-objects) always sync their full property list — their properties are yours by definition, so the setting does not narrow them. `owners` and `pipelines` have a fixed shape and are unaffected either way.
:::

New properties are added to your destination table automatically when the destination evolves its schema, which is the default. If a destination is set not to evolve, its existing columns stay as they are and the new property arrives on the topic without being added to the table. See [Schema Evolution](/schema-evolution-support).

#### What the columns are called

HubSpot returns a record's properties inside a `properties` object, and Streamkap keeps that shape on the topic so destinations that store nested data natively receive it unchanged. Destinations that need flat columns — every SQL database and warehouse — get the object flattened, which prefixes each column with `properties_`:

| HubSpot property | destination column |
| --- | --- |
| `dealname` | `properties_dealname` |
| `dealtype` | `properties_dealtype` |
| `hs_priority` | `properties_hs_priority` |

The record's own top-level fields — `id`, `createdAt`, `updatedAt`, `archived` — carry no prefix, because they are not properties. If a property looks missing from your table, check for the prefixed name before anything else.

### Daily quota pacing

The connector paces its API calls to stay under your portal's HubSpot limits, self-capping below the per-second account limit so it leaves headroom for your own integrations sharing the same portal.

If the portal's **daily** API budget is still exhausted, the connector pauses ingestion and reports an actionable status message showing when it will resume. Polling resumes automatically at the start of the next UTC day, and **no data is lost** — ingestion continues from where it paused. If you hit this regularly, reduce the number of selected resources or the load from other integrations on the same portal.

### Deletes and tombstones

Streamkap detects deletes two ways.

**The archived sweep.** When a record is **archived** in HubSpot, a periodic sweep (roughly every 6 hours) detects it and emits a record with `__deleted = true`, so your destination's tombstone/delete handling flips the row to deleted. Expect archived records to surface as deletes within one sweep interval. The sweep covers the six CRM object types — `contacts`, `deals`, `companies`, `tickets`, `line_items`, `products` — and any [custom objects](#custom-objects) you sync.

**Reconciliation.** Some deletes leave no archived record for the sweep to find: a record **merge** retires the losing record, and a **permanent purge** (including a GDPR erasure) removes it outright. A slower reconciliation pass — about once a day — closes that gap. It re-lists the live records for each of those same objects and tombstones any that have disappeared, so merged and purged records surface as deletes too, within about a day.

:::note
A **very large object** (roughly tens of thousands of records or more) is not reconciled, so a merge or purge on it is not caught — only its archived deletes are.
:::

:::note
**`owners` and `pipelines` are covered by neither mechanism.** Neither has an archived state in HubSpot — its list endpoint serves only live records — so a removed owner or deal pipeline is not tombstoned, and its row stays in your destination. Both resources still sync inserts and updates normally.
:::

### Per-object permissions

Scope problems are handled per object, not per connector. If the token is valid but missing the read scope for one selected object, only that object's syncing pauses — every other object keeps streaming. The connector shows **Broken**, and its status message names each denied object and the fix.

A denied object recovers on its own: Streamkap retries it periodically (roughly every 15 minutes), and syncing resumes as soon as the scope is granted in HubSpot. You can also clear it immediately by re-saving the connector config — or remove the object from **Resources** if you don't need it.

A fully invalid or revoked token is different: **all** syncing stops, and the connector stays Broken until you save a replacement token — it does not retry a rejected credential on its own.

### Connector status

| Status               | Meaning                                                                                     |
| -------------------- | ------------------------------------------------------------------------------------------- |
| **Pending**          | The source is saved and Streamkap is still deploying it. It moves to **Active** on its own once the connector starts; there is nothing to do. |
| **Active**           | Polling normally. A trace message may note a temporary daily-quota pause (see above).        |
| **Paused / Stopped** | You paused or stopped the source. Resume to continue from the last cursor.                    |
| **Broken**           | The connector needs attention. The status message says which case you are in: *"rejected the API token"* means the token is revoked or invalid — all syncing stops until you replace it; *"denied access to …"* names objects missing a scope — those objects pause while the rest keep streaming, and they recover automatically once the scope is granted. See [Troubleshooting](#troubleshooting). |

## Troubleshooting

**Broken: "rejected the API token" (401 / authentication error)**

The HubSpot access token was rejected — it is invalid, was revoked, or the Private App was deleted. All syncing stops, and Streamkap does not retry a rejected credential on its own: the source recovers when you save a replacement token.

**Resolution:**
- Open the source's **Auth** tab in Streamkap and click **Test connection**. It tests the stored token, so you can confirm the credential is the problem before touching HubSpot.
- Confirm the Private App still exists in HubSpot (**Settings → Integrations → Private Apps**) and its token has not been rotated.
- Re-copy the **access token** (the **service token** / **legacy Private App token** — the same value) from the app's **Auth** tab.
- Back on the source's **Auth** tab, click **Replace**, paste the fresh token, and save. The source recovers immediately.

  **Broken: "denied access to …" (403 on specific objects)**

  The token is valid but is missing the read scope for the named objects. Only those objects pause — every other object keeps streaming — and the status message lists each denied object.

  **Resolution:**
  - Add the missing read scope for each named object in HubSpot (**Private App → Scopes**). See [Required scopes](#required-scopes).
  - That's usually all: Streamkap retries denied objects roughly every 15 minutes, so syncing resumes on its own once the scope is granted. If HubSpot rotated the token when you changed scopes, replace it on the source's **Auth** tab.
  - Don't need the object? Remove it from **Resources** on the **Settings** tab instead.
  - **Test connection** on the **Auth** tab shows the same information on demand — it names each object the token cannot read.

  **No new records on the second poll**

  This is expected. The source is incremental — after the first full read it only emits records that changed in HubSpot since the last poll. If nothing changed, a poll produces zero records. Edit a record in HubSpot and it appears on the next poll.

  **A property I added in HubSpot isn't in my destination**

  New properties are picked up automatically on the next sync, so the usual causes are one of these:

  - **You're looking for the unprefixed name.** In a SQL destination the column is `properties_<name>`, not `<name>` — see [What the columns are called](#what-the-columns-are-called).
  - **The sync hasn't run yet.** The property list is re-read each poll — wait for the next one, and for a record carrying the new property to change.
  - **Sync all properties is off.** With it off only the properties in your **Properties** selection are synced, so a newly added one is not included until you add it there. Add it, or turn **Sync all properties** back on in the **Settings** tab. See [Properties and custom fields](#properties-and-custom-fields).
  - **The destination isn't evolving its schema.** The property is on the topic but the destination is not adding the column. See [Schema Evolution](/schema-evolution-support).

  A new property is only populated for records that change after you add it — HubSpot does not report untouched records as changed. See [Incremental sync](#incremental-sync).

  **Older records are missing after the first sync**

  Check the **Backfill start date** on the **Settings** tab. When it is set, the first sync of each object skips records last modified before that instant.

  Clearing or lowering the value does not bring them back — the bound applies to an object's *first* sync only, and that object is now on its own cursor. Remove the object from **Resources** and add it back to read it again from the new bound. See [Backfill start date](#backfill-start-date).

  **Ingestion paused — daily quota exhausted**

  The portal's daily HubSpot API budget was used up. The connector pauses and resumes automatically at the start of the next UTC day — no data is lost.

  **If it recurs:**
  - Reduce the number of selected resources.
  - Reduce the API load from other integrations sharing the same HubSpot portal.

  **A deleted record still shows in my destination**

  Deletes are detected by a periodic sweep (roughly every 6 hours), not on every poll, so an archived record can take up to one sweep interval to surface as a tombstone (`__deleted = true`). A merge or a permanent purge leaves no archived record, so it is caught instead by the slower daily reconciliation pass and can take up to about a day.

  Cases where the delete never arrives:
  - **The record was an owner or a pipeline.** Neither has an archived state in HubSpot, so neither the sweep nor reconciliation can tell a removed one from a live one. See [Deletes and tombstones](#deletes-and-tombstones).
  - **The object is very large and the delete was a merge or purge.** Reconciliation does not run on very large objects (roughly tens of thousands of records), so on those a merge or purge falls back to the archived sweep, which does not see it. Archived deletes are still caught.

  **Error: resource "deferred pending pilot demand"**

  Associations, quotes, engagements, and property history are deferred and cannot be selected yet. Remove the deferred resource from your selection, or contact support to have it enabled. See [Not yet supported](#not-yet-supported).

## See Also

* [Sources](/sources) — managing source connectors, status, and config history.
* [Salesforce CDC](/webhook-salesforce) — streaming CRM change events from Salesforce.
