# ZoomInfo

- Tool: FlutterFlow
- Difficulty: Advanced
- Time required: 60 minutes
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to ZoomInfo via a Firebase Cloud Function proxy: ZoomInfo's PKI-to-JWT authentication (username, client ID, and private key) cannot run safely in compiled Dart, so the token exchange and Enrich API calls live entirely in the function. FlutterFlow sends a domain or email variable; the function returns enriched company and contact data. Note that ZoomInfo is enterprise-only with annual contracts — no self-serve API keys.

## Connecting FlutterFlow to ZoomInfo: Lead Enrichment Without Leaking Enterprise Credentials

Despite appearing in the Communication category, ZoomInfo is a B2B sales-intelligence platform, not a chat or calling tool. Its primary use-case in a FlutterFlow app is lead enrichment: a user types a company domain or business email address into a form, and the app returns firmographic and contact data — company name, industry, headcount, revenue range, contact title, LinkedIn URL — without the user having to fill it in manually. This dramatically improves lead capture quality in sales or CRM-style mobile apps.

The integration challenge is ZoomInfo's authentication model. ZoomInfo uses a PKI-based flow: a username, client ID, and private key are combined to generate a short-lived JWT, then that JWT is sent as a Bearer token to the Enrich API at api.zoominfo.com. Putting the private key in Dart code ships it inside the compiled app binary, where it can be extracted. The entire authentication and enrichment call chain must therefore run in a Firebase Cloud Function, which FlutterFlow calls with just a domain or email variable.

ZoomInfo is enterprise-only with annual contracts (commonly $15K+/year, sales-gated, no self-serve tier — verify with your account rep). Credit usage is metered per record, meaning every API call consumes your contract's credit balance. This makes debouncing critical: triggering enrichment on every keystroke while the user types a domain would burn through your credit cap extremely quickly. Call the API only on form submission, not on input changes.

## Before you start

- A FlutterFlow project open in the browser at app.flutterflow.io
- An active ZoomInfo enterprise contract with API access provisioned (contact ZoomInfo sales if you do not have one)
- Your ZoomInfo API credentials: username, client ID, and private key (provided by ZoomInfo after contract activation)
- A Firebase project on the Blaze plan (required for Cloud Functions with outbound HTTP calls)
- Familiarity with Firebase Cloud Functions deployment — you will write and deploy a Node.js function

## Step-by-step guide

### 1. Step 1: Understand ZoomInfo's authentication flow and why it belongs in a Cloud Function

Before writing any FlutterFlow code, it is important to understand why this integration requires a server-side proxy and what ZoomInfo's authentication actually involves — this will save you from a frustrating debugging loop later.

ZoomInfo's API authentication is a JWT-based PKI flow. Your ZoomInfo account comes with three credentials: a username (your ZoomInfo login email), a client ID (a string identifier for your API app), and a private key (a cryptographic secret). To make API calls, you:
1. Combine the username and client ID to create a payload.
2. Sign the payload with the private key to produce a JWT.
3. Send the JWT to ZoomInfo's auth endpoint to receive a short-lived Bearer access token.
4. Use the Bearer token in the Authorization header of Enrich/Search API calls.

Verify the exact format of the JWT payload and signing algorithm with your ZoomInfo account representative — the specifics can differ between contract tiers.

The private key step is the critical security constraint: if you put the private key in Dart code, it ships inside the compiled Flutter app binary. Anyone who downloads your app and uses standard reverse-engineering tools can extract the private key and use it to call ZoomInfo's API as you — consuming your per-record credit balance and exposing your enterprise data contract.

The correct architecture: the private key never leaves your Firebase Cloud Function. FlutterFlow sends only a domain or email to your function; the function does the JWT generation and Enrich call; the function returns only the enriched data. ZoomInfo never sees a direct call from the FlutterFlow app.

**Expected result:** You have a clear understanding of the three-credential JWT flow and have confirmed your ZoomInfo username, client ID, and private key are available from your ZoomInfo account admin panel.

### 2. Step 2: Deploy a Firebase Cloud Function that handles JWT generation and Enrich API calls

Create a Firebase Cloud Function in your Firebase project (Blaze plan required for outbound HTTP). This function is responsible for:
1. Reading the ZoomInfo credentials from Firebase environment config — never from source code.
2. Generating a JWT using the private key (using the jsonwebtoken npm package).
3. Exchanging the JWT for a short-lived Bearer access token at ZoomInfo's token endpoint.
4. Calling the ZoomInfo Enrich API at api.zoominfo.com with the Bearer token and the domain or email passed by FlutterFlow.
5. Returning the enriched company and contact data as JSON.

Before deploying, set environment config:
firebase functions:config:set zoominfo.username="your@email.com" zoominfo.client_id="YOUR_CLIENT_ID" zoominfo.private_key="YOUR_PRIVATE_KEY"

The private_key value should be the PEM-formatted key string with newlines escaped. Deploy with firebase deploy --only functions.

The Cloud Function also handles caching of the access token in memory to avoid re-generating the JWT on every enrichment call, since JWT generation is computationally more expensive than a cached Bearer token lookup.

IMPORTANT: ZoomInfo bills per record enriched. Add logic in the function to validate that the input (domain or email) is non-empty and well-formed before calling the Enrich API — reject invalid inputs at the function level rather than burning a credit on a malformed request.

```
const functions = require('firebase-functions');
const axios = require('axios');
const jwt = require('jsonwebtoken');

let cachedToken = null;
let tokenExpiry = 0;

async function getZoomInfoToken(cfg) {
  if (cachedToken && Date.now() < tokenExpiry) return cachedToken;
  // Step 1: generate JWT from credentials
  // Verify exact payload claims with your ZoomInfo account rep
  const payload = {
    iss: cfg.client_id,
    sub: cfg.username
  };
  const signedJwt = jwt.sign(payload, cfg.private_key, {
    algorithm: 'RS256',
    expiresIn: '60s'
  });
  // Step 2: exchange JWT for Bearer access token
  // Verify the exact token endpoint URL for your ZoomInfo plan
  const res = await axios.post(
    'https://api.zoominfo.com/authenticate',
    { jwt: signedJwt },
    { headers: { 'Content-Type': 'application/json' } }
  );
  cachedToken = res.data.jwt; // verify response field name with your plan
  tokenExpiry = Date.now() + 55 * 60 * 1000; // cache ~55 minutes
  return cachedToken;
}

exports.enrichLead = functions.https.onRequest(async (req, res) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  if (req.method === 'OPTIONS') return res.status(204).send('');
  const cfg = functions.config().zoominfo;
  const { domain, email } = req.body;
  if (!domain && !email) {
    return res.status(400).json({ error: 'Provide domain or email' });
  }
  try {
    const token = await getZoomInfoToken(cfg);
    // Verify endpoint path and request body structure for your plan
    const body = domain
      ? { companyMatchKeys: [{ websiteURL: domain }], outputFields: ['id', 'name', 'industry', 'employeeCount', 'revenueRange'] }
      : { personMatchKeys: [{ emailAddress: email }], outputFields: ['firstName', 'lastName', 'jobTitle', 'companyName', 'linkedInURL'] };
    const endpoint = domain
      ? 'https://api.zoominfo.com/enrich/company'
      : 'https://api.zoominfo.com/enrich/contact';
    const result = await axios.post(endpoint, body, {
      headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
    });
    res.json(result.data);
  } catch (err) {
    const status = err.response ? err.response.status : 500;
    res.status(status).json({ error: err.message });
  }
});
```

**Expected result:** The Firebase Cloud Function deploys successfully. Testing it via Postman with a POST body of { "domain": "example.com" } returns a JSON object with company enrichment data. The function returns an error for an empty or malformed input.

### 3. Step 3: Create a ZoomInfo API Group and API Call in FlutterFlow

With the Cloud Function deployed and tested, configure FlutterFlow to call it.

In FlutterFlow, click API Calls in the left navigation panel. Click + Add and choose Create API Group. Name it ZoomInfo and set the Base URL to your Firebase Cloud Function's HTTPS trigger URL (e.g. https://us-central1-your-project.cloudfunctions.net).

Inside the ZoomInfo group, click Add API Call and name it EnrichLead. Set the Method to POST and the Endpoint to /enrichLead (or your function's route). You do not need to add an Authorization header — your Cloud Function handles all ZoomInfo authentication internally.

Under the Variables tab, add two variables: domain (String, optional) and email (String, optional). In the Body tab, select JSON and set:

```json
{
  "domain": "{{ domain }}",
  "email": "{{ email }}"
}
```

Under the Response & Test tab, paste a sample response JSON from your Cloud Function test — for a company enrichment, this might include fields like name, industry, employeeCount, and revenueRange. Click Generate JSON Paths to create bindings like $.data[0].name, $.data[0].industry, and $.data[0].employeeCount.

Verify the exact JSON path structure by running a real call through the Test tab (using a test domain like 'example.com') and examining the actual response shape from your Cloud Function. The structure will match your ZoomInfo plan's response format.

Save the API Call.

```
{
  "method": "POST",
  "endpoint": "/enrichLead",
  "headers": { "Content-Type": "application/json" },
  "body": {
    "domain": "{{ domain }}",
    "email": "{{ email }}"
  }
}
```

**Expected result:** A ZoomInfo API Group containing an EnrichLead API Call appears in the API Calls panel. The Response & Test tab shows JSON Path bindings populated from a real test call against your Cloud Function.

### 4. Step 4: Wire the Enrich action to a form button with debouncing

Now connect the EnrichLead API Call to your lead capture form in FlutterFlow.

On your lead form screen, add a TextField for the company domain (or email) and an 'Enrich' Button. The critical design decision here is to wire the enrichment call to the button tap — not to the TextField's onChange event. Triggering enrichment on every keystroke would call the Cloud Function dozens of times as the user types, burning through ZoomInfo's per-record credit cap extremely quickly. The Enrich button is the correct trigger.

Create a page variable called isEnriching (Boolean, initial value false) and another called enrichError (String, initial value ''). In the Action Flow Editor on the Enrich button:

1. First action: Update Page State → set isEnriching to true, clear enrichError.
2. Second action: Backend/API Calls → ZoomInfo → EnrichLead. Map the domain variable to the TextField's value.
3. On success branch: extract the JSON Path values from the action output and use Update Page State to set individual page variables (companyName, industry, headcount, revenueRange). Then set isEnriching to false.
4. On failure branch: set enrichError to a human-readable message and set isEnriching to false.

Disable the Enrich button when isEnriching is true (via a Conditional Widget property on the button's disabled state). Show a CircularProgressIndicator when isEnriching is true. Show the enrichError text in a red Text widget, conditionally visible when enrichError is not empty.

Bind the auto-filled output fields (Company Name, Industry, Headcount) to their respective page variables. After enrichment succeeds, these fields populate automatically without user input.

**Expected result:** The Enrich button triggers a single Cloud Function call with the entered domain. After the call returns, the Company Name, Industry, Headcount, and Revenue Range fields populate automatically. The button shows a loading state during the call and an error message on failure.

### 5. Step 5: Handle errors gracefully and protect the per-record credit cap

ZoomInfo's per-record billing model makes error handling and call discipline more important here than in most integrations. A few implementation details that protect your credit cap and improve user experience:

Validate input before calling: In the Action Flow Editor, add a Conditional action before the EnrichLead call. Check that the domain TextField is not empty and contains at least one period (basic domain format check). If validation fails, show a validation message — no credit is consumed. Similarly, validate email format before email-based enrichment.

Handle no-match gracefully: ZoomInfo returns a 200 response with an empty data array when no match is found for the input. Parse the array length from the JSON Path response. If the array is empty, show a 'No ZoomInfo data found for this domain' message rather than leaving all fields blank with no explanation.

Handle credit exhaustion: ZoomInfo returns an error when the account's credit cap is reached (verify the exact HTTP status and error code with your account rep — it may be 402 or a specific error message in the response body). Catch this in the failure branch of the Action Flow Editor and show a meaningful message like 'Enrichment is temporarily unavailable — contact your admin.'

For production apps, consider logging each enrichment call (domain queried, timestamp, whether a match was found) to Firestore so you can audit credit usage and identify if any screen is triggering unexpected enrichment calls. If you need help setting up this credit-tracking pattern alongside the FlutterFlow UI, RapidDev's team builds integrations like this every week — free scoping call at rapidevelopers.com/contact.

**Expected result:** The form correctly rejects empty or malformed domain input without calling the API. The no-match state shows a helpful message. Credit exhaustion and other API errors are caught and displayed to the user rather than silently failing.

## Best practices

- Never put the ZoomInfo private key, client ID, or Bearer token in any FlutterFlow field — the Cloud Function proxy is the only safe location for these credentials
- Trigger enrichment only on explicit button tap or form submission, never on TextField onChange — ZoomInfo bills per record and keystroke-level calls will drain your credit cap rapidly
- Validate domain or email format in the FlutterFlow Action Flow Editor before calling the Cloud Function to avoid burning credits on obviously invalid inputs
- Cache the ZoomInfo Bearer token in the Cloud Function with a buffer before expiry (subtract a few minutes from the token's TTL) to avoid regenerating the JWT on every enrichment call
- Log each enrichment call to Firestore with the timestamp, domain queried, and whether a match was found — this audit trail helps you track credit usage and identify unexpected API calls
- Handle the no-match response (empty data array) explicitly in the UI rather than leaving auto-fill fields blank with no explanation
- Store enriched data to Firestore after a successful call so users do not need to re-enrich the same domain on future app opens
- Verify all API details — authentication flow, endpoint paths, and response schema — with your ZoomInfo account representative, as they can differ between contract tiers

## Use cases

### Sales CRM app with auto-fill company data on lead forms

A mobile CRM app for sales reps includes a 'Add Lead' screen with a company domain field. After the rep types the domain and taps 'Enrich', the app calls ZoomInfo through the Cloud Function and auto-fills company name, industry, headcount, and the contact's title and LinkedIn URL. This eliminates manual data entry and improves lead record quality.

Prompt example:

```
On the Add Lead screen, add a 'Company Domain' text field and an 'Enrich' button. When tapped, call the ZoomInfo enrichment backend and auto-fill the Company Name, Industry, Headcount, and Primary Contact Title fields with the returned data.
```

### B2B event app that enriches attendee registrations

A B2B events app collects attendee email addresses during in-person events. After submission, the app silently calls ZoomInfo to append job title, company, and seniority level to the registration record stored in Firestore. Sales teams can then see enriched attendee data in the organizer dashboard.

Prompt example:

```
After a user submits their email on the event registration screen, silently enrich their contact record using ZoomInfo and save the returned company and title fields to their Firestore document without any visible loading UI.
```

### Account-based marketing dashboard enriching target accounts

An ABM dashboard app lets marketers enter a list of target company domains. The app enriches each domain through ZoomInfo and displays a card per company with industry, revenue range, headcount, and key contacts. The enrichment is batched on a scheduled Firebase Cloud Function to avoid burning credits interactively.

Prompt example:

```
Build an Account Dashboard screen that shows a card for each of our target company domains. Each card should display the company name, industry, headcount range, and estimated revenue populated from ZoomInfo enrichment.
```

## Troubleshooting

### Cloud Function returns 401 Unauthorized from ZoomInfo Enrich API

Cause: The cached Bearer token has expired, or the JWT was generated with incorrect claims (wrong algorithm, wrong payload structure, or expired signing time).

Solution: Clear the cached token in the Cloud Function and regenerate the JWT. Verify the JWT payload structure (claims, algorithm, and expiry) matches what ZoomInfo expects for your plan — consult your ZoomInfo account representative if the 401 persists after a fresh token. Check that the private key stored in Firebase environment config is the full PEM-formatted key with correct newline handling.

```
// Force token refresh:
cachedToken = null;
tokenExpiry = 0;
const freshToken = await getZoomInfoToken(cfg);
```

### XMLHttpRequest error in FlutterFlow web build even though the API Call Test tab succeeds

Cause: The Cloud Function is missing CORS headers. Browser-based FlutterFlow web builds enforce CORS; the API Call Test tab runs from FlutterFlow's servers (not the browser) and bypasses this.

Solution: Add CORS headers to your Firebase Cloud Function. Add res.setHeader('Access-Control-Allow-Origin', '*') at the top of the request handler and return 204 for OPTIONS preflight requests. Alternatively, wrap your function with the cors npm package.

```
res.setHeader('Access-Control-Allow-Origin', '*');
if (req.method === 'OPTIONS') return res.status(204).send('');
```

### ZoomInfo returns a 200 response but the enriched data fields are empty in FlutterFlow

Cause: The JSON Path bindings in the FlutterFlow API Call do not match the actual response structure returned by your Cloud Function, or the ZoomInfo response contains an empty data array (no match for the domain/email).

Solution: Re-open the Response & Test tab for the EnrichLead call, paste the actual JSON response from a successful Cloud Function call, and click Generate JSON Paths to regenerate the bindings. Also add a conditional check for an empty data array and show a 'No match found' state in the UI.

### Credits are being consumed faster than expected

Cause: Enrichment is being triggered more frequently than intended — for example, if a Backend Query is set to auto-refresh on page open, or if the action is wired to a TextField onChange event instead of a submit button.

Solution: Audit every place the EnrichLead API Call is triggered. Ensure the call is only fired on explicit button taps or form submissions, never on page load or text input changes. Add logging to the Cloud Function (write a Firestore document per call with timestamp and input) to identify the source of unexpected calls.

## Frequently asked questions

### Can I get ZoomInfo API access without an annual enterprise contract?

No. As of current information, ZoomInfo does not offer a self-serve API tier — API access is bundled with annual enterprise contracts that are commonly quoted at $15,000 or more per year and are gated behind a sales conversation. If you need B2B company data with a self-serve API and a free tier, consider alternatives like Apollo.io or HubSpot's Companies API.

### Why can't I generate the ZoomInfo JWT directly in a FlutterFlow Custom Action?

Generating a JWT requires the ZoomInfo private key, which is a cryptographic secret. FlutterFlow Custom Actions compile to Dart code and ship inside the app binary on iOS, Android, and web. Any secret embedded in the binary can be extracted using standard tools. The private key must stay on the server — in a Firebase Cloud Function — where only your code can access it. The function generates the JWT and calls ZoomInfo, returning only the non-sensitive enrichment results to FlutterFlow.

### How many API calls does my ZoomInfo contract include?

ZoomInfo billing is per-record enriched rather than per API call, and the exact credit allocation depends on your specific contract terms. Verify your credit cap, what counts as a consumed credit (successful match only vs. all calls), and the cost of exceeding the cap with your ZoomInfo account representative before deploying the integration to production.

### What happens if ZoomInfo does not have data for a domain I search?

ZoomInfo returns a 200 HTTP status with an empty or minimal data array when it cannot match the input domain or email to a known company or contact. Your Cloud Function should pass this empty response back to FlutterFlow, and your UI should show a 'No data found for this domain' message rather than leaving form fields blank. Whether an unmatched call still consumes a credit depends on your ZoomInfo contract terms — verify this with your account rep.

### Can I use ZoomInfo for consumer contact lookups, not just B2B company data?

ZoomInfo's dataset is focused on B2B contacts and companies — job titles, company firmographics, direct-dial business phone numbers, and professional email addresses. It is not designed or licensed for consumer identity lookups. Using ZoomInfo for consumer data enrichment may violate your contract terms and applicable privacy regulations (GDPR, CCPA). Consult your ZoomInfo contract and legal counsel before using the API for consumer-facing data.

---

Source: https://www.rapidevelopers.com/flutterflow-integrations/zoominfo
© RapidDev — https://www.rapidevelopers.com/flutterflow-integrations/zoominfo
