To integrate HubSpot with Lovable, create a HubSpot Private App to get an access token, store it in Cloud → Secrets, then build Edge Functions that proxy HubSpot CRM API calls for contacts, companies, deals, and form submissions. Unlike Salesforce's enterprise complexity, HubSpot has a generous free tier, straightforward REST API, and is ideal for startups building CRM-backed features into their Lovable apps.
Add CRM power to your Lovable app with HubSpot's API and Edge Functions
HubSpot is the most accessible CRM for startups and growing businesses integrating with Lovable. Its free tier supports unlimited users with contacts, companies, deals, and basic marketing tools at no cost — making it the right starting point for early-stage products. The HubSpot CRM API v3 is well-documented, uses straightforward REST conventions, and requires only a Private App access token for authentication (no complex OAuth2 flows for server-to-server integration).
The most common Lovable + HubSpot integration pattern is syncing form submissions to HubSpot as contacts. When a visitor fills out a contact form, demo request, or newsletter signup on your Lovable app, the Edge Function creates or updates a HubSpot contact with their information. This gives your sales team immediate visibility into leads without manually exporting data from a spreadsheet.
Beyond contact creation, the HubSpot CRM API lets you manage deals (opportunities), companies, and custom objects — making it possible to build CRM-powered dashboards directly in Lovable. A sales team might use a Lovable app to see their HubSpot pipeline, update deal stages, log notes, and view company details — essentially building a custom CRM UI on top of HubSpot's data layer.
HubSpot's Private App authentication is simple and secure: you create a Private App in your HubSpot portal, select the API scopes you need, and receive a long-lived access token. This token is used as a Bearer token in all API requests. Unlike OAuth2 user flows, Private App tokens do not expire unless you manually rotate them, making them ideal for server-to-server Edge Function integrations.
Integration method
HubSpot has no native Lovable connector. CRM operations are proxied through Lovable Edge Functions that call the HubSpot CRM API v3. The Private App access token (HubSpot's recommended authentication method) is stored in Cloud → Secrets and accessed via Deno.env.get(), keeping the token server-side and preventing CORS issues from direct browser calls.
Prerequisites
- A Lovable project with at least one deployed app (Edge Functions require Lovable Cloud)
- A HubSpot account (free CRM at hubspot.com — the free tier is sufficient for most Lovable integrations)
- Admin or Super Admin access to your HubSpot portal to create Private Apps
- Basic familiarity with HubSpot concepts: contacts, companies, deals, properties, and pipelines
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 recommended authentication method for server-to-server integrations like Lovable Edge Functions is Private Apps. Private Apps give you a scoped access token with exactly the permissions you specify, without requiring OAuth2 user authorization flows. To create a Private App: log in to HubSpot and go to Settings (gear icon in the top-right navigation). In the Settings sidebar, scroll to Integrations → Private Apps. Click 'Create a private app'. Give your app a descriptive name like 'Lovable CRM Integration'. In the Scopes tab, select the permissions your Edge Functions will need: - For contact management: crm.objects.contacts.read and crm.objects.contacts.write - For company management: crm.objects.companies.read and crm.objects.companies.write - For deals: crm.objects.deals.read and crm.objects.deals.write - For forms/tickets: tickets (if using Service Hub) - For viewing pipeline stages: crm.schemas.contacts.read, crm.schemas.deals.read Select only the scopes you need — the principle of least privilege applies here too. Click 'Create app'. HubSpot will display your access token — a long string starting with 'pat-na1-' or 'pat-eu1-' depending on your data center. Copy it immediately. HubSpot Private App tokens do not expire automatically, but you should rotate them periodically (every 90 days is a good practice). You can regenerate the token in Settings → Private Apps → your app → Auth → Token → Regenerate token.
Pro tip: Choose only the scopes your integration actually needs. Requesting 'all scopes' is convenient but creates unnecessary risk — if the token is ever compromised, an attacker has access to your entire HubSpot portal.
Expected result: A HubSpot Private App is created with a scoped access token. The token starts with 'pat-na1-' or 'pat-eu1-'. You have copied the token value ready to add to Lovable's secrets.
Add the HubSpot access token to Cloud Secrets
Add the HubSpot access token to Cloud Secrets
Store your HubSpot Private App access token in Lovable's Cloud Secrets panel. This token is the equivalent of a password for your HubSpot portal — anyone with the token can read and write CRM data within the scopes you configured. It must stay server-side, never appearing in your React frontend code or Git repository. In Lovable, click the '+' icon at the top of the editor to open the Cloud panel. Click the 'Secrets' tab. Click 'Add new secret'. Name: HUBSPOT_ACCESS_TOKEN Value: your HubSpot Private App access token (the full string starting with 'pat-') Click Save. If you plan to integrate with multiple HubSpot portals (for example, a multi-tenant app where each customer has their own HubSpot), store each token separately: HUBSPOT_TOKEN_CUSTOMER_A, HUBSPOT_TOKEN_CUSTOMER_B. The Edge Function can then select the appropriate token based on the requesting user's tenant. You may also want to add your HubSpot Portal ID as a secret (HUBSPOT_PORTAL_ID) — it is the number visible in your HubSpot URL (e.g., app.hubspot.com/contacts/12345678). Some HubSpot API operations require the portal ID.
Pro tip: Your HubSpot Portal ID is visible in every HubSpot URL when you are logged in — it is the large number after /contacts/, /deals/, etc. in the URL bar. Store it as HUBSPOT_PORTAL_ID for convenience.
Expected result: HUBSPOT_ACCESS_TOKEN (and optionally HUBSPOT_PORTAL_ID) appear in Cloud → Secrets with masked values.
Create the HubSpot contact sync Edge Function
Create the HubSpot contact sync Edge Function
Create the Edge Function that creates or updates HubSpot contacts from your Lovable app's forms. The function implements the create-or-update (upsert) pattern: it first tries to create the contact, and if HubSpot returns a 409 conflict (contact already exists with that email), it updates the existing contact instead. HubSpot CRM API v3 uses REST conventions. Contacts are at /crm/v3/objects/contacts. The create endpoint is POST with a 'properties' object containing HubSpot property names (email, firstname, lastname, company, phone, message, etc.). HubSpot property names are lowercase with underscores — 'firstname' not 'firstName', 'lifecyclestage' not 'lifecycleStage'. This trips up many developers. For the upsert pattern, when HubSpot returns 409, the response body includes the existing contact's ID. You then PATCH to /crm/v3/objects/contacts/{contactId} with the updated properties. The code below shows the complete create-or-update pattern. A useful shortcut: HubSpot provides a native upsert endpoint that handles create-or-update automatically. POST to /crm/v3/objects/contacts/upsert with an 'idProperty' of 'email' and the HubSpot API handles the conflict resolution. This is cleaner than the create-then-409-then-update pattern.
Create an Edge Function called hubspot-contact at supabase/functions/hubspot-contact/index.ts. Accept a POST with { email, firstName, lastName, company, phone, message, lifecycleStage }. Use HUBSPOT_ACCESS_TOKEN from secrets. Use the HubSpot CRM API v3 upsert endpoint to create or update the contact by email. Map firstName to 'firstname', lastName to 'lastname' (HubSpot property names are lowercase). Return the contact ID and whether it was created or updated. Include CORS headers.
Paste this in Lovable chat
1import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'23const corsHeaders = {4 'Access-Control-Allow-Origin': '*',5 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',6}78serve(async (req) => {9 if (req.method === 'OPTIONS') {10 return new Response('ok', { headers: corsHeaders })11 }1213 try {14 const {15 email,16 firstName = '',17 lastName = '',18 company = '',19 phone = '',20 message = '',21 lifecycleStage = 'lead',22 } = await req.json()2324 const accessToken = Deno.env.get('HUBSPOT_ACCESS_TOKEN')25 if (!accessToken) {26 throw new Error('HUBSPOT_ACCESS_TOKEN secret is not configured')27 }2829 if (!email) {30 return new Response(31 JSON.stringify({ error: 'email is required' }),32 { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }33 )34 }3536 // Use HubSpot's upsert endpoint for create-or-update by email37 const payload = {38 inputs: [39 {40 idProperty: 'email',41 id: email,42 properties: {43 email,44 firstname: firstName,45 lastname: lastName,46 company,47 phone,48 message, // Note: 'message' must be a property that exists in your HubSpot portal49 lifecyclestage: lifecycleStage,50 },51 },52 ],53 }5455 const response = await fetch('https://api.hubapi.com/crm/v3/objects/contacts/upsert', {56 method: 'POST',57 headers: {58 Authorization: `Bearer ${accessToken}`,59 'Content-Type': 'application/json',60 },61 body: JSON.stringify(payload),62 })6364 const data = await response.json()6566 if (!response.ok) {67 throw new Error(`HubSpot API error ${response.status}: ${JSON.stringify(data.message || data)}`)68 }6970 const result = data.results?.[0]71 return new Response(72 JSON.stringify({73 success: true,74 contactId: result?.id,75 created: result?.new,76 }),77 { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }78 )79 } catch (error) {80 return new Response(81 JSON.stringify({ error: error.message }),82 { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }83 )84 }85})Pro tip: HubSpot property names are all lowercase with underscores — 'firstname' not 'firstName', 'lifecyclestage' not 'lifecycleStage'. This is the most common reason HubSpot API calls succeed (200 OK) but properties do not appear in the CRM.
Expected result: The hubspot-contact Edge Function is deployed in Cloud → Edge Functions. Test it from the Edge Functions panel with a sample contact payload and verify the contact appears in HubSpot → Contacts.
Wire contact sync to your Lovable forms
Wire contact sync to your Lovable forms
Connect the Edge Function to the forms in your Lovable app where lead capture should happen. Common integration points are: contact forms, demo request forms, pricing page CTAs, newsletter signups (when used alongside Mailchimp for the bulk email), and account registration flows. Use supabase.functions.invoke() to call the Edge Function from your React form submission handler. The call should be non-blocking — your form's success flow (showing a thank you message, redirecting to a confirmation page) should not wait for HubSpot sync to complete. Wrap the invoke in a try-catch and continue the flow even if HubSpot sync fails. For forms that collect user data at registration time, add the HubSpot sync call alongside your Supabase Auth createUser call. After Supabase creates the account, call HubSpot in the background to create the CRM contact. This ensures your sales team is notified of every new user without blocking the registration flow. You can also trigger HubSpot contact creation from a Supabase database trigger — when a row is inserted into your users table, a database function calls the HubSpot Edge Function. This pattern works well for capturing contacts from any insertion path, not just your UI forms.
On my contact form page, after the form validates and submits to Supabase, also call the hubspot-contact Edge Function with the form data mapped to email, firstName, lastName, company, and message. Show a loading state during submission. On success, show a confirmation message. Handle errors silently (log them but don't show error to user if HubSpot sync fails). Set lifecycleStage to 'lead'.
Paste this in Lovable chat
1import { supabase } from '@/lib/supabase'23const syncToHubSpot = async (formData: ContactFormData) => {4 try {5 const { error } = await supabase.functions.invoke('hubspot-contact', {6 body: {7 email: formData.email,8 firstName: formData.firstName,9 lastName: formData.lastName,10 company: formData.company || '',11 message: formData.message || '',12 lifecycleStage: 'lead',13 },14 })15 if (error) console.error('HubSpot sync failed:', error)16 } catch (err) {17 // Non-blocking — log but do not disrupt user flow18 console.error('HubSpot sync error:', err)19 }20}2122// In your form submit handler:23const handleSubmit = async (formData: ContactFormData) => {24 // 1. Primary action: save to Supabase25 await supabase.from('contact_submissions').insert(formData)2627 // 2. Secondary action: sync to HubSpot (non-blocking)28 syncToHubSpot(formData) // Note: no await — fire and forget2930 // 3. Continue user flow regardless31 setSubmitted(true)32}Pro tip: Use 'fire and forget' (no await) for HubSpot sync calls in your UI — user-facing forms should complete immediately without waiting for CRM sync. Log failures for monitoring but never show CRM sync errors to end users.
Expected result: When a user submits the form, a new contact appears in HubSpot → Contacts within seconds. The form submission continues successfully even if the HubSpot call encounters an error.
Common use cases
Sync contact form submissions to HubSpot CRM as leads
When a visitor submits your Lovable contact form, demo request, or newsletter signup, an Edge Function creates or updates a HubSpot contact with their email, name, and any custom properties. If the contact already exists in HubSpot, the function updates their record instead of creating a duplicate. Your sales team sees every lead in HubSpot in real time.
Create an Edge Function called hubspot-contact that accepts a POST with { email, firstName, lastName, company, message, source } and creates or updates a HubSpot contact using the CRM API v3. Use HUBSPOT_ACCESS_TOKEN from secrets. If the contact already exists (409 response), update their record instead. Map source to the hubspot_owner_id or a custom lifecycleStage property. Return the contact ID on success.
Copy this prompt to try it in Lovable
Build a CRM dashboard showing HubSpot pipeline in Lovable
Create a deals pipeline view in your Lovable app that fetches HubSpot deals, associated contacts, and deal stages via Edge Functions. Display deals as Kanban columns by deal stage, with the ability to move deals between stages by calling the HubSpot update deal API. This gives your team a custom CRM interface on top of HubSpot data.
Create an Edge Function called hubspot-deals that fetches all active deals from HubSpot with associated contact names, deal amounts, and deal stages using HUBSPOT_ACCESS_TOKEN from secrets. Return deals grouped by pipeline stage. Then create a Kanban board UI in Lovable with columns for each deal stage, showing deal name, contact, and amount. Add drag-to-move functionality that calls a hubspot-update-deal Edge Function to update the stage.
Copy this prompt to try it in Lovable
Log customer support tickets as HubSpot service tickets
When a user submits a support request in your Lovable app, create a HubSpot Service Hub ticket via the Edge Function, associate it with the user's existing HubSpot contact, and set the ticket priority based on the issue type. Your support team works the ticket in HubSpot while users track status in your Lovable app.
Create an Edge Function that creates a HubSpot ticket when a user submits a support form in my app. Accept { userEmail, subject, description, priority } in the request body. Use HUBSPOT_ACCESS_TOKEN from secrets. First find the HubSpot contact by email (or create if not found), then create a ticket associated with that contact. Set the hs_ticket_priority based on the priority parameter. Return the ticket ID.
Copy this prompt to try it in Lovable
Troubleshooting
HubSpot API returns 403 Forbidden with 'You don't have permission to perform this operation'
Cause: The Private App access token does not have the required scope for the API endpoint being called. For example, reading contacts requires crm.objects.contacts.read; writing contacts requires crm.objects.contacts.write.
Solution: In HubSpot → Settings → Private Apps → your app → Scopes, add the missing scope. After updating scopes, regenerate the access token (the existing token does not automatically get new scopes). Update the HUBSPOT_ACCESS_TOKEN secret in Cloud → Secrets with the new token value, then redeploy your Edge Function.
Contacts are created in HubSpot but properties like company or phone are missing
Cause: HubSpot property names in the API payload are incorrect. HubSpot uses all-lowercase property names with underscores: 'firstname' not 'firstName', 'company' (correct as-is), 'phone' (correct), 'lifecyclestage' not 'lifecycleStage'.
Solution: In HubSpot → CRM → Contacts → Properties, find the internal name (not the label) of each property you want to set. The internal name is the value used in API calls. Common mappings: first name = 'firstname', last name = 'lastname', lifecycle stage = 'lifecyclestage', company name = 'company'. For custom properties, the internal name is set when the property is created and shown in the property settings.
1// Correct HubSpot property names in your payload:2properties: {3 email: email,4 firstname: firstName, // NOT firstName5 lastname: lastName, // NOT lastName6 company: company,7 phone: phone,8 lifecyclestage: 'lead', // NOT lifecycleStage9}HubSpot upsert returns 400 with 'PROPERTY_DOESNT_EXIST' error
Cause: The payload includes a property name that does not exist in your HubSpot portal. For example, 'message' is not a default HubSpot contact property — it needs to be created as a custom property first.
Solution: In HubSpot, go to Settings → CRM → Properties → Contact Properties → Create property. Create a custom text property named 'message' (or whatever custom field you need). Alternatively, remove the non-existent property from your API payload and map the data to an existing property like 'hs_lead_status' or a notes field. Check your portal's property list at Settings → CRM → Properties before adding properties to API payloads.
Edge Function returns 'HUBSPOT_ACCESS_TOKEN secret is not configured' error
Cause: The secret was added after the Edge Function's last deployment, or the secret name has a typo.
Solution: In Cloud → Secrets, verify the secret name is exactly HUBSPOT_ACCESS_TOKEN (case-sensitive). If the name is correct, trigger a redeployment by asking Lovable in chat to 'add a comment to the hubspot-contact Edge Function to redeploy it'. Secrets are injected at deploy time and require redeployment to take effect.
Best practices
- Create HubSpot Private Apps with the minimum required scopes — never use 'all scopes' or Super Admin tokens for API integrations.
- Use HubSpot's upsert endpoint (POST to /objects/contacts/upsert) rather than separate create and update flows — it is simpler, atomic, and handles duplicates correctly.
- Always validate that HubSpot property names in your API payload match the internal names in your HubSpot portal — labels and internal names often differ, and HubSpot silently ignores unknown property names rather than returning an error.
- For contact form integrations, make HubSpot sync non-blocking (fire and forget) so that CRM sync failures never disrupt the user experience.
- Store your HubSpot Portal ID as HUBSPOT_PORTAL_ID in secrets alongside the access token — some HubSpot API operations and form embed codes require it.
- Rotate your Private App access token every 90 days as a security best practice — in HubSpot Settings → Private Apps, click 'Regenerate token' and update the secret in Cloud → Secrets.
- For complex integrations involving multiple HubSpot objects (contacts + companies + deals), create a separate Edge Function per object type rather than one monolithic proxy — this keeps each function focused and easier to debug.
Alternatives
Choose Salesforce if your organization already uses it and needs deeper enterprise customization, complex process automation, and Apex triggers — at the cost of significantly more complex API integration.
Choose Salesforce Commerce Cloud if your primary need is headless e-commerce storefront rather than CRM contact and deal management.
Choose Mailchimp if your primary goal is email list management and campaigns rather than full CRM with deals, companies, and sales pipeline tracking.
Frequently asked questions
Does HubSpot have a free tier that works for a Lovable integration?
Yes — HubSpot's free CRM tier is genuinely capable and free forever. It includes unlimited contacts, companies, and deals; one deal pipeline; basic contact properties; and Private App API access for integrations. The free tier is sufficient for most Lovable integration use cases: contact syncing, deal tracking, and CRM dashboards. Paid tiers (Starter from $15/month, Professional from $90/month) add marketing automation, custom reporting, and advanced CRM features.
What is the difference between a HubSpot Private App token and an OAuth token?
A Private App token is a static access token you generate for a specific set of scopes in your HubSpot portal. It does not expire automatically, does not require a user to authorize it, and is ideal for server-to-server integrations like Lovable Edge Functions. An OAuth token is obtained through a user authorization flow (user logs in to HubSpot and approves permissions) and requires token refresh handling. For integrations where you own the HubSpot portal, Private Apps are simpler. OAuth is appropriate when your app integrates with multiple customers' HubSpot portals.
Can I read HubSpot form submissions instead of creating contacts from Lovable forms?
Yes — you can use HubSpot Forms API to read form submission data via Edge Functions, or you can use HubSpot Webhooks to have HubSpot push form submission events to your Lovable Edge Function receiver. However, the most common Lovable + HubSpot pattern is the reverse: create HubSpot contacts from Lovable's own forms rather than reading HubSpot form submissions. HubSpot Forms are hosted on HubSpot and do not have the custom styling and UX capabilities that Lovable provides.
How do I associate a contact with a company and deal in a single API call?
HubSpot's API requires separate calls to create associations between objects. First create the contact, then create the company, then create the deal, and finally create associations using the Associations API (POST to /crm/v3/objects/{fromObjectType}/{fromObjectId}/associations/{toObjectType}/{toObjectId}/{associationTypeId}). This requires multiple Edge Function API calls in sequence. For complex multi-object creation flows, RapidDev's team can help design the right Edge Function architecture to handle associations reliably.
Will HubSpot sync break if the user's email already exists in my CRM?
Not with the upsert pattern shown in this guide. The upsert endpoint handles the 'already exists' case automatically — if a contact with that email exists, HubSpot updates it; if not, HubSpot creates it. You never get a duplicate contact error with upsert. The upsert also returns a 'new' boolean indicating whether the contact was just created, so you can differentiate new leads from returning users in your Edge Function response.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation