Skip to main content
RapidDev - Software Development Agency
flutterflow-integrationsFlutterFlow API Call

HubSpot

Connect FlutterFlow to HubSpot by routing HubSpot CRM v3 API calls through a Firebase Cloud Function or Supabase Edge Function that holds your Private App token. FlutterFlow calls the proxy to create contacts from in-app sign-up forms and read deal pipeline data. Unlike Retool — which has a native HubSpot connector — FlutterFlow requires a hand-built API Group and proxy setup.

What you'll learn

  • How to create a HubSpot Private App token scoped to only the CRM objects your app needs
  • Why the Private App token must live in a backend proxy rather than FlutterFlow
  • How to POST new contacts to HubSpot from a FlutterFlow sign-up form
  • How to use HubSpot's POST-based search endpoint (not GET) to query contacts with filter bodies
  • How to read deal pipeline stages into a FlutterFlow Data Type and display them in a dashboard
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate15 min read60 to 90 minutesMarketingLast updated July 2026RapidDev Engineering Team
TL;DR

Connect FlutterFlow to HubSpot by routing HubSpot CRM v3 API calls through a Firebase Cloud Function or Supabase Edge Function that holds your Private App token. FlutterFlow calls the proxy to create contacts from in-app sign-up forms and read deal pipeline data. Unlike Retool — which has a native HubSpot connector — FlutterFlow requires a hand-built API Group and proxy setup.

Quick facts about this guide
FactValue
ToolHubSpot
CategoryMarketing
MethodFlutterFlow API Call
DifficultyIntermediate
Time required60 to 90 minutes
Last updatedJuly 2026

Connect FlutterFlow to HubSpot for Lead Capture and Pipeline Visibility

HubSpot's CRM is the system of record for many sales and marketing teams — storing contacts, companies, deals, tickets, and email sequences. Connecting FlutterFlow to HubSpot lets you build two highly valuable features: (1) automatic lead capture — when a user signs up or fills in a form inside your mobile app, a HubSpot contact is created instantly without manual import; (2) pipeline visibility — sales reps can open the app on their phone and see their deal pipeline, contact details, and deal stages without opening a laptop. HubSpot's Free CRM tier covers basic contacts and deals — no paid plan required to start.

The key FlutterFlow-specific detail is authentication. HubSpot's recommended auth method for server-to-server integrations is a Private App token — a Bearer token you generate inside HubSpot Settings → Integrations → Private Apps. This token can be scoped to only the CRM objects your app needs (crm.objects.contacts.read, crm.objects.contacts.write, crm.objects.deals.read) which limits damage if it ever leaks. But it must never leak: a FlutterFlow API Call header containing this token ships inside the compiled Flutter binary and can be extracted. The token must live exclusively in a Firebase Cloud Function or Supabase Edge Function environment variable.

HubSpot's CRM v3 API has one non-obvious behavior that trips up most first-time integrators: the contact search endpoint is a POST request with a JSON filter body, not a GET request with query parameters. If you try to search contacts with a GET, you will get a 404. Rate limits are tight at 100 requests per 10 seconds on most plans — easy to exceed if you fire multiple API calls simultaneously on screen load or inside a Loop action. Design your FlutterFlow actions to batch reads and cache aggressively.

Integration method

FlutterFlow API Call

FlutterFlow has no native HubSpot connector — this integration is built entirely with FlutterFlow API Calls hitting a backend proxy. The HubSpot Private App token (Bearer) is a server secret that cannot live in a FlutterFlow API Call header because it would compile into the Flutter binary. A Firebase Cloud Function or Supabase Edge Function holds the token and forwards CRM requests to https://api.hubapi.com. FlutterFlow calls the proxy to POST new contacts from sign-up forms and to GET deals and pipeline stages for a CRM dashboard.

Prerequisites

  • A HubSpot account — the Free CRM tier is sufficient for contacts and deals
  • HubSpot Settings → Integrations → Private Apps access (Super Admin role required to create private apps)
  • A Firebase project with Cloud Functions on Blaze plan or a Supabase project with Edge Functions enabled
  • A FlutterFlow project with an authentication flow already set up
  • Basic understanding of JSON request bodies — HubSpot's contact create endpoint uses a 'properties' object wrapper

Step-by-step guide

1

Create a HubSpot Private App and generate a scoped Bearer token

In your HubSpot account, click the Settings gear icon (top right), then navigate to Integrations → Private Apps in the left sidebar. Click Create a private app. Give it a name like 'FlutterFlow Integration' and add a description. Switch to the Scopes tab — this is where you define exactly which CRM objects the token can access. For lead capture plus deal visibility you need: crm.objects.contacts.read, crm.objects.contacts.write, crm.objects.deals.read. If you plan to create deals from the app, add crm.objects.deals.write. Avoid adding scopes you do not need — the token's blast radius if leaked is limited to what you scope. Click Create App in the top right, confirm the dialog, and you will see your Private App access token — a long string starting with 'pat-'. Copy it immediately; you cannot see it again after leaving this screen (you can regenerate it if you lose it, but all existing integrations using the old token will break). This token is your Bearer authentication credential — it goes directly into your backend environment variable only. Also make a note of your HubSpot portal ID (visible in the URL when logged in, e.g., app.hubspot.com/contacts/PORTAL_ID/).

Pro tip: Use the most minimal scope set that covers your features — if you only need to create contacts and read deals, do not add email or marketing scopes. Smaller scope = less damage if the token is ever compromised.

Expected result: You have a HubSpot Private App access token (pat-XXXX) scoped to the CRM objects your app needs, stored securely in a password manager or secrets vault.

2

Deploy a Firebase Cloud Function proxy for the HubSpot CRM v3 API

Create a Firebase Cloud Function that accepts CRM operation requests from FlutterFlow, injects the Private App token from environment variables, and forwards them to https://api.hubapi.com. The function needs to handle at least three operations: create a contact (POST /crm/v3/objects/contacts), search contacts (POST /crm/v3/objects/contacts/search), and list deals (GET /crm/v3/objects/deals with optional filtering). Rather than building separate functions for each operation, use a routing approach: accept an 'operation' field in the request body that determines which HubSpot endpoint the proxy calls. Include CORS headers in every response so FlutterFlow web builds work without browser blocking. The rate limit is 100 requests per 10 seconds — for the FlutterFlow use case this is usually fine since the app does not batch many CRM calls simultaneously, but add a warning log when the proxy detects a 429 response from HubSpot. Deploy from your local machine using the Firebase CLI and copy the function's HTTPS trigger URL. Set the HubSpot token as an environment variable using Firebase Secret Manager or the config system — never hardcode it in the function source.

index.js
1// functions/index.js (Firebase Cloud Functions)
2const functions = require('firebase-functions');
3const fetch = require('node-fetch');
4
5const HUBSPOT_BASE = 'https://api.hubapi.com';
6
7exports.hubspotProxy = functions.https.onRequest(async (req, res) => {
8 res.set('Access-Control-Allow-Origin', '*');
9 res.set('Access-Control-Allow-Methods', 'POST, OPTIONS');
10 res.set('Access-Control-Allow-Headers', 'Content-Type');
11 if (req.method === 'OPTIONS') { res.status(204).send(''); return; }
12
13 const TOKEN = process.env.HUBSPOT_PRIVATE_APP_TOKEN;
14 const { operation, payload } = req.body;
15
16 const headers = {
17 'Authorization': `Bearer ${TOKEN}`,
18 'Content-Type': 'application/json'
19 };
20
21 let endpoint, method, body;
22
23 switch (operation) {
24 case 'create_contact':
25 endpoint = '/crm/v3/objects/contacts';
26 method = 'POST';
27 body = JSON.stringify({ properties: payload });
28 break;
29 case 'search_contacts':
30 endpoint = '/crm/v3/objects/contacts/search';
31 method = 'POST';
32 body = JSON.stringify(payload);
33 break;
34 case 'list_deals':
35 endpoint = '/crm/v3/objects/deals?properties=dealname,amount,closedate,dealstage';
36 method = 'GET';
37 break;
38 default:
39 res.status(400).json({ error: 'Unknown operation' });
40 return;
41 }
42
43 const hsRes = await fetch(`${HUBSPOT_BASE}${endpoint}`, { method, headers, body });
44 const data = await hsRes.json();
45 res.status(hsRes.status).json(data);
46});
47

Pro tip: Log hsRes.status inside the function — when HubSpot returns a 4xx error (invalid property name, rate limit, bad request), the error details are in the response body and much easier to debug from Cloud Function logs than from a FlutterFlow test panel timeout.

Expected result: The Cloud Function is deployed and returns a HubSpot contact object or deal list when called with the appropriate operation and payload.

3

Create a FlutterFlow API Group and wire the lead capture form

In FlutterFlow, open API Calls in the left nav → + Add → Create API Group. Name it HubSpotProxy and paste your Cloud Function URL as the Base URL. No Authorization headers at the group level — the token is server-side. Create an API Call inside the group named createContact. Set method to POST, leave the endpoint path blank, and in the Body tab select JSON. Add two variables: operation (String, default value 'create_contact') and payload (JSON/String — the properties object for the new contact). Your body template: { "operation": "{{operation}}", "payload": {{payload}} }. For the payload value, build a JSON object in your Action Flow that includes email, firstname, lastname, and any custom properties like hs_lead_status or a custom 'app_source' field. In Response & Test, provide a sample payload like { "email": "test@example.com", "firstname": "Test", "lastname": "User" } and click Test. A successful create returns HTTP 201 with the new contact object including its id. Now go to your sign-up success screen in FlutterFlow. In the Action Flow Editor for the completion action, add a Backend/API Call action selecting createContact. Build the payload using FlutterFlow's Set Variable actions to combine the user's form inputs into the properties JSON. Add a conditional check on the API response status (201 = success) and show a success snackbar. Handle non-201 responses with an error message so users know if the CRM write failed without crashing the app.

api_call_body.json
1{
2 "operation": "{{operation}}",
3 "payload": {{payload}}
4}

Pro tip: HubSpot contact properties use snake_case internal names (firstname, lastname, email, phone, company) — not display names like 'First Name'. Check the HubSpot Property Settings page to find the internal name of any custom properties you want to set.

Expected result: Submitting the sign-up form creates a HubSpot contact visible in the HubSpot Contacts dashboard within seconds.

4

Search contacts and read deals into a dashboard Data Type

Create a second API Call in the HubSpotProxy group named searchContacts. Set method to POST. The body variables: operation (String, default 'search_contacts') and payload (String — the HubSpot search filter body). The HubSpot search endpoint uses a POST with a JSON body containing filterGroups, sorts, properties, and limit. A common search payload for finding a contact by email: { "filterGroups": [{ "filters": [{ "propertyName": "email", "operator": "EQ", "value": "{{email}}" }] }], "properties": ["email", "firstname", "lastname", "phone"], "limit": 10 }. This is a POST request with a filter body — not a GET with query parameters. In FlutterFlow, set the payload variable to this JSON string with the email from a search text field. Create a third API Call named listDeals with operation 'list_deals'. Create a Data Type called HubSpotDeal with fields: dealId (String), dealName (String), amount (String), closeDate (String), dealStage (String). Map the JSON Paths from the deals response: $.results[*].id, $.results[*].properties.dealname, $.results[*].properties.amount, $.results[*].properties.closedate, $.results[*].properties.dealstage. Build a Deals screen in FlutterFlow with a ListView bound to the listDeals Backend Query. Inside each ListView item, show the deal name, amount formatted as currency, and a colored badge for the deal stage. To keep the deal stage badge colors accurate, maintain a small JSON lookup in App State mapping HubSpot stage IDs to human-readable labels and display colors.

search_contacts_body.json
1{
2 "filterGroups": [{
3 "filters": [{
4 "propertyName": "email",
5 "operator": "EQ",
6 "value": "{{email}}"
7 }]
8 }],
9 "properties": ["email", "firstname", "lastname", "phone", "company"],
10 "limit": 10
11}

Pro tip: The search endpoint uses POST — not GET. A GET request to /crm/v3/objects/contacts/search returns a 404. This is the most common first-try mistake with HubSpot's CRM API.

Expected result: The Deals screen lists open deals from HubSpot with name, amount, and stage, and the contact search returns matching results when a user types an email address.

5

Add rate-limit-safe caching for contact and deal reads

HubSpot's rate limit is 100 requests per 10 seconds on most plans. For a single user opening a deals screen this is fine — but if multiple sales reps use the app simultaneously and each screen load triggers multiple CRM reads (deals list, contact details, pipeline stages), you can easily approach the limit. The safest approach is to cache deal list responses in Firestore or Supabase with a 5-minute TTL. Deal data changes infrequently enough that 5-minute freshness is acceptable for a mobile sales tool. For contact searches (which users expect to be real-time), do not cache — always hit the live API. Add the caching logic inside your Cloud Function for the list_deals operation only: check a Firestore document for a recent deals snapshot and return it if it is under 5 minutes old. For bulk syncs — importing all HubSpot contacts into your app's own database — use a scheduled Cloud Function with sequential loops and a small delay between batches rather than on-device batching, which would burn through the rate limit instantly. If you need help building the bulk sync or a more sophisticated caching strategy for multi-rep deployments, RapidDev's team builds FlutterFlow integrations like this every week — free scoping call at rapidevelopers.com/contact.

index.js
1// Caching for list_deals operation inside hubspotProxy
2const admin = require('firebase-admin');
3if (!admin.apps.length) admin.initializeApp();
4const db = admin.firestore();
5
6// Inside the 'list_deals' case:
7const cacheRef = db.collection('hubspot_cache').doc('deals_list');
8const cached = await cacheRef.get();
9if (cached.exists) {
10 const { payload, cached_at } = cached.data();
11 if (Date.now() - cached_at < 5 * 60 * 1000) { // 5 minutes
12 res.json({ ...payload, from_cache: true });
13 return;
14 }
15}
16// Fetch from HubSpot, then cache:
17const data = await hsRes.json();
18await cacheRef.set({ payload: data, cached_at: Date.now() });
19res.json({ ...data, from_cache: false });
20

Pro tip: If your app has multiple sales reps each viewing their own deals (filtered by owner), cache deals per owner_id rather than globally so each rep's view is independently cached.

Expected result: Deal list loads serve from cache for most requests, reducing HubSpot API calls to one per 5 minutes per cached scope and keeping the integration well within the 100-requests/10-second limit.

Common use cases

Lead capture — push app sign-ups directly into HubSpot contacts

A SaaS company builds a FlutterFlow app where new user registrations automatically create HubSpot contacts with email, first name, last name, and a custom 'App Source' property. Sales reps in HubSpot see new app sign-ups appear in their contacts list within seconds without any CSV import.

FlutterFlow Prompt

After the user completes registration, call the HubSpot proxy to create a contact with their email, first name, last name, and set a custom property called 'app_source' to 'mobile_app'.

Copy this prompt to try it in FlutterFlow

In-app deal pipeline dashboard for a sales team

A sales team builds a FlutterFlow app where reps can see their open deals by pipeline stage, with deal name, amount, and close date visible on a kanban-style or list view. Tapping a deal opens the contact details. Data comes from the HubSpot Deals API through the proxy.

FlutterFlow Prompt

Build a screen listing open deals grouped by pipeline stage, showing deal name, amount, and expected close date for the current logged-in user's assigned deals.

Copy this prompt to try it in FlutterFlow

Field sales contact lookup and note-taking app

A field sales rep uses a FlutterFlow app to search for HubSpot contacts by company name or email while on site. They view the contact's deal history and add a call note directly from the app using the HubSpot Engagements API, synced back to the CRM instantly.

FlutterFlow Prompt

Create a search screen where the user types a company name and sees matching HubSpot contacts, with a detail page showing their deals and a text field to add a note.

Copy this prompt to try it in FlutterFlow

Troubleshooting

HubSpot returns 404 when searching contacts

Cause: The contact search endpoint is a POST request to /crm/v3/objects/contacts/search with a JSON filter body. Developers frequently try a GET request which returns 404 because there is no GET endpoint at that path.

Solution: Ensure your proxy uses method: 'POST' for the search_contacts operation and sends the filterGroups body as JSON. The filter body structure is: { "filterGroups": [{ "filters": [{ "propertyName": "email", "operator": "EQ", "value": "the@email.com" }] }] }. Check the Cloud Function logs to confirm the method and endpoint are correct.

Contact creation returns 400 Bad Request with 'Property does not exist'

Cause: The property name in the payload does not match HubSpot's internal property name. HubSpot uses snake_case internal names (firstname, lastname, phone) that differ from the display names shown in the UI.

Solution: In your HubSpot account, go to Settings → Properties, find the property you want to set, and click it to see the 'Internal name' — that is the exact string you must use in the API payload. Custom properties have auto-generated internal names. Also check that you are not sending a read-only computed property like createdate in the create payload.

FlutterFlow web build shows 'XMLHttpRequest error' when calling the proxy

Cause: The Cloud Function is missing CORS headers, or the FlutterFlow API Group Base URL points to api.hubapi.com directly instead of your proxy URL.

Solution: Confirm the Base URL in your FlutterFlow API Group is your Cloud Function URL, not api.hubapi.com. Verify the function includes res.set('Access-Control-Allow-Origin', '*') on all response paths including errors and the OPTIONS preflight handler. Redeploy the function after making changes and test with a REST client to confirm CORS headers appear in the response.

HubSpot returns 429 Too Many Requests

Cause: The 100-requests-per-10-second rate limit has been exceeded. This can happen if multiple FlutterFlow screens load simultaneously and each triggers multiple API calls, or if a Loop action fires CRM calls without delays.

Solution: Add caching for read operations (deals list, contact details) with a 5-minute TTL so concurrent screen opens do not each trigger fresh API calls. For any FlutterFlow Loop action that calls the proxy, add a Wait action between iterations. For bulk sync operations, move them to a scheduled Cloud Function that runs server-side rather than triggering from the app.

Best practices

  • Scope your HubSpot Private App token to the minimum required permissions — if your app only creates contacts and reads deals, do not add email, marketing, or settings scopes.
  • Never place the Private App token in a FlutterFlow API Call Authorization header — it compiles into the Flutter binary and ships to every device.
  • Use the POST search endpoint (/crm/v3/objects/contacts/search) for contact lookups — not GET with query params, which does not exist on this path.
  • Cache deal list reads for 5 minutes to avoid rate limit pressure from concurrent sales reps opening the app simultaneously.
  • Use HubSpot's internal property names in API payloads (firstname, lastname, phone) — display names from the HubSpot UI will cause 400 errors.
  • For bulk contact syncs, use a scheduled server-side Cloud Function with sequential loops rather than on-device batching, which easily exceeds the 100-requests/10-second ceiling.
  • Add a contact duplicate check before creating — HubSpot will create a duplicate if you POST the same email twice; use the search endpoint first to confirm uniqueness.
  • Handle the 201 Created response for contact creation separately from 200 OK — FlutterFlow's API response checking needs to treat both as success.

Alternatives

Frequently asked questions

Does FlutterFlow have a native HubSpot connector like Retool does?

No. Retool's native HubSpot resource connector allows direct API calls from Retool's server-side query execution engine. FlutterFlow has no equivalent native connector — every HubSpot integration in FlutterFlow requires building API Calls manually and routing them through your own backend proxy. This is more work upfront but gives you full control over caching, error handling, and rate limiting logic.

Can I use OAuth instead of a Private App token?

Yes — HubSpot supports OAuth 2.0 for third-party app integrations where users authorize your app to access their HubSpot account. This is the right choice if you are building a product where multiple different HubSpot customers connect their own accounts. For a single-tenant integration where you control the HubSpot account, the Private App token is simpler and recommended by HubSpot's own documentation.

How do I assign a newly created contact to a specific sales rep in HubSpot?

Include the hubspot_owner_id property in the contact creation payload. The owner ID is the HubSpot user ID of the sales rep — you can find it in HubSpot Settings → Users and Teams, or query it via the Owners API endpoint. Store a mapping of app user to HubSpot owner ID in your Supabase or Firestore database and look it up before calling the create endpoint.

What happens if the HubSpot contact creation call fails while a user is signing up?

Design your FlutterFlow app so that the sign-up flow completes in your own database (Supabase or Firestore) first, then fires the HubSpot contact creation as a secondary step. If the HubSpot call fails, the user still has a working account in your app — you can retry the CRM sync on next login or via a scheduled server function. Never block sign-up completion on a successful HubSpot write.

Can I create HubSpot deals from FlutterFlow, not just contacts?

Yes — add a create_deal operation to your proxy that POSTs to /crm/v3/objects/deals with properties including dealname, amount, closedate, pipeline, and dealstage. You can also associate the deal with a contact by calling the associations endpoint after creation: POST /crm/v4/objects/deals/{deal_id}/associations/default/contacts/{contact_id}. Add a createDeal API Call in FlutterFlow that triggers this proxy operation from a deal-creation form in your app.

RapidDev

Talk to an Expert

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

Book a free consultation

Integrations are where projects stall

We wire FlutterFlow integrations like this one every week — auth, webhooks, edge cases included. Fixed-price builds from $13K, delivered in 6–10 weeks.

Talk to an integration engineer

We put the rapid in RapidDev

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