惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Know Your Adversary
Know Your Adversary
N
News and Events Feed by Topic
C
CXSECURITY Database RSS Feed - CXSecurity.com
P
Privacy & Cybersecurity Law Blog
P
Privacy International News Feed
Recent Announcements
Recent Announcements
T
Threatpost
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Last Week in AI
Last Week in AI
博客园 - 叶小钗
AWS News Blog
AWS News Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
Cyberwarzone
Cyberwarzone
月光博客
月光博客
The Cloudflare Blog
罗磊的独立博客
P
Palo Alto Networks Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 聂微东
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
T
Tailwind CSS Blog
PCI Perspectives
PCI Perspectives
Forbes - Security
Forbes - Security
Latest news
Latest news
V
Visual Studio Blog
阮一峰的网络日志
阮一峰的网络日志
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
WordPress大学
WordPress大学
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
有赞技术团队
有赞技术团队
N
News and Events Feed by Topic
爱范儿
爱范儿
L
Lohrmann on Cybersecurity
Cisco Talos Blog
Cisco Talos Blog
I
Intezer
S
Secure Thoughts
Hacker News: Ask HN
Hacker News: Ask HN
C
CERT Recently Published Vulnerability Notes
TaoSecurity Blog
TaoSecurity Blog
博客园 - 司徒正美
Google DeepMind News
Google DeepMind News
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Google Online Security Blog
Google Online Security Blog
H
Hacker News: Front Page
Recent Commits to openclaw:main
Recent Commits to openclaw:main
V2EX - 技术
V2EX - 技术
Microsoft Azure Blog
Microsoft Azure Blog
O
OpenAI News
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Webroot Blog
Webroot Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
How to sync large amounts of contacts from the HubSpot API
Sapnesh Naik · 2026-05-08 · via DEV Community

If you are building API integrations with HubSpot in your product, you will eventually need to sync more than 10,000 contacts from a customer's portal. That is not easy when you want a durable, resumable sync that fetches only new and changed records, instead of refetching everything on every run. You also need to think about managing the auth tokens for 100s of users, record storage, webhook updates, and more.

This guide walks through how to build a production-ready HubSpot contacts sync using Claude Code, Cursor, Codex, or any other AI Coding Agent with the Nango AI Function Builder skill.

hubspot-contacts-sync-demo-nango

The full working demo, including the Nango sync, an Express backend, and an HTML dashboard for browsing synced contacts, is on GitHub at NangoHQ/blog-demos/how-to-sync-large-amounts-of-contacts-from-hubspot-api.

Note: The same steps can also be used for syncing HubSpot companies, deals, tickets, and any other CRM Nango supports.

Why syncing large amounts of contacts from HubSpot is hard

HubSpot exposes contacts through two endpoints, and neither one is enough on its own:

Endpoint Filter by lastmodifieddate? 10k total cap?
GET /crm/v3/objects/contacts (basic list) No No
POST /crm/v3/objects/contacts/search Yes Yes (400 error past 10k)

The search-the-crm guide confirms it: "The search endpoints are limited to 10,000 total results for any given query. Attempting to page beyond 10,000 will result in a 400 error." Search is also rate-limited to five requests per second per account.

So the search endpoint is the only one that filters by lastmodifieddate (what you need for incremental syncs), but it walls off at 10k. The basic list endpoint has no cap but no filter. The HubSpot community forum is full of 400 reports from developers who only used search and assumed their code was broken.

Solution to sync large amounts of contacts from HubSpot

Split the sync into two phases that share state via Nango checkpoints:

  1. Initial phase. Page through every contact using the basic list endpoint. No 10k cap. After each page, save a checkpoint with the after cursor and the highest lastmodifieddate seen so far. Note: while this endpoint does have lastmodifieddate in its response, it does not support filtering by lastmodifieddate.
  2. Incremental phase. Switch to the search endpoint. Filter by lastmodifieddate GT <watermark>, sort ascending by lastmodifieddate, and page through. Each page advances the watermark. If a request 400s past the 10k cap, catch the error, save the latest watermark, and start a fresh search from it inside the same run. Repeat until a search returns fewer than 10k results, so no modified records are missed.

Why building this from scratch is harder than it looks

The two-phase pattern is the easy part. The infrastructure underneath is the hard part:

  • Durable checkpoints that survive process restarts and store per-connection cursors and watermarks.
  • Resumable execution, so a 200k-contact backfill can stop and pick up across multiple runs without skipping or duplicating records.
  • HubSpot OAuth lifecycle with concurrent token refreshes and graceful handling of BAD_REFRESH_TOKEN.
  • Rate-limit handling with backoff and per-connection queuing for HubSpot's 5 req/s search cap.
  • Sync-completion webhooks so your backend knows when new records landed.

What Nango provides for building API integrations

Nango is an agentic API integrations platform. It gives you five building blocks that turn the two-phase pattern from a multi-week project into minutes of work with a coding agent:

  1. Wide API support with managed auth. Pre-built auth for 700+ APIs, including HubSpot, with OAuth 2.0, API keys, basic auth, JWT, and other auth types. Token refresh, encryption, and revocation handling are managed for you.
  2. AI Function Builder skill for coding agents. Install once with npx skills add NangoHQ/skills and your AI coding agent (Claude Code, Cursor, Codex, Gemini CLI, and others) gets the context to research the API, write a Nango sync or action in TypeScript, run dryruns against a real connection, and iterate until the integration works.
  3. Actions and tool calls. Actions are functions written in TypeScript that run on Nango's infrastructure and are triggered from your app or by an AI agent. For example, a create-contact action calls POST /crm/v3/objects/contacts with the right token for the right connection, returning the created record.
  4. Data syncs with record storage. Syncs run on Nango's serverless, tenant-isolated runtime. Synced records are stored in Nango's cache so your app can read them in a paginated manner with nango.listRecords(). The sync handles retries, rate limits, and resumable execution automatically.
  5. Webhooks from external APIs. Nango ingests webhooks from external providers like HubSpot (for example, contact.deletion events), attributes each event to the right connection, and forwards a normalized payload to your backend. You write the handler once and Nango deals with provider-specific signing, retries, and routing.

Implementing a HubSpot contacts sync with Nango

Follow the steps below to implement a durable HubSpot contacts sync using the Nango AI Function Builder skill.

By the end of this section, you will have:

  • A customizable auth UI in your product so customers can connect their HubSpot account.
  • An initial backfill sync that fires the moment a new HubSpot connection is created and pulls all contacts.
  • An incremental phase that picks up only modified contacts on every run (every hour, customizable) and slides past the 10k search cap automatically.
  • Checkpoints that survive runtime restarts and rate limits.
  • A backend that receives sync-completion webhooks and reads only the records that changed.

Prerequisites

You will need a Nango account (the free tier is enough for development). Then register your own HubSpot OAuth app with the crm.objects.contacts.read scope, set the OAuth callback URL to https://api.nango.dev/oauth/callback, and configure HubSpot as an integration in the Nango dashboard.

Nango dashboard with HubSpot configured as an integration: client ID, client secret, callback URL, and the crm.objects.contacts.read scope

Note: If you do not have your own HubSpot OAuth app yet, you can use Nango's pre-configured developer credentials to get going, then swap to your own credentials before going to production.

Nango dashboard offering pre-configured HubSpot developer credentials for a quick start

Next, install the Nango CLI and initialize an integrations project:

npm install -g nango
nango init
cd nango-integrations

Enter fullscreen mode Exit fullscreen mode

Then install the AI Function Builder skill, so your coding agent has the context it needs to write Nango functions:

npx skills add NangoHQ/skills -s building-nango-functions-locally

Enter fullscreen mode Exit fullscreen mode

This adds Nango's interface patterns, Zod schema conventions, dryrun testing flow, and CLI usage to whichever coding agent you use.

Tip: LLM training data on Nango is often stale. Add the Nango docs MCP server alongside the skill, so your agent can pull current API references during code generation.

Installing the Nango AI skill in Claude Code

Finally, set a Nango dev API key in a .env file at the root of nango-integrations so the CLI and dryrun commands can authenticate:

# nango-integrations/.env
NANGO_SECRET_KEY_DEV=<your-dev-api-key>

Enter fullscreen mode Exit fullscreen mode

You can create or copy a dev API key from the Nango dashboard.

Step 1: Add a HubSpot test connection

Before generating the sync, create a real HubSpot connection in the Nango dashboard. The AI coding agent uses this connection to run nango dryrun against the live HubSpot API while it iterates on the function, so the generated code is tested against real responses.

In the Nango dashboard, click Add Test Connection, pick HubSpot, and complete the OAuth flow against a HubSpot developer account. Copy the connection ID from the connection details page; you will pass it into the prompt in the next step.

Adding a HubSpot test connection from the Nango dashboard via the OAuth flow

Step 2: Generate the contacts sync

Open your project in your AI coding agent and invoke the AI Function Builder skill with a prompt like the one below. Replace the connection ID with the one you just created.

Build a Nango sync for HubSpot contacts that runs every hour, handles
portals with more than 10,000 contacts reliably, and resumes cleanly
across runs without skipping or duplicating records.

Integration ID: hubspot
Connection ID: <your-connection-id>

Properties to sync: firstname, lastname, email, phone, jobtitle,
company, createdate, lastmodifieddate.

Enter fullscreen mode Exit fullscreen mode

The agent figures out the rest from HubSpot's docs and the AI Function Builder skill: which endpoints to use, how to paginate, how to handle the 10k search cap, and how to checkpoint for resumability.

Claude Code generating the HubSpot contacts sync function via the Nango AI Function Builder skill

The agent researches both endpoints, writes the function, generates a test suite, runs nango dryrun against your test connection, and iterates on real responses until everything passes. Here is what the agent does behind the scenes.

Step 3: Review the generated sync

When the agent finishes, you will see three new files inside nango-integrations/hubspot/:

  • hubspot/syncs/fetch-contacts.ts: the sync function itself, with the two-phase logic, checkpoint schema, and Zod model.
  • hubspot/tests/fetch-contacts.test.json: a recorded HubSpot API response captured during the agent's dryrun, used as a deterministic fixture for unit tests.
  • hubspot/tests/hubspot-fetch-contacts.test.ts: the unit test that replays the fixture and asserts the sync produces the expected records.

Let's look at the important bits of the sync file. The full file with imports, schemas, helpers, and types is in the demo repo.

1. The createSync declaration and phase dispatcher. A string-only CheckpointSchema carries the phase, the basic-list cursor, and the watermark across runs.

const CheckpointSchema = z.object({
    phase: z.string(),
    after: z.string(),
    lastmodifieddate: z.string()
});

const sync = createSync({
    description: 'Two-phase HubSpot contacts sync that slides past the 10k search cap.',
    version: '1.0.0',
    frequency: 'every hour',
    autoStart: true,
    checkpoint: CheckpointSchema,
    models: { HubspotContact: HubspotContactSchema },
    exec: async (nango) => {
        const checkpoint = await nango.getCheckpoint();
        const phase = checkpoint?.phase === 'incremental' ? 'incremental' : 'initial';
        let watermark = checkpoint?.lastmodifieddate || undefined;

        if (phase === 'initial') {
            watermark = await runInitialPhase(nango, checkpoint?.after, watermark);
        }
        await runIncrementalPhase(nango, watermark);
    }
});

Enter fullscreen mode Exit fullscreen mode

2. Initial phase: paginate through the basic list endpoint, save a checkpoint per page. No 10k cap on this endpoint, so a 200k-contact portal flows through across however many runs are needed.

const proxyConfig = {
    endpoint: '/crm/v3/objects/contacts',
    params: { limit: PAGE_LIMIT, properties: PROPERTIES.join(',') },
    paginate: {
        type: 'cursor' as const,
        cursor_name_in_request: 'after',
        cursor_path_in_response: 'paging.next.after',
        response_path: 'results',
        limit: PAGE_LIMIT,
        on_page: async ({ nextPageParam }) => {
            if (typeof nextPageParam === 'string' && nextPageParam) {
                await nango.saveCheckpoint({ phase: 'initial', after: nextPageParam, lastmodifieddate: watermark ?? '' });
            } else {
                // Backfill done. Switch to incremental on the next run.
                await nango.saveCheckpoint({ phase: 'incremental', after: '', lastmodifieddate: watermark ?? '' });
            }
        }
    },
    retries: 3
};

for await (const page of nango.paginate<HubspotContactRaw>(proxyConfig)) {
    const contacts = page.map(toContact);
    await nango.batchSave(contacts, 'HubspotContact');
    watermark = highestWatermark(contacts, watermark);
}

Enter fullscreen mode Exit fullscreen mode

3. Incremental phase: search filtered by watermark, ascending, checkpointed per page. This is the part that solves the 10k cap. filterWatermark defines the search; savedWatermark advances per page. When a paged request 400s past the 10k cap, catch the error, restart the search from the latest watermark, and continue inside the same run until a slice returns fewer than 10k results.

let filterWatermark = savedWatermark;

while (true) {
    const filterGroups = [{
        filters: [{
            propertyName: 'lastmodifieddate',
            operator: 'GT',
            value: toMillisecondsString(filterWatermark)  // HubSpot wants Unix milliseconds
        }]
    }];

    let after: string | undefined;
    let hitTenKCap = false;

    while (true) {
        let response;
        try {
            response = await nango.post<SearchResponse>({
                endpoint: '/crm/v3/objects/contacts/search',
                data: {
                    filterGroups,
                    sorts: [{ propertyName: 'lastmodifieddate', direction: 'ASCENDING' }],
                    properties: [...PROPERTIES],
                    limit: PAGE_LIMIT,
                    ...(after ? { after } : {})
                },
                retries: 3
            });
        } catch (err) {
            // Past the 10k cap, HubSpot returns a 400. Restart the search
            // from the latest watermark inside the same run.
            if (isTenKCapError(err)) {
                hitTenKCap = true;
                break;
            }
            throw err;
        }

        const contacts = response.data.results.map(toContact);
        if (contacts.length === 0) return;

        await nango.batchSave(contacts, 'HubspotContact');
        savedWatermark = highestWatermark(contacts, savedWatermark);

        await nango.saveCheckpoint({ phase: 'incremental', after: '', lastmodifieddate: savedWatermark ?? '' });

        after = response.data.paging?.next?.after;
        if (!after) return;
    }

    if (!hitTenKCap) return;
    filterWatermark = savedWatermark;  // Slide the search window forward and loop.
}

Enter fullscreen mode Exit fullscreen mode

A few design decisions worth calling out:

  • Checkpoints saved per page. The initial phase uses nango.paginate with an on_page callback; the incremental phase saves inline after each batchSave. Either way, if the function is interrupted, the next run resumes from the saved state rather than starting over.
  • Ascending sort on lastmodifieddate. Each search returns the lowest-watermark records first. When a request 400s past the 10k cap, the latest saved watermark is already past everything we just processed, so restarting the search from it picks up the next slice without gaps. The outer loop keeps sliding the window forward until a slice fits under 10k.
  • Milliseconds, not ISO, for the search filter. HubSpot's search endpoint expects lastmodifieddate as Unix milliseconds. toMillisecondsString handles the conversion.

Step 4: Deploy

Deploy just this sync to your Nango dev environment:

nango deploy --sync fetch-contacts dev

Enter fullscreen mode Exit fullscreen mode

Nango CLI terminal output after deploying the fetch-contacts sync to the dev environment

The sync now runs every hour for every connected HubSpot portal, starting with the initial backfill and automatically transitioning to the incremental phase once the backfill catches up.

Step 5: Handle sync-completion webhooks in your backend

When a sync run finishes with new records, Nango sends a sync-completion webhook to your backend with counts of added, updated, and deleted records, plus a modifiedAfter timestamp. Set the webhook URL in the Nango dashboard under Environment Settings > Webhooks. The demo app shows a working Express backend and an HTML dashboard that displays the synced contacts.

Setting the sync-completion webhook URL in Nango Environment Settings

Then fetch only the records that changed:

const records = await nango.listRecords({
    providerConfigKey: 'hubspot',
    connectionId,
    model: 'HubspotContact',
    modifiedAfter: webhookPayload.modifiedAfter,
});

Enter fullscreen mode Exit fullscreen mode

Tip: Always verify the webhook signature before processing using nango.verifyIncomingWebhookRequest(). Nango signs payloads with HMAC-SHA256, and verification requires the raw request body (before JSON parsing).

nango.listRecords is paginated. Loop through next_cursor until it returns null, so you do not silently drop records on portals with very high modification rates.

Local testing tip: To inspect webhooks during development, run ngrok against your local backend and set the ngrok URL as the webhook target in Nango's environment settings.

Pasting the ngrok URL as the webhook target in Nango environment settings for local development

Step 6: Connect a new HubSpot account and test

With the sync deployed, connect a new HubSpot portal through Nango's auth UI. The sync starts immediately because of autoStart: true.

nango-live-sync

To verify the sync is doing the right thing:

  • Check the dashboard. The Nango dashboard shows per-run logs, record counts, current checkpoint, and durations. You should see an initial-phase run that each process all the contacts, then incremental runs that process only modified contacts.
    Nango sync logs

  • Watch the checkpoint. During the initial phase, the checkpoint shows phase: "initial" with an advancing after cursor. Once the backfill completes, the phase flips to "incremental" and the lastmodifieddate watermark starts moving forward.

  • Modify a contact in HubSpot. Within the next hour, the incremental sync should pick it up, and your backend should receive a sync-completion webhook with addedOrUpdated: 1.

Edge cases worth knowing about

  • Custom properties. Update the prompt with the custom property names and rerun the AI Function Builder skill. For per-customer property lists, store them in connection metadata and read them during the sync.
  • Deletes. Search does not return deleted contacts. Subscribe to HubSpot's contact.deletion webhook. Nango can forward those webhooks to your backend after attributing each event to the right connection.

Conclusion

Syncing large amounts of contacts from HubSpot requires both of HubSpot's contact endpoints. Search filters by lastmodifieddate but caps at 10,000 results. The basic list endpoint has no cap but cannot filter. A reliable sync uses both, with checkpoints that resume cleanly across runs and durable infrastructure underneath: OAuth refresh, retries, rate limit handling, and webhook routing.

With Nango, the sync logic is a code function your AI coding agent generates from a prompt, while the infrastructure is managed for you. The same approach works for HubSpot companies, deals, tickets, and any of Nango's 700+ supported APIs.

Related reading: