HubSpot to Snowflake: how to load CRM data your team can actually model
9 min read Data engineering The Adapters team
Last updated July 2026
Field mapping auto-plugged · tap a port to rewire
Plug a source port into
Transform on this cable
JSON in
JSON out
5 sample records ready
To load HubSpot into Snowflake, read each CRM object (contacts, companies, deals, tickets) from
the HubSpot CRM API v3, land the records in a staging table, and MERGE them into target tables
keyed on the object id. Pull only changed rows using the
hs_lastmodifieddate property as a watermark, store associations in a bridge table so
attribution and pipeline models can join, and map epoch-millisecond timestamps to UTC
TIMESTAMP_NTZ. Handle archived records so deleted contacts do not linger in the
warehouse.
Key takeaways
- HubSpot has no native Snowflake sync. A managed connector or a scheduled job has to sit between the CRM API v3 and the warehouse and move each object.
- Incremental beats full pulls. Daily and burst rate limits make a naive full scan slow, so watermark on
hs_lastmodifieddateand page with the cursor. - Associations belong in a bridge table. Deal-to-contact and contact-to-company links are many-to-many, so a junction table is what lets attribution and pipeline models join cleanly.
- Timestamps are epoch milliseconds. HubSpot date properties arrive as Unix millis, so cast them to
TIMESTAMP_NTZin UTC or every cohort report drifts.
How do you load HubSpot into Snowflake?
You load HubSpot into Snowflake by authenticating to the HubSpot CRM API v3 with a private
app token, reading each object with its properties and associations, landing the batch in a
staging table, and running a MERGE into target tables keyed on the object id.
HubSpot ships no native Snowflake driver, so a managed connector or a scheduled job has to
sit in the middle and move the data.
The pipeline has three moving parts. First, auth: a private app (or OAuth app) scoped to the
CRM objects you need, which gives you the token every request carries. Second, the read side:
the v3 API exposes contacts, companies, deals,
tickets, and engagements or events, each with a properties model that mixes
HubSpot's default fields with the custom properties your team has added. You ask for the
properties you want by name and page through results with a paging cursor. Third, the write
side: a Snowflake warehouse and role with insert and merge rights on the destination schema,
plus a stage if you load files before the MERGE.
Watch the limits while you read. HubSpot enforces both a daily call cap and a per-second
burst limit, and it returns results in pages you walk with an opaque after
cursor rather than an offset. A production job requests only the properties it needs, backs
off when it hits a 429, and checkpoints the cursor so a mid-run failure resumes instead of
restarting. The managed path is the
HubSpot to Snowflake
integration, which handles the token, pagination, the watermark, staging, the MERGE, and
retries. The shape is the same one you would use for a
Salesforce to
Snowflake load, just with a different CRM API in front of it.
How do you map HubSpot properties to Snowflake types?
Number properties such as a deal amount map to Snowflake NUMBER, datetime
properties (which arrive as epoch milliseconds) map to TIMESTAMP_NTZ stored in
UTC, enumeration and dropdown properties map to VARCHAR, and the object
id stays a VARCHAR natural key. Get the timestamp cast right at load
time or every date filter and cohort report will be off.
The subtlety with HubSpot is the properties model. Every object carries default properties
plus custom ones your admins created, and the API hands most values back as strings inside a
properties object. You decide the real type on the way in. Money and counts
become NUMBER, dates become timestamps, and single-select or multi-select
enumerations become VARCHAR (a multi-select is a semicolon-delimited string you
can split later). Keep the raw payload in a VARIANT column too, so a property you
did not model today is still recoverable without a re-pull.
| HubSpot object / property | HubSpot type | Snowflake type | Note |
|---|---|---|---|
| deal.amount | number | NUMBER(38,2) | Pipeline value. Use NUMBER, never FLOAT, so revenue rollups do not drift |
| deal.closedate, contact.createdate | datetime (epoch millis) | TIMESTAMP_NTZ | Divide the millis to seconds and cast in UTC. This is what cohort and velocity models read |
| hs_lastmodifieddate | datetime (epoch millis) | TIMESTAMP_NTZ | Store in UTC. This is the incremental watermark, so treat it as an absolute instant |
| deal.dealstage, contact.lifecyclestage | enumeration | VARCHAR | Internal stage id, not the label. Join to a pipeline reference table for display names |
| contact.email, company.name | string | VARCHAR | Plain text. Email is a natural dedupe key for contacts but is not the primary id |
| id (vid / object id) | string | VARCHAR | Opaque HubSpot id. Keep as VARCHAR and MERGE on it; do not cast to a number |
| hs_object_source, multi-select props | enumeration (multi) | VARCHAR | Semicolon-delimited. Store raw, split into an array downstream if you need it normalized |
Cluster large object tables on the column you filter on most (often
hs_lastmodifieddate or a stage), and keep the VARIANT raw column
beside the typed columns. That way a new custom property is a schema addition, not a
historical backfill.
How do you load HubSpot incrementally?
You load HubSpot incrementally by using hs_lastmodifieddate as a watermark:
filter the CRM Search API with a lastmodifieddate greater-than the last stored
value, sort ascending on that field, and page through the changes. Store the high watermark
after each successful run, overlap the window by a few minutes, and MERGE on the object
id so retries stay idempotent.
The contrast with a full sync is stark. A full sync reads every contact, company, and deal on
every run, which burns your daily call budget and grows slower as the CRM does. An
incremental load asks the Search API for only the objects whose
hs_lastmodifieddate moved since last time, so a run that finds a few thousand
changed records finishes in seconds. You still want a periodic full reconciliation (say
weekly) to catch anything a watermark can miss, but the day-to-day path should be
incremental. Overlap the window slightly because modification times can commit a little out
of order, and keep the MERGE idempotent so re-reading a handful of rows costs nothing.
Choosing an extraction method is really a freshness-versus-effort decision. Here is how the three common approaches trade off.
| Extraction method | Pros | Cons |
|---|---|---|
| Full sync (read every object each run) | Simple to reason about, self-correcting, catches deletes by absence | Slow, burns daily rate limit, scales badly as the CRM grows |
| hs_lastmodifieddate watermark (Search API) | Moves only changed rows, fast, cheap on rate limit, easy to checkpoint | Misses hard deletes on its own, needs window overlap for out-of-order commits |
| Webhooks / events | Near real-time, push-based, low steady-state cost | Delivery is not guaranteed, needs an endpoint and a backfill path, easy to miss during downtime |
Most teams land on a hybrid: an hs_lastmodifieddate watermark on a schedule for
the bulk of the work, optional webhooks for the objects that need to be fresh within minutes,
and a weekly full reconciliation to true-up deletes. That is the same pattern behind our
walkthrough of how to
sync Salesforce to
Snowflake, because CRM sources share the same incremental problems.
How do you handle HubSpot associations in the warehouse?
You handle HubSpot associations by loading each object into its own table and storing the links (deal-to-contact, contact-to-company) in a separate bridge, or junction, table. Because associations are many-to-many, a bridge table with the two object ids and the association type is what lets attribution and pipeline models join contacts to deals without duplicating or losing rows.
HubSpot exposes associations through the associations API and inline on each object read. A
single deal can associate with several contacts, and a contact can sit under one company but
touch many deals. If you flatten those links onto the object rows, you either fan out the
deal amount across contacts (and double-count revenue) or you drop links. The clean model is
three object tables (contacts, companies, deals) plus a
deal_contact bridge and a contact_company bridge, each holding the
pair of ids and the association type. Attribution then joins deals to contacts through the
bridge, and pipeline reporting joins deals to companies the same way.
Two things keep the model honest. First, idempotency: land every batch in staging, collapse
to one row per id keeping the latest hs_lastmodifieddate, then MERGE
on id into the target so a re-run after a timeout produces the same state rather
than duplicates. Second, deletes: HubSpot archives records instead of hard-deleting them, and
an archived contact simply stops appearing in a plain watermark query. Read the archived flag
(or reconcile against a full list) and either soft-delete the row with a
_deleted_at stamp or remove it, so a contact deleted in the CRM does not linger
as a ghost in Snowflake. Once the objects and bridges are in place, a RevOps analyst or a
non-technical teammate can
ask questions of that data in plain
English instead of waiting on a SQL queue.
Should you build this or use a managed integration?
Build it yourself when you have engineers who want to own the token refresh, pagination, watermark, MERGE, and delete handling, and time to maintain them as HubSpot's API evolves. Use a managed integration when you would rather map the objects once and let the connector run the incremental sync, retries, and per-record logging. The API work is not hard, but it is ongoing.
A hand-rolled pipeline is a real amount of code: OAuth or private-app token handling,
cursor-based pagination, the daily and burst rate-limit backoff, the
hs_lastmodifieddate watermark with window overlap, the staging-plus-MERGE for
idempotency, association bridges, and archived-record handling. None of it is exotic, and a
capable data engineer can stand it up in a sprint. The cost shows up later, in the
maintenance: a new custom property, a changed rate limit, a schema drift, or a silent switch
from incremental back to full refresh. Someone has to own that.
A managed connector does the same work behind a mapping UI, with scheduled incremental sync, automatic retries, and per-record logs so you can see exactly which row failed and why. Our pricing is flat by plan (Starter $49, Growth $149, Scale $399) rather than metered per row, so a launch month that touches millions of records costs the same as a quiet one. See pricing for the full breakdown, or read how we think about any data integration platform. To see the HubSpot object mapping against your own portal, start a demo.
Questions
How do you connect HubSpot to Snowflake?
You connect HubSpot to Snowflake by authenticating to the HubSpot CRM API v3 with a private app token, reading each object with its properties and associations, landing the batch in a staging table, and running a MERGE into target tables keyed on the object id. HubSpot ships no native Snowflake driver, so a managed connector or a scheduled job sits in the middle.
How do you load HubSpot into Snowflake incrementally?
You load incrementally by using hs_lastmodifieddate as a watermark: filter the CRM Search API for records changed since the last stored value, sort ascending, and page with the cursor. Store the high watermark, overlap the window by a few minutes, and MERGE on the object id so retries stay idempotent. Add a periodic full reconciliation to catch deletes.
How do HubSpot properties map to Snowflake types?
Number properties like deal amount map to NUMBER, datetime properties arrive as epoch milliseconds and map to TIMESTAMP_NTZ in UTC, enumeration and dropdown properties map to VARCHAR, and the object id stays a VARCHAR natural key. Keep the raw payload in a VARIANT column so a property you did not model today is still recoverable without a re-pull.
How do you handle HubSpot associations in Snowflake?
Load each object (contacts, companies, deals) into its own table and store the links in a bridge table holding the two object ids and the association type. Because deal-to-contact and contact-to-company associations are many-to-many, the junction table is what lets attribution and pipeline models join without double-counting revenue or dropping links.
How do you handle deleted HubSpot records in the warehouse?
HubSpot archives records rather than hard-deleting them, and an archived contact simply stops appearing in a plain watermark query. Read the archived flag or reconcile against a full list, then soft-delete the row with a deleted_at stamp or remove it. Otherwise a contact deleted in the CRM lingers as a ghost row in Snowflake and skews counts.
What are the HubSpot API rate limits to plan around?
HubSpot enforces both a daily call cap and a per-second burst limit, and it returns results in pages you walk with an opaque cursor rather than an offset. A production job requests only the properties it needs, backs off when it hits a 429, and checkpoints the cursor so a mid-run failure resumes instead of restarting from zero.
Load HubSpot into Snowflake incrementally, without writing the merge logic
Private-app auth, cursor pagination, hs_lastmodifieddate watermark, association bridges, archived-record handling, MERGE, and retries. Map the pair in the browser and pay a flat price from $49 a month.
No credit card required.