To use HubSpot CRM with V0, generate your contacts or deals UI in V0, then create a Next.js API route at app/api/hubspot/route.ts that uses HubSpot's Node.js SDK to read and write CRM records. Store your HubSpot Private App access token in Vercel Dashboard → Settings → Environment Variables as HUBSPOT_ACCESS_TOKEN. V0 handles the UI scaffolding; your API route handles all CRM operations server-side so the token is never exposed to the browser.
Building a CRM Interface for HubSpot with V0
HubSpot is far more than a marketing tool — it is a full CRM platform that tracks contacts, manages deal pipelines, handles support tickets, and logs every customer interaction. For founders building internal tools, client portals, or custom sales dashboards with V0, the HubSpot API gives you programmatic access to all of this data. You can build a contact search page, a deal board showing pipeline stages, a ticket management view, or a custom reporting dashboard that pulls live data from HubSpot directly into your V0-generated Next.js app.
The integration follows V0's standard pattern for external services: V0 generates the React components and UI, and a Next.js API route handles all communication with HubSpot's API. This is important for security — HubSpot Private App access tokens have broad CRM access and must never be exposed in the browser. By keeping all HubSpot API calls in the server-side route handler, the token stays in Vercel's encrypted environment variables and is never sent to users' browsers.
HubSpot's v3 API covers Contacts, Companies, Deals, Tickets, Line Items, and custom objects through a consistent REST interface. The official @hubspot/api-client npm package wraps these endpoints with TypeScript types, making it straightforward to build on. One important distinction from simpler tools: HubSpot uses property-based data models where each object (contact, deal, etc.) has typed properties that you must specify explicitly when creating or updating records.
Integration method
V0 generates your CRM interface — contact lists, deal pipelines, or ticket dashboards — while a Next.js API route proxies all calls to HubSpot's API server-side, protecting your Private App access token. The API route runs as a serverless function on Vercel, reads your HubSpot token from environment variables, and returns structured CRM data to the React components V0 generated.
Prerequisites
- A V0 account with a Next.js project at v0.dev
- A HubSpot account (free CRM tier is sufficient for API access)
- A HubSpot Private App with the necessary CRM scopes (created in Settings → Integrations → Private Apps)
- A Vercel account with your V0 project deployed via GitHub
- Basic understanding of HubSpot object types: Contacts, Deals, Tickets, Companies
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 modern API uses Private Apps for server-to-server integrations. Unlike the older OAuth flow, a Private App gives you a static access token tied to a specific set of permission scopes — much simpler for internal tools and single-tenant integrations. To create a Private App, log into your HubSpot account and navigate to Settings (the gear icon in the top-right). In the left sidebar, scroll to Integrations and click Private Apps. Click Create a private app. Give it a name like 'V0 Integration' and a description. On the Scopes tab, select the permissions your app needs. For reading contacts and deals you need crm.objects.contacts.read and crm.objects.deals.read. For creating or updating records, add the corresponding write scopes. For tickets, add crm.objects.tickets.read and crm.objects.tickets.write. After configuring scopes, click Create app. HubSpot will show you a long access token starting with pat-na1- (or a similar regional prefix). Copy this token immediately and store it somewhere secure — HubSpot will not show it again in full. This token is what your Next.js API route will use to authenticate all requests to HubSpot. Important: never put this token in your V0 chat messages or in any code you commit to GitHub. It grants broad CRM access and should only ever live in environment variables. HubSpot's Private App tokens do not expire on their own, but you can rotate them at any time from the Private Apps settings page.
Pro tip: Grant only the scopes your app needs. If you only need to read contacts for a lookup tool, do not add write scopes. Least-privilege access limits the blast radius if the token is ever accidentally exposed.
Expected result: You have a HubSpot Private App access token (starting with pat-na1- or similar) saved securely. The token has the correct CRM scopes for your use case.
Generate Your CRM Interface with V0
Generate Your CRM Interface with V0
With your authentication plan in place, use V0 to generate the front-end components for your CRM feature. V0 excels at building data tables, Kanban boards, form UIs, and dashboard layouts — exactly the kinds of interfaces that make CRM data useful. The key is to tell V0 the exact API endpoints your components should call so the fetch logic is wired up correctly from the start. For a contact list, ask V0 for a table component that fetches from /api/hubspot/contacts. For a deal pipeline, ask for a Kanban board that groups data from /api/hubspot/deals by a stage field. For a lead form, specify the POST endpoint and the fields you need. V0 will generate TypeScript React components with Tailwind CSS styling, including loading states and basic error handling. Review the generated fetch calls in the component code. For GET requests, the pattern should be fetching from your API route with optional query parameters. For POST requests (creating records), the component should serialize form data to JSON and POST it to your API route. Make sure V0's generated components handle the response correctly — for list data, they should map over an array; for creation, they should show a success or error message based on the API response status. If V0 generates placeholder hardcoded data instead of actual fetch calls, prompt it again: 'Update this component to fetch real data from /api/hubspot/contacts instead of using static mock data.' V0 can iterate quickly on the data-fetching logic.
Create a contact management dashboard with a data table that fetches from /api/hubspot/contacts. Show columns for First Name, Last Name, Email, Company, Lifecycle Stage, and Last Activity Date. Include a search bar that filters by name or email, a loading skeleton while data loads, and an empty state message if no contacts are returned. Add a Create Contact button that opens a modal form.
Paste this in V0 chat
Pro tip: Ask V0 to include TypeScript interfaces for your HubSpot data shapes (contact properties, deal fields). This makes the code easier to maintain and helps catch property name mismatches early.
Expected result: V0 generates a polished CRM interface with fetch calls pointed at your API routes. The components handle loading, error, and empty states cleanly.
Create the HubSpot API Route
Create the HubSpot API Route
Now build the server-side API route that your V0 components will call. Create the file app/api/hubspot/route.ts in your project. This serverless function runs on Vercel's infrastructure, reads your HubSpot access token from environment variables, and proxies CRM requests to HubSpot's API. Install the official HubSpot Node.js SDK by adding @hubspot/api-client to your package.json dependencies. This package provides typed clients for every HubSpot object type (contacts, deals, companies, tickets) and handles authentication by accepting your access token in the constructor. The API route below handles two operations: a GET request that fetches contacts with optional search filtering, and a POST request that creates a new contact. HubSpot's v3 API uses a properties object pattern — when reading a contact, you specify which properties you want returned; when creating a contact, you set values in a properties object matching HubSpot's internal property names. One critical detail with HubSpot's contacts API: property names are lowercase with underscores, not camelCase. For example, the contact's first name is 'firstname' (one word, no underscore), the last name is 'lastname', and the email is 'email'. Company name is 'company', phone number is 'phone', and lifecycle stage is 'lifecyclestage'. These exact property names must match what HubSpot expects — mismatches silently return empty values rather than throwing an error. For deals, the key properties are 'dealname', 'amount', 'closedate', 'dealstage', and 'pipeline'. The dealstage value must match the internal ID of the stage in your HubSpot pipeline, not the display name. You can find stage IDs in HubSpot Settings → Objects → Deals → Pipelines.
Add a Next.js API route at app/api/hubspot/route.ts that uses the @hubspot/api-client package. For GET requests, fetch up to 50 contacts with properties firstName, lastName, email, company, and lifecycleStage using the HUBSPOT_ACCESS_TOKEN environment variable. For POST requests, create a new contact from the request body. Return contacts as a JSON array.
Paste this in V0 chat
1import { Client } from '@hubspot/api-client';2import { NextRequest, NextResponse } from 'next/server';34const hubspot = new Client({5 accessToken: process.env.HUBSPOT_ACCESS_TOKEN,6});78export async function GET(request: NextRequest) {9 try {10 const { searchParams } = new URL(request.url);11 const query = searchParams.get('q');1213 let contacts;1415 if (query) {16 // Search contacts by email or name17 const searchResponse = await hubspot.crm.contacts.searchApi.doSearch({18 query,19 limit: 50,20 properties: ['firstname', 'lastname', 'email', 'company', 'lifecyclestage', 'lastmodifieddate'],21 filterGroups: [],22 sorts: [],23 after: 0,24 });25 contacts = searchResponse.results;26 } else {27 // Fetch all contacts (paginated)28 const response = await hubspot.crm.contacts.basicApi.getPage(29 50,30 undefined,31 ['firstname', 'lastname', 'email', 'company', 'lifecyclestage', 'lastmodifieddate']32 );33 contacts = response.results;34 }3536 const formatted = contacts.map((c) => ({37 id: c.id,38 firstName: c.properties.firstname || '',39 lastName: c.properties.lastname || '',40 email: c.properties.email || '',41 company: c.properties.company || '',42 lifecycleStage: c.properties.lifecyclestage || '',43 lastModified: c.properties.lastmodifieddate || '',44 }));4546 return NextResponse.json({ contacts: formatted });47 } catch (error: unknown) {48 console.error('HubSpot GET error:', error);49 return NextResponse.json(50 { error: 'Failed to fetch contacts' },51 { status: 500 }52 );53 }54}5556export async function POST(request: NextRequest) {57 try {58 const body = await request.json();59 const { firstName, lastName, email, company, phone } = body;6061 if (!email) {62 return NextResponse.json(63 { error: 'Email is required' },64 { status: 400 }65 );66 }6768 const contact = await hubspot.crm.contacts.basicApi.create({69 properties: {70 firstname: firstName || '',71 lastname: lastName || '',72 email,73 company: company || '',74 phone: phone || '',75 },76 associations: [],77 });7879 return NextResponse.json(80 { id: contact.id, message: 'Contact created successfully' },81 { status: 201 }82 );83 } catch (error: unknown) {84 console.error('HubSpot POST error:', error);85 return NextResponse.json(86 { error: 'Failed to create contact' },87 { status: 500 }88 );89 }90}Pro tip: Add a separate route at app/api/hubspot/deals/route.ts for deal-specific operations to keep your API routes organized and single-purpose.
Expected result: The API route returns a JSON array of contact objects when called with GET. A POST request with email creates a new contact and returns its HubSpot ID.
Add Environment Variables in Vercel
Add Environment Variables in Vercel
Your API route reads HUBSPOT_ACCESS_TOKEN from environment variables. You need to add this to Vercel so the serverless function can authenticate with HubSpot when your app is deployed. Open your Vercel Dashboard and navigate to your project. Click the Settings tab, then click Environment Variables in the left sidebar. In the form, enter HUBSPOT_ACCESS_TOKEN as the key and paste your Private App token (the long string starting with pat-na1-) as the value. Set the environment scope to Production, Preview, and Development so the route works across all deployments. Important: do not add the NEXT_PUBLIC_ prefix to this variable. The NEXT_PUBLIC_ prefix would expose the value in the browser's JavaScript bundle, making your access token visible to anyone who inspects your app's network requests or source code. The HubSpot access token must only be readable server-side in the API route. If your app needs to display the HubSpot portal ID (a non-sensitive identifier used in some embed scenarios), you can add NEXT_PUBLIC_HUBSPOT_PORTAL_ID with the appropriate prefix since it is not a secret. Your portal ID appears in the URL when you are logged into HubSpot, for example hub.hubspot.com/contacts/12345678 — the number is the portal ID. After saving the environment variables, trigger a new deployment by pushing any commit to your GitHub repository or clicking Redeploy in the Vercel Dashboard. Environment variable changes only take effect after redeployment. For local development, add HUBSPOT_ACCESS_TOKEN to your .env.local file (which should already be in your .gitignore).
Pro tip: Vercel lets you set different values per environment scope. Consider using a separate HubSpot sandbox account's token for Preview deployments so test data does not pollute your production CRM.
Expected result: Vercel Dashboard shows HUBSPOT_ACCESS_TOKEN saved as an environment variable. After redeployment, calling /api/hubspot/contacts returns real HubSpot data instead of an authentication error.
Extend to Deals and Tickets
Extend to Deals and Tickets
Once contacts are working, extending the integration to deals and tickets follows the same pattern. Create additional API routes — app/api/hubspot/deals/route.ts for deals and app/api/hubspot/tickets/route.ts for tickets — each using the hubspot.crm.deals and hubspot.crm.tickets client namespaces from the same @hubspot/api-client package. For deals, the critical properties are dealname, amount, closedate, dealstage, and pipeline. The dealstage must be an internal ID, not the display name. To find stage IDs programmatically, call hubspot.crm.pipelines.pipelinesApi.getAll('deals') which returns all pipelines and their stage objects with both the label (display name) and the ID you need for filtering and creating deals. For a Kanban pipeline board, fetch all open deals and group them client-side by the dealstage property. Your V0-generated Kanban component should accept a deals array and render columns for each stage. Ask V0 to generate the Kanban layout with drag-and-drop reordering if you need that interactivity — V0 can scaffold @dnd-kit/core for this purpose. For tickets, the key properties are subject, content, hs_ticket_priority, and hs_pipeline_stage. Tickets also have associations to contacts and companies, which you can fetch by including associations in the API request or by making a follow-up call to hubspot.crm.associations.v4.basicApi. For complex integrations that require webhooks from HubSpot (for real-time CRM sync), custom object support, or multi-portal setups, RapidDev's team can help architect the full solution including webhook verification and database synchronization.
Add a deals pipeline view to the dashboard. Create a Kanban board that fetches from /api/hubspot/deals and groups deals into columns by their stage. Each deal card should show the deal name, amount formatted as currency, and expected close date. The columns should be labeled Prospecting, Qualification, Proposal, and Closed Won.
Paste this in V0 chat
1import { Client } from '@hubspot/api-client';2import { NextRequest, NextResponse } from 'next/server';34const hubspot = new Client({5 accessToken: process.env.HUBSPOT_ACCESS_TOKEN,6});78export async function GET(request: NextRequest) {9 try {10 const { searchParams } = new URL(request.url);11 const stage = searchParams.get('stage');1213 const filters = stage14 ? [{ propertyName: 'dealstage', operator: 'EQ' as const, value: stage }]15 : [];1617 const response = await hubspot.crm.deals.searchApi.doSearch({18 limit: 100,19 properties: ['dealname', 'amount', 'closedate', 'dealstage', 'pipeline', 'hubspot_owner_id'],20 filterGroups: filters.length > 0 ? [{ filters }] : [],21 sorts: [{ propertyName: 'closedate', direction: 'ASCENDING' as const }],22 after: 0,23 });2425 const deals = response.results.map((d) => ({26 id: d.id,27 name: d.properties.dealname || 'Untitled Deal',28 amount: d.properties.amount ? parseFloat(d.properties.amount) : null,29 closeDate: d.properties.closedate || '',30 stage: d.properties.dealstage || '',31 pipeline: d.properties.pipeline || '',32 }));3334 return NextResponse.json({ deals });35 } catch (error: unknown) {36 console.error('HubSpot deals error:', error);37 return NextResponse.json(38 { error: 'Failed to fetch deals' },39 { status: 500 }40 );41 }42}Pro tip: HubSpot rate limits Private App calls to 100 requests per 10 seconds and 1,000 requests per day on free accounts. Add simple caching with a Map or use Vercel's Edge Config for frequently-read data.
Expected result: The deals API route returns grouped deal data. The V0-generated Kanban board renders deals in their correct pipeline stages.
Common use cases
Internal Sales Pipeline Dashboard
A sales manager uses V0 to build a custom pipeline view showing all open deals grouped by stage. The dashboard fetches deal records from HubSpot's Deals API, displays deal amount, owner, and close date, and lets managers filter by pipeline stage. Updates made in the dashboard can write back to HubSpot via POST requests.
Build a sales pipeline dashboard with a Kanban board layout showing deal stages: Prospecting, Qualification, Proposal, Closed Won, Closed Lost. Each card should show the deal name, amount in dollars, and expected close date. Fetch deals from /api/hubspot/deals and group them by stage property.
Copy this prompt to try it in V0
Contact Search and Lookup Tool
A support team builds a contact lookup page in V0 that lets agents search for customers by email or name, view their full HubSpot contact record, and see associated deals and tickets. The search calls a Next.js API route that queries HubSpot's Contacts API and returns enriched contact data.
Create a contact search page with a search bar that queries /api/hubspot/contacts?q=searchterm. Display results in a table with columns for name, email, phone, company, and lifecycle stage. Clicking a row should expand it to show recent activity and associated deals.
Copy this prompt to try it in V0
Lead Capture Form with CRM Write-Back
A marketing team embeds a lead capture form in their V0-generated landing page. On submission, the form POSTs to a Next.js API route that creates a new HubSpot contact and associates it with a specific pipeline stage, eliminating manual CRM entry for inbound leads.
Build a lead capture form with fields for first name, last name, email, company name, and a message. On submit, POST to /api/hubspot/contacts/create and show a success message. Include client-side validation that the email field is properly formatted before submitting.
Copy this prompt to try it in V0
Troubleshooting
API route returns 401 with 'authentication credentials are missing' from HubSpot
Cause: The HUBSPOT_ACCESS_TOKEN environment variable is not set in Vercel, or the token was not available at the time of the last deployment.
Solution: Go to Vercel Dashboard → your project → Settings → Environment Variables and verify HUBSPOT_ACCESS_TOKEN is present with the correct value. After adding or updating it, trigger a new deployment — environment variable changes require a redeploy to take effect.
Contacts are returned but all property values are empty strings or undefined
Cause: HubSpot's API only returns the properties you explicitly request. If you do not specify property names in the getPage or search call, HubSpot returns only the default minimal set.
Solution: Add the property names array to your API call: hubspot.crm.contacts.basicApi.getPage(50, undefined, ['firstname', 'lastname', 'email', 'company']). Use HubSpot's exact internal property names (lowercase, no camelCase) — check HubSpot Settings → Properties to see the internal names.
1// Specify which properties you want returned2const response = await hubspot.crm.contacts.basicApi.getPage(3 50,4 undefined, // after cursor5 ['firstname', 'lastname', 'email', 'company', 'lifecyclestage']6);Creating a contact returns 409 'Contact already exists with this email'
Cause: HubSpot enforces email uniqueness across contacts. If a contact with that email already exists in your CRM, the creation will fail.
Solution: Before creating, search for an existing contact with that email first. If found, use the update API (hubspot.crm.contacts.basicApi.update) instead of create. Alternatively, handle the 409 response in your API route and return a meaningful error to the UI.
1// Check for existing contact before creating2try {3 const existing = await hubspot.crm.contacts.basicApi.getById(4 email,5 ['email'],6 undefined,7 undefined,8 false,9 'email' // Use email as the ID type10 );11 // Contact exists — update instead12 await hubspot.crm.contacts.basicApi.update(existing.id, { properties });13} catch {14 // Contact not found — create it15 await hubspot.crm.contacts.basicApi.create({ properties, associations: [] });16}Module not found: Can't resolve '@hubspot/api-client' after deploying to Vercel
Cause: The @hubspot/api-client package is not listed in your package.json, so Vercel's build does not install it.
Solution: Add @hubspot/api-client to your package.json dependencies section. Commit the updated package.json and push to trigger a new Vercel build with the package installed.
1// package.json2{3 "dependencies": {4 "@hubspot/api-client": "^11.0.0",5 "next": "15.0.0",6 "react": "^19.0.0"7 }8}Best practices
- Create a dedicated HubSpot Private App for each application rather than sharing tokens across projects — this limits scope and makes token rotation easier.
- Request only the CRM scopes your app actually needs; avoid adding broad write permissions if you only need read access.
- Specify property names explicitly in every HubSpot API call to avoid receiving empty default property sets.
- Cache frequently-read data like pipeline stage definitions locally — these rarely change and fetching them on every request wastes rate limit quota.
- Handle HubSpot's 429 rate limit responses with exponential backoff retry logic, especially in high-traffic applications.
- Store only the minimum necessary data from HubSpot in your own database — treat HubSpot as the source of truth and fetch fresh data rather than duplicating records.
- Use HubSpot's webhook subscriptions for real-time sync instead of polling; create a dedicated API route to receive and verify HubSpot webhook payloads.
- Test with a HubSpot developer sandbox account (free to create at developers.hubspot.com) to avoid polluting your production CRM with test data.
Alternatives
Salesforce is an alternative if your organization requires enterprise-grade CRM with advanced customization, complex approval workflows, and large-scale sales operations that exceed HubSpot's capabilities.
Zoho CRM is an alternative if you need a HubSpot-comparable feature set at a lower price point, particularly for small teams that do not need HubSpot's marketing automation.
Pipedrive is an alternative if your primary use case is sales pipeline management with a simpler, deal-focused interface rather than HubSpot's broader marketing and CRM suite.
Frequently asked questions
Does V0 have a native HubSpot integration?
No, V0 does not have a one-click HubSpot integration like it does for Neon, Supabase, or Stripe through the Vercel Marketplace. You need to create a Next.js API route that uses HubSpot's Node.js SDK with a Private App access token. V0 can generate the UI components and API route boilerplate when you prompt it specifically.
What is a HubSpot Private App and why do I need one?
A Private App is HubSpot's recommended authentication method for server-to-server integrations. It gives you a static access token tied to specific CRM permission scopes, without requiring users to go through an OAuth flow. For internal tools built with V0, a Private App token is the simplest and most appropriate authentication approach. You create it in HubSpot Settings → Integrations → Private Apps.
Can I use the HubSpot API to write data back to the CRM from my V0 app?
Yes, HubSpot's v3 API supports full create, read, update, and delete operations on contacts, deals, companies, and tickets. In your Next.js API route, use the POST endpoint to create records and PATCH for updates. Make sure your Private App has the corresponding write scopes enabled — write access requires separate scope selection from read access when creating the Private App.
How do I handle HubSpot's API rate limits in my V0 app?
HubSpot limits requests to 100 per 10 seconds and 1,000 per day on free accounts (higher on paid plans). For typical internal dashboards, these limits are rarely hit. If you need higher throughput, implement simple server-side caching in your API route using an in-memory Map or a cache like Upstash Redis to avoid redundant HubSpot calls for data that does not change frequently.
Can I embed the HubSpot meeting scheduler in a V0-generated page?
Yes, HubSpot provides a JavaScript embed snippet for meeting scheduling links. In your V0 project, ask V0 to add a section with the HubSpot meetings embed. You paste in the HubSpot-provided script tag and the booking widget loads inline. This does not require an API route — it is a pure client-side embed. Make sure to add it inside a useEffect hook to avoid server-side rendering errors.
How is this different from the HubSpot Marketing Hub integration?
This tutorial covers the full HubSpot CRM — contacts, deals, tickets, and pipeline management. The HubSpot Marketing Hub integration focuses specifically on email campaigns, marketing automation, forms, and lead nurturing workflows. If you need both CRM and marketing features, your Private App will need scopes for both areas. Most founders building custom dashboards need the CRM API covered here.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation