Pular para o conteúdo principal

Sync HubSpot Contacts via API Without Missing a Single Update

Retrieve Newly Created and Updated HubSpot Contacts

Syncing HubSpot contacts looks like the simplest task in the world until the second run. The first run brings everything, works beautifully, and you mark the task done. Then the second run arrives, and with it a choice that defines the entire fate of the integration: you bring the whole base again, which is slow, expensive, and burns through the rate limits, or you bring only what changed since last time, which is fast and elegant, but full of traps for whoever does not know the rules.

The right answer is almost always incremental sync, and it depends entirely on one thing: how you use the records' creation and modification dates. Getting that detail right is the difference between a routine that runs in seconds and faithfully captures every change, and a routine that loses updates silently or keeps clogging the destination with duplicates until nobody trusts what is there anymore.

In this guide, I will show you the complete incremental pattern, from the first load of the base to the pointer that makes each subsequent run nearly instant. It is the same pattern that sustains an always-current BI, an ERP that receives exactly the right contacts, and a nurturing cadence that leaves nobody out because of stale data.

The two timestamps that rule the sync

Every record in HubSpot carries two date fields that are the heart of any sync: createdate, which marks when the record was created, and hs_lastmodifieddate, which marks the last time it changed. The initial load sweeps the base by createdate, oldest to newest. The incremental sync, from the second run on, does a search for hs_lastmodifieddate greater than the moment the last run finished. This modification-date filter is powerful because it captures, in a single pass, both new contacts and edited ones, without you having to handle the two cases separately.

POST /crm/v3/objects/contacts/search

{

"filterGroups": [

{ "filters": [

{ "propertyName": "hs_lastmodifieddate", "operator": "GTE", "value": "1719700000000" }

] }

],

"sorts": [{ "propertyName": "hs_lastmodifieddate", "direction": "ASCENDING" }],

"properties": ["email", "firstname", "lastname", "hs_lastmodifieddate"],

"limit": 100

}

Why this matters for your operation

A well-built incremental sync is what keeps your systems speaking the same language with zero human effort. The sales team sees in the ERP exactly the same contact marketing sees in HubSpot. The BI reflects today's base, not last week's. When the sync loses updates, the effect is worse than having no sync at all, and that is a point few people realize. Without a sync, everyone knows they need to check in HubSpot. With a broken sync, everyone trusts data that is wrong: a stage change that did not propagate, an updated email that never reached the other system, and the decision gets made on stale information, with the false sense that everything is fine.

The mistake that makes you lose updates

homem-no-computador-seo-otimizada

Almost everyone's instinct is to use the exact time the last sync finished as the next cutoff. Do not do that. Step back and always work with a window that overlaps. The reason is technical and important: records changed right at the cutoff boundary can slip past the search, either because of a small clock difference between your system and the HubSpot servers, or, more often, because the Search API reads from an index that is only eventually consistent. A just-created or just-changed record, even with its hs_lastmodifieddate already set, can take moments and sometimes several minutes to appear in search results, so the overlap window should be at least as wide as that indexing delay, not just a few seconds.

A tip from someone who has been burned: always prefer to repeat over to miss. An overlap window of a few minutes brings a few duplicate records, which you remove for free with id deduplication. Missing an update, on the other hand, is a silent error nobody notices until the data diverges down the line, when it is already too late to know exactly what slipped through and when.

Paginate and deduplicate by id

Search paginates by cursor and carries the 10,000-record cap per filter set, like any other search. If a sync window brings more than that, narrow the time interval until it fits below the cap. And when writing to the destination, always use the contact id as the operation key. That is what makes the magic work: a contact that appears twice because of the window overlap is simply updated a second time, instead of becoming a new record. Deduplication by id is what makes the overlap safe, because you get the guarantee of never missing a change without paying the price of duplicating records.

What to do during activity spikes

On days of a big campaign, a list import, or an automation that touched many records at once, a short time window can blow past the 10,000 cap easily. In those cases, the solution is to narrow the window dynamically: if a time range brought a volume near the cap, split it in half and run the two halves separately. This keeps each search comfortably below the limit even when the volume of changes spikes, and prevents an activity surge from breaking the sync right when it is needed most.

Handling deletions and archiving

Incremental sync by modification date captures creations and edits elegantly, but it has an important blind spot you need to handle consciously: deletions. When a contact is deleted or archived in HubSpot, it simply stops appearing in the search, with no trace or warning. If your destination needs to reflect those deletions, handle them separately, for example by periodically comparing the set of active ids in HubSpot with the set of ids in the destination, and marking as inactive everything that disappeared from the source but is still in the destination.

It is worth deciding this policy early, in the integration design: does the destination mirror deletions or just accumulate records forever? For a historical BI, you might prefer to keep the record with an inactive flag, preserving history. For an operational ERP, you might need to actually remove it so you do not work with dead data. There is no single right answer, but there is an obligation to choose consciously, because the API's silence about deletions will not make that decision for you.

Store the new cutoff

At the end of each run, save the highest hs_lastmodifieddate you actually processed in that round. The next sync starts from exactly that point, minus the overlap margin you defined. This persisted pointer is what turns the routine into something truly incremental, instead of sweeping an arbitrary fixed interval every time. Store it somewhere that survives between runs: a field in a control table, a state variable in the automation flow, a dedicated file. What matters is that it is not lost when the routine ends.

The complete flow, start to finish

  1. Initial load: sweep the base by createdate, paginating by cursor, and write everything to the destination using the contact id as the key.
  2. Store the pointer: save the highest hs_lastmodifieddate processed in that first load.
  3. Incremental sync: search by hs_lastmodifieddate greater than the pointer, minus the overlap window.
  4. Paginate the search by cursor; if a window exceeds 10,000 records, narrow the time interval and run again.
  5. Deduplicate by id when writing, so the window overlap updates instead of duplicating.
  6. Update the pointer with the new maximum hs_lastmodifieddate and repeat the cycle on the next run.

In practice: the base that grew on its own overnight

An operation ran the contact sync every night and the destination base swelled endlessly, night after night. The routine had no pointer and no id deduplication: it simply recreated records on each run, stacking duplicate on duplicate. Within a few weeks, the destination held nearly triple the contacts HubSpot actually contained, and any report built on that base was pure fiction, with rates and totals that meant nothing.

The fix was to adopt the hs_lastmodifieddate pointer and deduplicate by id on write, and the effect was twofold and immediate. The base's phantom growth stopped on the first night. And, as a bonus, the sync started running in a fraction of the time, because it stopped reprocessing the entire base and started touching only the contacts that actually changed since the night before. The same routine that took hours started finishing in minutes, with finally trustworthy data on the other side.

Incremental sync checklist

  1. Does the initial load sweep by createdate and write with the contact id as the key?
  2. Does the incremental sync search by hs_lastmodifieddate greater than the pointer?
  3. Is there an overlap window of a few minutes at the cutoff point?
  4. Does the search paginate by cursor and narrow the interval when it exceeds 10,000?
  5. Does the write deduplicate by id, so the overlap does not create duplicates?
  6. Do deletions and archiving have a defined policy handled separately?
  7. Is the pointer with the highest hs_lastmodifieddate persisted between runs?

Frequently asked questions

Why use an overlap window?

To avoid missing records changed at the cutoff boundary. The Search API reads from an eventually-consistent index, so a just-changed record can take moments, sometimes minutes, to appear, and clocks between systems can drift. Size the overlap to cover that indexing delay. It brings a few duplicates, removed for free by id deduplication.

Should I sync by createdate or by hs_lastmodifieddate?

Initial load by createdate. Incremental sync by hs_lastmodifieddate, which captures both new and edited contacts since the last cutoff, in a single filter. One caveat for contacts: they have two modification fields, lastmodifieddate (property changes, visible in the app) and hs_lastmodifieddate (any activity, including engagement such as email opens and page views). hs_lastmodifieddate catches more, but it can move for reasons unrelated to your data, so choose consciously. The response also returns a top-level updatedAt field that mirrors it and is a consistent value to store as your pointer.

What if the window exceeds 10,000 records?

Narrow the time interval until it falls below the Search API cap, and paginate each range by cursor. During activity spikes, split the window in half dynamically.

How do I avoid duplicating contacts in the destination?

Use the contact id as the key when writing. That way a contact that appears twice because of the overlap is updated instead of creating a new record.

How do I handle deletions in incremental sync?

The modification-date search does not see deletions. Handle them separately, comparing active ids in HubSpot with the destination's and marking as inactive or removing what disappeared, according to your policy.

Can I sync companies and deals the same way?

Yes. The pattern of createdate for initial load and hs_lastmodifieddate for incremental works for any object. Only the type in the URL and the properties you request change.

Have a CRM, ERP, or BI that needs HubSpot's updated contacts, without missing or duplicating? At Insight Sales we design incremental syncs that run reliably at volume. Talk to our team and find out how we can help.

Ready to take your operation to the next level?

Talk to a specialist and see how we can help.

paper-plane