# How to Integrate FlutterFlow with HubSpot CRM

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 40-55 min
- Compatibility: FlutterFlow Free+ (Firebase Cloud Functions required — Blaze plan)
- Last updated: March 2026

## TL;DR

Integrate FlutterFlow with HubSpot by building a Cloud Function that proxies API calls to HubSpot CRM v3 using a private app access token. Create contacts via POST /crm/v3/objects/contacts, list deals via GET /crm/v3/objects/deals, and update pipelines via PATCH. Mirror data to Firestore for offline access. Never use the deprecated HubSpot API key — use a Private App token created in HubSpot settings.

## Build a mobile HubSpot CRM companion in FlutterFlow for lead capture and deal management

Sales and marketing teams need mobile access to HubSpot — capturing leads immediately after a networking event, updating deal stages while in a customer meeting, or checking contact history before a call. FlutterFlow can power all of these use cases. HubSpot's CRM API v3 provides endpoints for contacts, companies, deals, tickets, and activities — all accessible via a Private App access token. This tutorial walks through creating the Private App, building a Cloud Function proxy, creating a lead capture form, building a deals pipeline view, and setting up HubSpot workflow webhooks to keep your Firestore data in sync.

## Before you start

- A HubSpot account (HubSpot CRM free tier works for basic contact and deal management)
- Firebase project with Cloud Functions enabled (Blaze plan)
- Node.js installed locally for Cloud Function development
- A FlutterFlow project where you want to add CRM features (lead capture form, deals dashboard, contact list)

## Step-by-step guide

### 1. Create a HubSpot Private App and get the access token

HubSpot deprecated API keys in November 2022 — all new integrations require a Private App access token. To create one: log into HubSpot → click your account name (top-right) → Settings (gear icon) → Integrations → Private Apps → Create private app. Give it a name (e.g., FlutterFlowMobile) and a description. Under Scopes, select the permissions your app needs: crm.objects.contacts.read, crm.objects.contacts.write, crm.objects.deals.read, crm.objects.deals.write, crm.objects.companies.read. Click Create app. HubSpot shows the access token once — copy it immediately. Store it in Firebase Functions config: firebase functions:config:set hubspot.access_token='pat-na1-YOUR_TOKEN'. The access token does not expire (unlike OAuth tokens) and is scoped to only the permissions you selected. If you need to add permissions later, edit the Private App scopes and a new token is generated. Revoke the old token from the Private Apps list when you generate a new one.

**Expected result:** Private App is created in HubSpot and the access token is stored in Firebase Functions config. The Private App shows the scopes: contacts read/write, deals read/write.

### 2. Build the HubSpot Cloud Function proxy

Create a Cloud Function named hubspot that accepts POST requests from FlutterFlow with an action parameter. The function uses the HubSpot access token to call HubSpot's CRM API v3 at https://api.hubapi.com. Actions to handle: getContacts (GET /crm/v3/objects/contacts with properties and limit), createContact (POST /crm/v3/objects/contacts), getDeals (GET /crm/v3/objects/deals with pipeline and stage), createDeal (POST /crm/v3/objects/deals), updateDeal (PATCH /crm/v3/objects/deals/{id}), associateContactToDeal (PUT /crm/v3/associations/contact/deal/{contactId}/to/{dealId}/contact_to_deal). After fetching data, write to Firestore for offline access.

```
// functions/index.js — HubSpot CRM proxy
// Install: cd functions && npm install axios
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const axios = require('axios');

admin.initializeApp();

const HS_BASE = 'https://api.hubapi.com';

function hubspotHeaders() {
  return {
    Authorization: `Bearer ${functions.config().hubspot.access_token}`,
    'Content-Type': 'application/json',
  };
}

exports.hubspot = functions.https.onRequest(async (req, res) => {
  res.set('Access-Control-Allow-Origin', '*');
  if (req.method === 'OPTIONS') { res.status(204).send(''); return; }

  const { action, contactId, dealId, data, limit = 50 } = req.body;
  const db = admin.firestore();
  const headers = hubspotHeaders();
  let result;

  try {
    if (action === 'getContacts') {
      const r = await axios.get(`${HS_BASE}/crm/v3/objects/contacts`, {
        headers,
        params: {
          limit,
          properties: 'firstname,lastname,email,phone,company,lifecyclestage',
          archived: false,
        },
      });
      const contacts = r.data.results.map((c) => ({
        id: c.id,
        firstName: c.properties.firstname || '',
        lastName: c.properties.lastname || '',
        email: c.properties.email || '',
        phone: c.properties.phone || '',
        company: c.properties.company || '',
        lifecycleStage: c.properties.lifecyclestage || 'lead',
      }));
      // Cache in Firestore
      const batch = db.batch();
      contacts.forEach((contact) => {
        batch.set(db.collection('hs_contacts').doc(contact.id), {
          ...contact, cachedAt: admin.firestore.FieldValue.serverTimestamp(),
        });
      });
      await batch.commit();
      result = contacts;

    } else if (action === 'createContact') {
      const r = await axios.post(`${HS_BASE}/crm/v3/objects/contacts`, {
        properties: {
          firstname: data.firstName,
          lastname: data.lastName,
          email: data.email,
          phone: data.phone,
          company: data.company,
          hs_lead_status: 'NEW',
          lifecyclestage: 'lead',
        },
      }, { headers });
      const created = {
        id: r.data.id,
        ...data,
        lifecycleStage: 'lead',
      };
      await db.collection('hs_contacts').doc(r.data.id).set({
        ...created, cachedAt: admin.firestore.FieldValue.serverTimestamp(),
      });
      result = created;

    } else if (action === 'getDeals') {
      const r = await axios.get(`${HS_BASE}/crm/v3/objects/deals`, {
        headers,
        params: {
          limit,
          properties: 'dealname,amount,dealstage,pipeline,closedate,hs_deal_stage_probability',
          archived: false,
        },
      });
      const deals = r.data.results.map((d) => ({
        id: d.id,
        name: d.properties.dealname || 'Untitled Deal',
        amount: parseFloat(d.properties.amount || 0),
        stage: d.properties.dealstage || 'appointmentscheduled',
        pipeline: d.properties.pipeline || 'default',
        closeDate: d.properties.closedate || null,
        probability: parseFloat(d.properties.hs_deal_stage_probability || 0),
      }));
      const batch = db.batch();
      deals.forEach((deal) => {
        batch.set(db.collection('hs_deals').doc(deal.id), {
          ...deal, cachedAt: admin.firestore.FieldValue.serverTimestamp(),
        });
      });
      await batch.commit();
      result = deals;

    } else if (action === 'updateDeal') {
      await axios.patch(
        `${HS_BASE}/crm/v3/objects/deals/${dealId}`,
        { properties: { dealstage: data.stage, amount: data.amount?.toString() } },
        { headers }
      );
      await db.collection('hs_deals').doc(dealId).update({
        stage: data.stage, cachedAt: admin.firestore.FieldValue.serverTimestamp(),
      });
      result = { success: true };
    }

    res.json({ success: true, data: result });
  } catch (err) {
    const msg = err.response?.data?.message || err.message;
    console.error('HubSpot CF error:', msg);
    res.status(500).json({ success: false, error: msg });
  }
});
```

**Expected result:** Cloud Function is deployed. A POST with {action: 'getContacts'} returns HubSpot contacts and writes them to the hs_contacts Firestore collection.

### 3. Configure FlutterFlow API Manager and build the lead capture form

In FlutterFlow, go to API Manager → Add API Group → name it HubSpotProxy. Set Base URL to your Cloud Function URL. Method: POST. Header: Content-Type: application/json. Add API Calls: getContacts with body {"action": "getContacts", "limit": 50}, createContact with body {"action": "createContact", "data": {"firstName": "[firstName]", "lastName": "[lastName]", "email": "[email]", "phone": "[phone]", "company": "[company]"}}. Test each call and verify the response. Build the lead capture form: add a page or bottom sheet with TextFields for first name, last name, company, email, and phone. Add a Page State variable for each field. The Submit Button Action Flow: call createContact API with the Page State values → on success show SnackBar 'Contact added to HubSpot' → navigate to the Contacts page or dismiss the sheet. This is the core use case: a salesperson meets someone at an event, opens the app, and creates a HubSpot contact in under 30 seconds.

**Expected result:** Lead capture form submits to the Cloud Function and creates a real HubSpot contact. The contact appears in HubSpot CRM within seconds.

### 4. Build the deals pipeline view with stage updating

Create a Deals page in FlutterFlow with a Backend Query reading from hs_deals Firestore collection, ordered by closeDate ascending. Display deals in a ListView showing: deal name (bold), amount (formatted as currency), stage (as a colored badge using Conditional Value), and close date. For stage updating: when the user taps a deal item, navigate to a DealDetail page passing the deal document reference as a parameter. On the DealDetail page, show all deal properties and add a DropDown for the deal stage populated with your HubSpot pipeline stages. The standard HubSpot default pipeline stages are: appointmentscheduled (Appointment Scheduled), qualifiedtobuy (Qualified To Buy), presentationscheduled (Presentation Scheduled), decisionmakerboughtin (Decision Maker Bought-In), contractsent (Contract Sent), closedwon (Closed Won), closedlost (Closed Lost). When the user selects a new stage from the DropDown: call updateDeal API with the deal ID and new stage, show a SnackBar 'Stage updated in HubSpot', and update the local Firestore hs_deals document.

**Expected result:** Deals page shows all deals with their current stages and amounts. Changing the stage DropDown on DealDetail updates the deal in HubSpot and locally in Firestore.

### 5. Receive HubSpot workflow webhooks to keep Firestore synced

HubSpot workflows can trigger webhooks when deal or contact properties change — for example, when a deal is marked Closed Won in the HubSpot web interface, trigger a webhook to your Cloud Function to update Firestore. Create a separate Cloud Function named hubspotWebhook to receive these events. Register it in HubSpot: your HubSpot account → Automation → Workflows → create or edit a workflow → Actions → Send a webhook → POST to your Cloud Function URL. The webhook payload includes the object type (contact or deal), the record ID, and the properties that changed. Your Cloud Function handler: verify the webhook signature using the HubSpot client secret (from Private App settings → Auth), extract the record ID, fetch the latest record from HubSpot API, and update the corresponding Firestore document. FlutterFlow's real-time Firestore listener picks up the change within 1-2 seconds — the deals pipeline view updates automatically when a deal moves stages in HubSpot.

**Expected result:** When a deal stage changes in HubSpot's web interface, the FlutterFlow app updates within 2-3 seconds via the webhook → Firestore → real-time listener chain.

## Complete code example

File: `HubSpot CRM Integration Architecture`

```text
HubSpot Private App Setup
==========================
Location: HubSpot → Settings → Integrations → Private Apps
Permissions needed:
  crm.objects.contacts.read + write
  crm.objects.deals.read + write
  crm.objects.companies.read
Token format: pat-na1-XXXXXXXX (North America)
Storage: firebase functions:config:set hubspot.access_token='pat-na1-...'

Cloud Function: hubspot (HTTPS onRequest)
==========================================
Actions:
  getContacts   → GET /crm/v3/objects/contacts
                   properties: firstname,lastname,email,phone,company
  createContact → POST /crm/v3/objects/contacts
                   properties: firstname,lastname,email,phone,company,lifecyclestage
  getDeals      → GET /crm/v3/objects/deals
                   properties: dealname,amount,dealstage,pipeline,closedate
  updateDeal    → PATCH /crm/v3/objects/deals/{id}
                   properties: dealstage, amount

Cloud Function: hubspotWebhook (HTTPS onRequest)
  Trigger: HubSpot Automation → Workflows → Send webhook
  Actions: update hs_contacts or hs_deals in Firestore

Firestore Cache Schema
=======================
hs_contacts/{hubspotId}
  id, firstName, lastName, email, phone, company
  lifecycleStage, cachedAt

hs_deals/{hubspotId}
  id, name, amount, stage, pipeline, closeDate
  probability, cachedAt

FlutterFlow Pages
==================
Contacts
  Backend Query: hs_contacts (Firestore)
  ListView → ContactCard (name, email, company, lifecycle badge)
  FAB → Lead Capture Bottom Sheet
    Fields: firstName, lastName, company, email, phone
    Submit → createContact → SnackBar

Deals
  Backend Query: hs_deals (Firestore, order by closeDate)
  ListView → DealCard (name, amount, stage badge, closeDate)
  Tap → DealDetail (param: dealRef)

DealDetail (param: dealRef)
  Backend Query: hs_deals/{dealId}
  DropDown (stage) → On Change → updateDeal API → update Firestore

HubSpot Default Pipeline Stages
=================================
appointmentscheduled  → Appointment Scheduled
qualifiedtobuy        → Qualified To Buy
presentationscheduled → Presentation Scheduled
decisionmakerboughtin → Decision Maker Bought-In
contractsent          → Contract Sent
closedwon             → Closed Won
closedlost            → Closed Lost
```

## Common mistakes

- **Using a HubSpot API key (the old-style token found in Settings → Integrations → API Key) instead of a Private App access token** — HubSpot deprecated all API keys on November 30, 2022. API keys no longer work for new API calls — calls authenticated with old API keys return 401 Unauthorized with the message 'This hapikey (API key) no longer has access to HubSpot APIs.' Any integration built before late 2022 using API keys must be migrated to Private App tokens. Fix: Create a Private App in HubSpot → Settings → Integrations → Private Apps. The Private App token starts with 'pat-'. Use it in the Authorization: Bearer header in your Cloud Function. The Private App token does not expire and is scoped to only the permissions you explicitly grant.
- **Calling HubSpot API endpoints directly from FlutterFlow's API Manager without a Cloud Function, putting the access token in the API Group headers** — The Private App access token in FlutterFlow's API Manager headers is embedded in the compiled mobile app binary. Anyone who decompiles the APK or IPA can extract the token and make HubSpot API calls with your permissions — reading all your CRM contacts, creating bulk fake contacts, or modifying deal data. Fix: Route all HubSpot API calls through a Cloud Function. Store the access token in Firebase Functions config (firebase functions:config:set) where it is only accessible in the server-side function code. The Cloud Function is the only component that knows the HubSpot token.
- **Not requesting the minimum required scopes when creating the Private App, or requesting all available scopes 'just in case'** — Requesting scopes you do not need (e.g., billing write, settings write) makes the Private App a higher-risk target if the token is ever compromised. Requesting too few scopes means your API calls return 403 Forbidden errors, which can be confusing to debug. Fix: Start with exactly the scopes you need: crm.objects.contacts.read + write for contact management, crm.objects.deals.read + write for deal management. Add scopes incrementally as you build new features. Each scope addition regenerates the token — update Firebase Functions config when this happens.

## Best practices

- Create the HubSpot Private App with minimum required scopes — start with contacts and deals only, add more as needed
- Store the Private App token exclusively in Firebase Functions config — never in FlutterFlow API Manager headers, app code, or Firestore documents
- Cache HubSpot contacts and deals in Firestore for instant display and offline access — HubSpot API calls add 300-800ms latency
- Use HubSpot property names exactly as documented — firstname not first_name, dealname not deal_name. Test each field name in HubSpot's API explorer at developers.hubspot.com/get-started
- Set up HubSpot workflow webhooks for high-frequency changes (deal stage updates, contact lifecycle changes) instead of polling — webhooks are real-time and free
- Log all HubSpot write operations (createContact, updateDeal) to a Firestore audit_log collection with userId and timestamp for accountability
- Use HubSpot's API rate limits as a guide: 100 requests per 10 seconds for most operations. With Firestore caching, you stay well below this limit

## Frequently asked questions

### What is the difference between HubSpot and Salesforce integrations in FlutterFlow?

Both require a Cloud Function proxy pattern. The key differences: HubSpot uses a Private App access token (no expiry, no OAuth flow required for simple integrations), while Salesforce requires OAuth 2.0 with token refresh every 2 hours. HubSpot's API is more developer-friendly with consistent v3 REST endpoints at api.hubapi.com. Salesforce's SOQL query language is more powerful for complex filtering. HubSpot has a generous free tier; Salesforce requires Professional or Enterprise for API access. For non-technical founders, HubSpot's integration is significantly simpler.

### Can I create HubSpot companies and associate contacts to them?

Yes — add action: 'createCompany' to your Cloud Function with POST /crm/v3/objects/companies (properties: name, domain, phone, city, industry). Then associate the company to a contact with PUT /crm/v3/associations/contact/company/{contactId}/to/{companyId}/contact_to_company. Companies appear in HubSpot's company records associated with the contact. Add getCompanies action to your Cloud Function with the same pattern as getContacts.

### How do I get notifications when someone submits a HubSpot form?

HubSpot form submissions can trigger a workflow webhook. Create a HubSpot workflow: Enrollment trigger → 'Contact submits a form' → select the form → Action: Send a webhook → your Cloud Function URL. The webhook payload includes all form field values and the contact's ID. Your Cloud Function can create a Firestore notification document, send a push notification via Firebase Cloud Messaging, or update a leads dashboard. This is useful for marketing landing pages that feed into a FlutterFlow sales tool.

### Can I sync HubSpot contacts bidirectionally with my FlutterFlow app's users collection?

Yes — implement a two-way sync. When a user signs up in your FlutterFlow app: the registration Action Flow calls createContact via your HubSpot Cloud Function, stores the returned HubSpot contact ID in the Firestore user document. When HubSpot updates the contact (via workflow webhook): the Cloud Function updates the Firestore user document with the new HubSpot lifecycle stage. When the user updates their profile in FlutterFlow: call updateContact via your Cloud Function. Store hubspotContactId in every Firestore user document as the link between systems.

### Is HubSpot CRM free tier sufficient for this integration?

HubSpot CRM free tier includes unlimited contacts, deals, and companies with full REST API access via Private Apps. The free tier limitations that might affect a FlutterFlow integration: no custom deal pipelines (only the default pipeline), limited workflow automation (basic triggers), no custom properties beyond the standard ones. HubSpot Starter ($20/user/month) adds deal stage customization, more properties, and email integration. For a basic lead capture and deal tracking app, the free tier is sufficient.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-integrate-flutterflow-with-hubspot
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-integrate-flutterflow-with-hubspot
