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
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
Create a HubSpot Private App and get your access token
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.
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
1# .env2# HubSpot Private App token — server-side only, never use NEXT_PUBLIC_ prefix3# Get this from HubSpot Settings → Integrations → Private Apps → Create4HUBSPOT_ACCESS_TOKEN=pat-na1-your_token_here56# 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=12345678Pro 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.
Create a HubSpot API service and contact management routes
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).
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
1// lib/hubspot.ts2const HUBSPOT_BASE = 'https://api.hubapi.com';34async 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');78 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 });1617 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 }2122 return res.json();23}2425export 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}3435export 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 matches41 }),42 });43}4445export 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}5354export 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}6465export 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.
Embed the HubSpot tracking script and Forms API
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.
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
1// components/HubSpotContactForm.tsx2'use client';3import { useState } from 'react';45interface FormData {6 firstname: string;7 lastname: string;8 email: string;9 company: string;10}1112export function HubSpotContactForm() {13 const [formData, setFormData] = useState<FormData>({14 firstname: '', lastname: '', email: '', company: ''15 });16 const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');1718 async function handleSubmit(e: React.FormEvent) {19 e.preventDefault();20 setStatus('loading');2122 const portalId = process.env.NEXT_PUBLIC_HUBSPOT_PORTAL_ID;23 const formGuid = process.env.NEXT_PUBLIC_HUBSPOT_FORM_GUID;2425 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 );3738 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 }4849 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 }5253 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.
Build a deals pipeline dashboard with React
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.
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
1// app/api/hubspot/deals/route.ts2import { NextResponse } from 'next/server';3import { listDeals } from '@/lib/hubspot';45export 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;910 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 };2324 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 }));3132 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.
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.
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.
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.
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.
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
Choose Salesforce over HubSpot if your organization uses Salesforce as the system of record and needs enterprise-grade CRM capabilities with custom objects, Apex code, and complex workflow automation.
Choose Zoho CRM if you need a cost-effective HubSpot alternative with a broader feature set at lower pricing, especially if you use other Zoho products (Zoho Books, Desk, Campaigns).
Choose Pipedrive over HubSpot if your primary use case is sales pipeline management — Pipedrive's UI and API are more focused on deal tracking and less on marketing automation.
Choose Freshsales if you want a HubSpot alternative with built-in phone, email sequencing, and AI-powered lead scoring at a lower price point.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation