Skip to main content
RapidDev - Software Development Agency
bolt-ai-integrationsBolt Chat + API Route

How to Integrate Bolt.new with HubSpot

HubSpot's REST API integrates with Bolt.new through a Next.js API route using a Private App access token. Store your token in .env, create API routes that call HubSpot's contacts, deals, and companies endpoints, and embed HubSpot's tracking script and Forms API for lead capture. Free CRM tier is available. Rate limit is 100 requests per 10 seconds.

What you'll learn

  • How to create a HubSpot Private App and get a scoped access token with the right CRM permissions
  • How to create and read HubSpot contacts, deals, and companies from a Next.js API route in Bolt.new
  • How to embed HubSpot's tracking script and capture leads using the HubSpot Forms API
  • How to build a CRM dashboard that displays deals pipeline and contact list in React
  • How to handle HubSpot's rate limits and pagination for large contact databases
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate18 min read25 minutesMarketingApril 2026RapidDev Engineering Team
TL;DR

HubSpot's REST API integrates with Bolt.new through a Next.js API route using a Private App access token. Store your token in .env, create API routes that call HubSpot's contacts, deals, and companies endpoints, and embed HubSpot's tracking script and Forms API for lead capture. Free CRM tier is available. Rate limit is 100 requests per 10 seconds.

HubSpot CRM Integration for Bolt.new Apps

HubSpot has become one of the most widely used CRM platforms among startups and SMBs, in large part because of its generous free tier. The free CRM includes unlimited contacts, deal tracking, a forms and landing page builder, and live chat — making it accessible for nearly any stage of business. For Bolt.new apps, HubSpot integration typically serves one of three purposes: capturing and enriching leads from the app into HubSpot (a marketing or SaaS product website), reading CRM data to display in an internal tool or client portal (a custom dashboard that surfaces HubSpot deals and contacts), or building a lightweight CRM experience custom-tailored to a specific vertical.

HubSpot's REST API is clean, well-documented, and uses standard HTTP conventions. Authentication uses Private App tokens — long-lived, scoped access tokens that you create per integration in the HubSpot developer portal. Unlike older OAuth flows that required public apps and multi-step authorization, Private Apps give you a token immediately that you can use for server-to-server integrations. This token approach is ideal for Bolt.new because you store it in .env and it never needs to be refreshed or rotated unless you choose to.

The most important technical consideration is rate limiting. HubSpot's free and Starter tiers allow 100 API requests per 10 seconds, which is more than enough for interactive dashboards but requires care when syncing large datasets. For bulk operations (importing thousands of contacts), use HubSpot's Bulk API or batch endpoints rather than individual CRUD calls. This guide covers the full integration: creating a Private App with the right scopes, setting up API routes in Bolt.new, building a lead capture form using HubSpot's Forms API, and displaying CRM data in a React dashboard.

Integration method

Bolt Chat + API Route

HubSpot integrates with Bolt.new through a Next.js API route that uses a Private App access token for authentication. The token is stored in .env and only accessed server-side, keeping it out of the browser bundle. HubSpot's tracking script and Forms embed code load as regular client-side scripts in React. All CRM CRUD operations (create/read/update/delete for contacts, deals, and companies) go through the API route proxy.

Prerequisites

  • A HubSpot account (hubspot.com) — the free CRM tier is sufficient for contacts and deals integration; no credit card required
  • A HubSpot Private App access token created in Settings → Integrations → Private Apps with the required CRM scopes
  • Required scopes for the integration: crm.objects.contacts.read and crm.objects.contacts.write for contact management; crm.objects.deals.read for deal pipeline access
  • A Bolt.new project using Next.js (for the API route pattern that keeps the HubSpot token server-side)
  • Your HubSpot portal ID (a numeric ID visible in the HubSpot URL: app.hubspot.com/contacts/YOUR_PORTAL_ID) for the tracking script

Step-by-step guide

1

Create a HubSpot Private App and get your access token

HubSpot's Private Apps are the recommended authentication method for server-to-server integrations. Unlike OAuth apps that require a multi-step authorization flow and involve redirecting users, Private Apps generate a long-lived access token that you use directly — similar to an API key but with granular scope control. To create a Private App, log into HubSpot and go to Settings (the gear icon in the top navigation) → Integrations → Private Apps. Click 'Create a private app.' Give it a descriptive name like 'My Bolt App' so you can identify it later. On the Scopes tab, select the permissions your app needs. For CRM access, the key scopes are: crm.objects.contacts.read (read contacts), crm.objects.contacts.write (create and update contacts), crm.objects.deals.read (read deals), crm.objects.deals.write (create and update deals), crm.objects.companies.read (read companies), and forms (access HubSpot forms). Only select the scopes you actually need — least-privilege access is a security best practice. Click 'Create app.' HubSpot will show you the access token — a long string starting with 'pat-'. Copy it immediately. The token is only displayed once in full — if you miss it, you will need to rotate the token in the Private App settings to generate a new one. The access token you just copied goes in your Bolt.new project's .env file as HUBSPOT_ACCESS_TOKEN. This token is scoped to the permissions you selected and is tied to your HubSpot portal. Treat it like a password — do not share it or commit it to version control. If it is ever compromised, rotate it immediately in the Private App settings. Also note your HubSpot Portal ID for the tracking script. You can find it in the URL when logged into HubSpot (app.hubspot.com/contacts/YOUR_PORTAL_ID/) or under Settings → Account Setup → Account. Store this as NEXT_PUBLIC_HUBSPOT_PORTAL_ID in .env since it is safe to expose client-side.

Bolt.new Prompt

Add HubSpot environment variables to .env. Create the file with HUBSPOT_ACCESS_TOKEN=your_token_here (server-side, no NEXT_PUBLIC_ prefix) and NEXT_PUBLIC_HUBSPOT_PORTAL_ID=your_portal_id_here (client-side safe, used for tracking script). Show a comment explaining where to find each value in the HubSpot dashboard.

Paste this in Bolt.new chat

.env
1# .env
2# HubSpot Private App token server-side only, never use NEXT_PUBLIC_ prefix
3# Get this from HubSpot Settings Integrations Private Apps Create
4HUBSPOT_ACCESS_TOKEN=pat-na1-your_token_here
5
6# HubSpot Portal ID safe for client-side use (in tracking script)
7# Find this in your HubSpot URL: app.hubspot.com/contacts/YOUR_PORTAL_ID/
8NEXT_PUBLIC_HUBSPOT_PORTAL_ID=12345678

Pro tip: Select the minimum scopes needed for your integration. Adding unnecessary scopes increases the blast radius if the token is ever compromised. You can always edit the Private App and add more scopes later.

Expected result: You have a HubSpot Private App access token copied to your .env file and your portal ID noted for the tracking script. The token has the scopes needed for your planned integration.

2

Create a HubSpot API service and contact management routes

With credentials configured, build the HubSpot API service layer in Bolt.new. This consists of a reusable utility module (lib/hubspot.ts) that handles authentication headers and common API patterns, plus Next.js API routes for specific CRM operations. The HubSpot REST API base URL is https://api.hubapi.com. Authentication is added as a Bearer token in the Authorization header. The key v3 CRM endpoints are: POST /crm/v3/objects/contacts (create contact), GET /crm/v3/objects/contacts/{contactId} (get contact by ID), PATCH /crm/v3/objects/contacts/{contactId} (update contact), and POST /crm/v3/objects/contacts/search (search contacts by filter criteria). For upsert behavior (create if not exists, update if exists), use the crm/v3/objects/contacts endpoint with the idProperty parameter set to email — HubSpot will match on email address and update the existing contact rather than creating a duplicate. Paste the Bolt prompt below to scaffold the HubSpot service module and the contacts API routes. After Bolt generates the code, verify that lib/hubspot.ts reads from process.env.HUBSPOT_ACCESS_TOKEN and that no token value appears in any client-side file. Check that the contact creation route handles HubSpot's 409 Conflict response (which occurs when you try to create a contact with an email that already exists in the CRM) gracefully — the correct behavior in most cases is to update the existing contact rather than returning an error. HubSpot's API returns contacts with a properties object containing all the CRM field values. Standard contact properties include firstname, lastname, email, company, phone, and jobtitle. Custom properties you create in HubSpot's property settings are accessed by their internal name (a lowercase_snake_case string you define when creating the property).

Bolt.new Prompt

Create a HubSpot CRM integration for this Next.js app. (1) Create lib/hubspot.ts with a hubspotFetch(endpoint, options) utility that adds Authorization: Bearer {HUBSPOT_ACCESS_TOKEN} headers and handles errors. Export createContact(properties), updateContact(contactId, properties), getContact(contactId), searchContacts(filters), and listContacts(limit, after) functions using HubSpot v3 CRM API endpoints. (2) Create a Next.js API route at app/api/hubspot/contacts/route.ts that handles GET (list contacts with pagination) and POST (create/upsert contact by email). For POST, use idProperty: email for upsert behavior to avoid duplicates. Return standardized contact objects with id, email, firstName, lastName, company. Include TypeScript types throughout.

Paste this in Bolt.new chat

lib/hubspot.ts
1// lib/hubspot.ts
2const HUBSPOT_BASE = 'https://api.hubapi.com';
3
4async function hubspotFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
5 const token = process.env.HUBSPOT_ACCESS_TOKEN;
6 if (!token) throw new Error('HUBSPOT_ACCESS_TOKEN not configured');
7
8 const res = await fetch(`${HUBSPOT_BASE}${endpoint}`, {
9 ...options,
10 headers: {
11 Authorization: `Bearer ${token}`,
12 'Content-Type': 'application/json',
13 ...options.headers,
14 },
15 });
16
17 if (!res.ok) {
18 const error = await res.json().catch(() => ({ message: res.statusText }));
19 throw Object.assign(new Error(error.message ?? 'HubSpot API error'), { status: res.status, body: error });
20 }
21
22 return res.json();
23}
24
25export interface HubSpotContactProperties {
26 firstname?: string;
27 lastname?: string;
28 email?: string;
29 company?: string;
30 phone?: string;
31 jobtitle?: string;
32 [key: string]: string | undefined;
33}
34
35export async function createOrUpdateContact(properties: HubSpotContactProperties) {
36 return hubspotFetch('/crm/v3/objects/contacts', {
37 method: 'POST',
38 body: JSON.stringify({
39 properties,
40 // Upsert by email — updates existing contact if email matches
41 }),
42 });
43}
44
45export async function listContacts(limit = 10, after?: string) {
46 const params = new URLSearchParams({
47 limit: limit.toString(),
48 properties: 'firstname,lastname,email,company,phone,createdate',
49 ...(after && { after }),
50 });
51 return hubspotFetch(`/crm/v3/objects/contacts?${params}`);
52}
53
54export async function searchContacts(query: string) {
55 return hubspotFetch('/crm/v3/objects/contacts/search', {
56 method: 'POST',
57 body: JSON.stringify({
58 query,
59 properties: ['firstname', 'lastname', 'email', 'company'],
60 limit: 20,
61 }),
62 });
63}
64
65export async function listDeals(limit = 10, after?: string) {
66 const params = new URLSearchParams({
67 limit: limit.toString(),
68 properties: 'dealname,amount,dealstage,closedate,hubspot_owner_id',
69 ...(after && { after }),
70 });
71 return hubspotFetch(`/crm/v3/objects/deals?${params}`);
72}

Pro tip: HubSpot contact properties are case-sensitive and use snake_case names. The standard properties are firstname (not firstName), lastname, email, company, phone, jobtitle. Custom property names are whatever you defined when creating them in HubSpot Settings → Properties.

Expected result: lib/hubspot.ts provides authenticated access to HubSpot's CRM API. The /api/hubspot/contacts route lists existing contacts and creates new ones with upsert behavior. Submitting a contact form in the Bolt preview creates a real contact in your HubSpot CRM.

3

Embed the HubSpot tracking script and Forms API

Beyond the REST API, HubSpot provides two client-side tools that are extremely valuable for marketing integrations: the HubSpot tracking script (which tracks page visits and identifies visitors across sessions) and the Forms API (which lets you embed HubSpot-managed forms or programmatically submit to them). The HubSpot tracking script, called the HubSpot JavaScript API or hs-tracking-code.js, is a small async script that you add to your site's HTML head. It tracks page views, identifies contacts who fill out forms, and enables HubSpot's live chat widget if you have that configured. To add it in a Next.js app, use a Script component in your root layout with the src pointing to //js.hs-scripts.com/YOUR_PORTAL_ID.js. The portal ID comes from your .env file as NEXT_PUBLIC_HUBSPOT_PORTAL_ID. This script is completely client-side and safe to include since it does not expose any sensitive credentials. HubSpot Forms gives you two options for embedding forms. The simplest is the Embed Code: HubSpot generates a small JavaScript snippet in your Forms dashboard (Marketing → Forms → select a form → Actions → Share) that renders the form with full HubSpot styling and submission handling. Paste this snippet into a React component using useEffect to inject the form script — similar to the Calendly embed pattern. The programmatic approach uses the HubSpot Forms Submit API: POST to https://api.hsforms.com/submissions/v3/integration/submit/{portalId}/{formGuid} with a JSON payload containing the form fields. This approach lets you use your own custom React form while still submitting data to HubSpot's form system, which triggers HubSpot workflows, email notifications, and lead nurture sequences you have configured in HubSpot. The portal ID and form GUID are both public values — no access token required for form submissions, since this endpoint is designed for client-side use.

Bolt.new Prompt

Add two HubSpot features to this Next.js app: (1) Add the HubSpot tracking script to the root layout using Next.js Script component with strategy='afterInteractive'. Load it from //js.hs-scripts.com/{portalId}.js where portalId comes from NEXT_PUBLIC_HUBSPOT_PORTAL_ID. (2) Create a ContactForm component that submits to HubSpot's Forms API at https://api.hsforms.com/submissions/v3/integration/submit/{portalId}/{formGuid}. The form should have fields for firstname, lastname, email, and company. On submit, POST to the HubSpot Forms endpoint (no authentication needed for this public endpoint). Show success and error states. Add NEXT_PUBLIC_HUBSPOT_FORM_GUID=your_form_guid_here to .env.

Paste this in Bolt.new chat

components/HubSpotContactForm.tsx
1// components/HubSpotContactForm.tsx
2'use client';
3import { useState } from 'react';
4
5interface FormData {
6 firstname: string;
7 lastname: string;
8 email: string;
9 company: string;
10}
11
12export function HubSpotContactForm() {
13 const [formData, setFormData] = useState<FormData>({
14 firstname: '', lastname: '', email: '', company: ''
15 });
16 const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
17
18 async function handleSubmit(e: React.FormEvent) {
19 e.preventDefault();
20 setStatus('loading');
21
22 const portalId = process.env.NEXT_PUBLIC_HUBSPOT_PORTAL_ID;
23 const formGuid = process.env.NEXT_PUBLIC_HUBSPOT_FORM_GUID;
24
25 try {
26 const res = await fetch(
27 `https://api.hsforms.com/submissions/v3/integration/submit/${portalId}/${formGuid}`,
28 {
29 method: 'POST',
30 headers: { 'Content-Type': 'application/json' },
31 body: JSON.stringify({
32 fields: Object.entries(formData).map(([name, value]) => ({ name, value })),
33 context: { pageUri: window.location.href, pageName: document.title },
34 }),
35 }
36 );
37
38 if (res.ok) {
39 setStatus('success');
40 setFormData({ firstname: '', lastname: '', email: '', company: '' });
41 } else {
42 setStatus('error');
43 }
44 } catch {
45 setStatus('error');
46 }
47 }
48
49 if (status === 'success') {
50 return <div className="rounded-lg bg-green-50 p-6 text-green-800">Thanks! We will be in touch soon.</div>;
51 }
52
53 return (
54 <form onSubmit={handleSubmit} className="space-y-4">
55 <div className="grid grid-cols-2 gap-4">
56 <input required placeholder="First name" value={formData.firstname}
57 onChange={(e) => setFormData({ ...formData, firstname: e.target.value })}
58 className="rounded border p-2 w-full" />
59 <input required placeholder="Last name" value={formData.lastname}
60 onChange={(e) => setFormData({ ...formData, lastname: e.target.value })}
61 className="rounded border p-2 w-full" />
62 </div>
63 <input required type="email" placeholder="Email" value={formData.email}
64 onChange={(e) => setFormData({ ...formData, email: e.target.value })}
65 className="rounded border p-2 w-full" />
66 <input placeholder="Company" value={formData.company}
67 onChange={(e) => setFormData({ ...formData, company: e.target.value })}
68 className="rounded border p-2 w-full" />
69 <button type="submit" disabled={status === 'loading'}
70 className="w-full rounded bg-orange-500 py-2 font-medium text-white hover:bg-orange-600 disabled:opacity-50">
71 {status === 'loading' ? 'Submitting...' : 'Get in Touch'}
72 </button>
73 {status === 'error' && <p className="text-red-600 text-sm">Something went wrong. Please try again.</p>}
74 </form>
75 );
76}

Pro tip: The HubSpot Forms Submit API is a public endpoint designed for client-side use — it intentionally requires no authentication. Form submissions trigger HubSpot workflows and email notifications you configure in the HubSpot Forms editor, which is more powerful than a plain REST contact creation.

Expected result: The HubSpot tracking script loads on every page of the deployed app, tracking visits. The contact form submits to HubSpot's Forms API without needing an access token — contacts appear in HubSpot CRM within seconds of form submission.

4

Build a deals pipeline dashboard with React

For internal tools and CRM dashboards, displaying the HubSpot deals pipeline in your app's interface is one of the most valuable integration features. Sales teams can view their pipeline in a custom interface that matches the company's design system, integrated alongside other internal tools — without switching to HubSpot's default UI. The HubSpot Deals API returns deals with properties including dealname, amount, dealstage, closedate, and hubspot_owner_id. Deal stages are specific to your HubSpot pipeline — the default stages are 'appointmentscheduled,' 'qualifiedtobuy,' 'presentationscheduled,' 'decisionmakerboughtin,' 'contractsent,' 'closedwon,' and 'closedlost.' You can retrieve your pipeline's custom stage IDs from HubSpot Settings → CRM → Deals → Pipelines. For a Kanban-style pipeline view, group deals by their dealstage property and display each stage as a column. React state manages the current view (by stage, by owner, by expected close date). The amount field is returned as a string from HubSpot — parse it to a number and format as currency for display. Add pagination handling for large deal pipelines. HubSpot's list endpoints return up to 100 items per page with a paging.next.after cursor for the next page. The API route should accept an 'after' query parameter for cursor-based pagination, and the React component can implement infinite scroll or 'Load more' functionality using the returned cursor value.

Bolt.new Prompt

Build a deals pipeline dashboard that fetches deal data from /api/hubspot/deals. Create the API route at app/api/hubspot/deals/route.ts that calls HubSpot's deals endpoint, fetching dealname, amount, dealstage, closedate properties. In the React frontend, display deals grouped by pipeline stage as Kanban columns. Each deal card shows: deal name, formatted dollar amount, days until close date (colored red if overdue). Add a total pipeline value summary at the top showing sum of all open deals. Handle pagination with a 'Load more' button. Use Tailwind CSS with a clean table layout as an alternative to Kanban for mobile.

Paste this in Bolt.new chat

app/api/hubspot/deals/route.ts
1// app/api/hubspot/deals/route.ts
2import { NextResponse } from 'next/server';
3import { listDeals } from '@/lib/hubspot';
4
5export async function GET(request: Request) {
6 const { searchParams } = new URL(request.url);
7 const limit = parseInt(searchParams.get('limit') ?? '50');
8 const after = searchParams.get('after') ?? undefined;
9
10 try {
11 const data = await listDeals(limit, after) as {
12 results: Array<{
13 id: string;
14 properties: {
15 dealname: string;
16 amount: string;
17 dealstage: string;
18 closedate: string;
19 };
20 }>;
21 paging?: { next?: { after: string } };
22 };
23
24 const deals = data.results.map((deal) => ({
25 id: deal.id,
26 name: deal.properties.dealname,
27 amount: parseFloat(deal.properties.amount ?? '0'),
28 stage: deal.properties.dealstage,
29 closeDate: deal.properties.closedate,
30 }));
31
32 return NextResponse.json({
33 deals,
34 nextCursor: data.paging?.next?.after ?? null,
35 });
36 } catch (error) {
37 const message = error instanceof Error ? error.message : 'Unknown error';
38 return NextResponse.json({ error: message }, { status: 500 });
39 }
40}

Pro tip: HubSpot's deal amount is stored as a string, not a number. Always use parseFloat() or parseInt() when calculating totals, and format with Intl.NumberFormat for currency display in the React component.

Expected result: The deals dashboard displays your HubSpot pipeline grouped by stage. Deal cards show names, amounts, and close dates with color-coded urgency indicators. The total pipeline value summary updates based on the visible deals.

Common use cases

Lead Capture from Marketing Site to HubSpot CRM

A contact form or email signup on a marketing website that automatically creates a HubSpot contact with the submitted information. The contact is tagged with the form source and any UTM parameters from the URL. Uses the HubSpot Forms API or the v3 Contacts API depending on whether you want HubSpot's native form handling or full custom control.

Bolt.new Prompt

Add a lead capture form to this landing page that creates contacts in HubSpot. Create a form component with fields for first name, last name, email, company, and a message. On submit, call a Next.js API route at /api/hubspot/contacts that POSTs to HubSpot's contacts API (https://api.hubapi.com/crm/v3/objects/contacts) with HUBSPOT_ACCESS_TOKEN from process.env. Map form fields to HubSpot properties: firstname, lastname, email, company, message. Show a success message on submission. Add error handling for duplicate email addresses (HubSpot returns 409 for existing contacts). Use TypeScript and Tailwind CSS.

Copy this prompt to try it in Bolt.new

Internal CRM Dashboard

An internal team dashboard showing the HubSpot deals pipeline with deal names, amounts, stages, and close dates. Sales reps can view their pipeline at a glance in a custom interface that matches the company's internal tools design, rather than using HubSpot's default UI.

Bolt.new Prompt

Build a CRM pipeline dashboard that reads deal data from HubSpot. Create a Next.js API route at /api/hubspot/deals that calls https://api.hubapi.com/crm/v3/objects/deals?properties=dealname,amount,dealstage,closedate,hubspot_owner_id with HUBSPOT_ACCESS_TOKEN. Return deals grouped by pipeline stage. In the React frontend, display deals as Kanban-style columns (Appointment Scheduled, Qualified to Buy, Presentation Scheduled, Decision Maker Bought-In, Contract Sent, Closed Won, Closed Lost). Show deal name, formatted amount, and close date in each card. Use Tailwind CSS with a clean, minimal design.

Copy this prompt to try it in Bolt.new

Contact Enrichment on User Signup

When a new user signs up for a SaaS product, automatically create or update a HubSpot contact with their details and set custom properties indicating their signup source, plan, and usage tier. This syncs product usage data into HubSpot for marketing segmentation and sales outreach.

Bolt.new Prompt

Create a HubSpot contact sync service at lib/hubspot.ts that exports createOrUpdateContact(email, properties) and updateContactProperty(email, propertyName, value) functions. Both should call HubSpot's upsert endpoint (POST /crm/v3/objects/contacts with idProperty: email) using HUBSPOT_ACCESS_TOKEN from process.env. In the API route that handles user registration at app/api/auth/register/route.ts, call createOrUpdateContact after creating the user in the database to sync their data to HubSpot. Map these properties: email, firstname, lastname, signup_source, plan_tier. Add TypeScript types for HubSpot contact properties.

Copy this prompt to try it in Bolt.new

Troubleshooting

HubSpot API returns 401 Unauthorized on all requests

Cause: The HUBSPOT_ACCESS_TOKEN in .env is incorrect, has been rotated or deleted, or the Private App was deactivated. HubSpot Private App tokens start with 'pat-na1-' (US) or 'pat-eu1-' (EU) — using an older API key (which started with a different format) will not work.

Solution: Go to HubSpot Settings → Integrations → Private Apps. Verify your Private App still exists and is active. Click on the app, go to the Auth tab, and check the access token. If needed, click 'Rotate token' to generate a new one and update HUBSPOT_ACCESS_TOKEN in your .env file. Ensure the token value in .env has no extra spaces or line breaks.

HubSpot API returns 409 Conflict when creating a contact

Cause: A contact with that email address already exists in your HubSpot portal. HubSpot treats email as a unique identifier for contacts.

Solution: Use the upsert pattern instead of pure create. Send a POST to /crm/v3/objects/contacts with the idProperty: 'email' parameter in the query string to tell HubSpot to update the existing contact if the email matches. Alternatively, use PATCH to update an existing contact by its ID.

typescript
1// Upsert contact by email (create if not exists, update if exists)
2const res = await fetch(`${HUBSPOT_BASE}/crm/v3/objects/contacts?idProperty=email`, {
3 method: 'POST',
4 headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
5 body: JSON.stringify({ properties }),
6});

HubSpot rate limit error — 429 Too Many Requests

Cause: The HubSpot Free and Starter plans allow 100 API requests per 10 seconds (10 requests/second). If a dashboard component fetches multiple endpoint on load, or a sync operation makes rapid consecutive calls, it can exhaust this limit.

Solution: Implement request batching for bulk operations — use HubSpot's batch endpoints (/crm/v3/objects/contacts/batch/read) instead of individual GET requests for each contact. Add a delay between bulk operations. For dashboard applications, cache the API responses in React state or SWR and avoid refetching on every component mount.

typescript
1// Batch read contacts by IDs (one request instead of N)
2const batchRes = await hubspotFetch('/crm/v3/objects/contacts/batch/read', {
3 method: 'POST',
4 body: JSON.stringify({
5 inputs: contactIds.map((id) => ({ id })),
6 properties: ['firstname', 'lastname', 'email'],
7 }),
8});

HubSpot tracking script does not load in the Bolt WebContainer preview

Cause: HubSpot's tracking script (js.hs-scripts.com) may be blocked by ad-blocking browser extensions in the development environment, or the portal ID is missing or incorrect.

Solution: This is expected behavior in development — ad blockers commonly block analytics and tracking scripts. The tracking script works correctly on deployed sites. Verify NEXT_PUBLIC_HUBSPOT_PORTAL_ID is a pure numeric string (no quotes) in .env. Test the tracking script on the deployed Netlify or Bolt Cloud site where ad blockers are less likely to be active.

Best practices

  • Create HubSpot Private Apps with the minimum necessary scopes — only request read/write access for the CRM objects your app actually uses, not blanket access to all HubSpot data
  • Use HubSpot's upsert pattern (POST with idProperty=email) for contact creation to prevent duplicate contacts when the same email submits your form multiple times
  • Route all HubSpot Private App token usage through server-side API routes — the token gives access to your entire CRM, so it must never appear in client-side JavaScript
  • Use the HubSpot Forms Submit API (public endpoint, no token needed) for lead capture forms in React components — it triggers HubSpot workflows and notifications without exposing your Private App token
  • Implement response caching in your dashboard API routes for read-heavy operations like deal pipeline views — HubSpot data does not change faster than a human can act on it, so a 60-second cache significantly reduces API calls
  • Handle HubSpot pagination using the paging.next.after cursor for large contact or deal databases — the default page size is 10, so set a higher limit (up to 100) when building list views
  • Test your HubSpot integration thoroughly in development using HubSpot's sandbox or a test portal (Settings → Account → Sandboxes) before running code against your production CRM data

Alternatives

Frequently asked questions

Does HubSpot work with Bolt.new?

Yes. HubSpot's REST API uses standard HTTPS requests that work in Bolt's WebContainer via a Next.js API route. You store a HubSpot Private App token in .env, create API routes that proxy CRM requests, and build React components that fetch from those routes. The HubSpot tracking script and Forms API also work as client-side scripts loaded in the React app.

Do I need a paid HubSpot plan to integrate with Bolt.new?

No — HubSpot's free CRM tier includes full API access via Private Apps with contacts, deals, companies, and forms endpoints. The free tier has no credit card requirement and includes unlimited contacts. Paid plans (Starter, Professional, Enterprise) add advanced features like custom objects, marketing automation, and higher API rate limits, but the basic integration described in this guide works completely on the free plan.

How do I connect Bolt.new to HubSpot CRM?

Create a HubSpot Private App (Settings → Integrations → Private Apps) with the CRM scopes you need, copy the access token, add it to your .env file as HUBSPOT_ACCESS_TOKEN, and create Next.js API routes that call HubSpot's REST API endpoints with the token in the Authorization header. React components call your API routes — never HubSpot's API directly — to keep the token server-side.

Can I embed HubSpot forms in a Bolt.new app without an API key?

Yes. HubSpot's Forms Submit API is a public endpoint that accepts form submissions without any authentication. Use it to submit form data from React components directly to HubSpot — just include the public portal ID and form GUID (both visible in your HubSpot Forms settings). You can also embed HubSpot's pre-built form using its script embed code, which also requires no API token.

What is HubSpot's API rate limit and how do I handle it in Bolt.new?

HubSpot's free and Starter tiers allow 100 API requests per 10 seconds. For interactive dashboards with a few API calls per page load, this limit is rarely hit. For bulk operations (importing many contacts, syncing large datasets), use HubSpot's batch endpoints that process up to 100 records per request, and add delays between batch requests. Professional and Enterprise plans have higher rate limits.

How do I deploy a Bolt.new HubSpot app to Netlify?

Use Bolt's native Netlify integration (Settings → Applications) or push to GitHub and connect to Netlify. In Netlify's site Environment Variables settings, add HUBSPOT_ACCESS_TOKEN (server-side only) and NEXT_PUBLIC_HUBSPOT_PORTAL_ID (safe for client-side). The NEXT_PUBLIC_ prefix is required for Netlify to expose the portal ID in the browser bundle for the tracking script. Trigger a redeploy after adding variables.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.