# Salesforce

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

## TL;DR

Connect FlutterFlow to Salesforce using the REST API and SOQL queries. Because the OAuth consumer secret cannot live inside a compiled Flutter app, route the token exchange through a Firebase Cloud Function that also returns the instance-specific base URL. FlutterFlow API Calls then query Opportunities, Contacts, and Leads using `Bearer` token auth against your org's unique Salesforce instance URL.

## Salesforce in FlutterFlow: Connected Apps, SOQL, and the Dynamic Instance URL

Salesforce exposes its data through a REST API at a per-organization URL — your org's instance URL looks something like `https://yourcompany.my.salesforce.com`. Unlike most REST APIs where the base URL is fixed, Salesforce returns `instance_url` dynamically in the OAuth token response. This means you cannot hardcode the API base URL in FlutterFlow: the instance URL must be retrieved from the OAuth flow and stored in App State before any CRM API calls can be made. If you ever refresh a sandbox or switch environments, the instance URL changes.

Salesforce's defining query language is SOQL — Salesforce Object Query Language. Instead of OData filter params or module endpoint paths, you send SOQL strings like `SELECT Id, Name, StageName FROM Opportunity WHERE CloseDate > TODAY` as URL-encoded query parameters to `/services/data/v60.0/query/?q=...`. Results come back under a `records` array, not `value` or `data`. SOQL must be URL-encoded: spaces become `+`, special characters are percent-encoded.

The entire OAuth flow requires a consumer secret — a server credential that must never be embedded in a compiled Flutter app. A Firebase Cloud Function handles the OAuth exchange and is the only place the consumer secret lives. Salesforce Sales Cloud starts at around $25 per user per month (verify current pricing at salesforce.com/editions-pricing/sales-cloud). API call limits are approximately 1,000 per user per 24-hour period on lower editions, shared across the entire organization — be conservative with list refresh rates in your FlutterFlow app to avoid hitting the ceiling.

## Before you start

- A Salesforce org (Developer Edition is free at developer.salesforce.com/signup) or a licensed Sales Cloud org
- System Administrator access to Salesforce Setup to create a Connected App
- A Firebase project on the Blaze (pay-as-you-go) plan with Cloud Functions enabled
- A FlutterFlow project on at least the Starter plan to access API Calls and App State
- Basic familiarity with FlutterFlow's left-nav panels, API Calls section, and App Values

## Step-by-step guide

### 1. Create a Salesforce Connected App and capture OAuth credentials

In Salesforce, go to Setup (gear icon → Setup). In the left Quick Find search box, type 'App Manager' and click it. Click 'New Connected App' in the top right.

Fill in the basic information: Connected App Name (e.g. 'FlutterFlow Integration'), API Name (auto-filled), and Contact Email. Under 'API (Enable OAuth Settings)', tick the checkbox 'Enable OAuth Settings'. In the 'Callback URL' field, enter `https://login.salesforce.com/services/oauth2/success` — this is a Salesforce-provided success page that works for server-side flows.

Under 'Selected OAuth Scopes', add: 'Access and manage your data (api)', 'Perform requests on your behalf at any time (refresh_token, offline_access)', and 'Full access (full)'. Move them to the Selected Scopes column. Click 'Save'. Salesforce will warn you that changes take 2–10 minutes to propagate — wait before testing.

After saving, click 'Manage Consumer Details' (or view the App in App Manager). Copy the 'Consumer Key' (also called Client ID) and 'Consumer Secret' — these go into the Firebase Cloud Function, not into FlutterFlow. Also note the 'Manage' page, where you can configure IP restrictions and authorized users if needed for production.

To get an initial refresh token for server-to-server use, you'll need to complete one OAuth flow interactively. Build the authorization URL: `https://login.salesforce.com/services/oauth2/authorize?response_type=code&client_id=YOUR_CONSUMER_KEY&redirect_uri=https://login.salesforce.com/services/oauth2/success&scope=api+refresh_token`. Open this URL in a browser while logged into Salesforce, approve the access, and you'll be redirected to a URL containing `?code=AUTH_CODE`. Copy that code and exchange it for a refresh token using the Cloud Function you'll build in the next step.

**Expected result:** The Connected App shows 'Consumer Key' and 'Consumer Secret' in the Manage Consumer Details section. The OAuth scopes include 'api' and 'refresh_token'. The app status shows 'Active'.

### 2. Deploy a Firebase Cloud Function for Salesforce OAuth

The Cloud Function is the security boundary between your Flutter app and Salesforce's OAuth credentials. When called, it uses the stored consumer key, consumer secret, and refresh token to fetch a new access token from Salesforce, and returns both the access token and the instance URL to FlutterFlow.

Open your Firebase project → Build → Functions. Create the function code shown below. In your Firebase CLI, set the environment config: `firebase functions:config:set salesforce.consumer_key="..." salesforce.consumer_secret="..." salesforce.refresh_token="..."`. To get the initial refresh token, exchange the auth code from Step 1: POST to `https://login.salesforce.com/services/oauth2/token` with `grant_type=authorization_code`, `client_id`, `client_secret`, `code`, and `redirect_uri`. The response includes `refresh_token`, `access_token`, and crucially `instance_url` — that instance URL is org-specific.

Deploy the function with `firebase deploy --only functions` and copy the HTTPS trigger URL. Test it by calling it in a browser — it should return a JSON object with `access_token` and `instance_url`.

Note: the function includes CORS headers so FlutterFlow web builds (running in a browser) can call it. For mobile-only apps, CORS headers are harmless extras.

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

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

  const config = functions.config().salesforce;
  const tokenUrl = 'https://login.salesforce.com/services/oauth2/token';

  try {
    const params = new URLSearchParams();
    params.append('grant_type', 'refresh_token');
    params.append('client_id', config.consumer_key);
    params.append('client_secret', config.consumer_secret);
    params.append('refresh_token', config.refresh_token);

    const response = await axios.post(tokenUrl, params);
    return res.json({
      access_token: response.data.access_token,
      instance_url: response.data.instance_url,
      expires_in: 7200 // Salesforce tokens typically 2 hours
    });
  } catch (err) {
    console.error('Salesforce token error:', err.response?.data || err.message);
    return res.status(500).json({ error: 'Token refresh failed' });
  }
});
```

**Expected result:** Calling the Cloud Function URL returns `{"access_token": "...", "instance_url": "https://yourcompany.my.salesforce.com", "expires_in": 7200}`. The instance_url is unique to your Salesforce organization.

### 3. Configure App State for the token and instance URL

Because Salesforce's instance URL is org-specific and returned dynamically from the token endpoint, you must store it in App State rather than hardcoding it into the FlutterFlow API Group's base URL.

In the FlutterFlow left nav, click the puzzle-piece icon → App State. Add two String variables:
1. `sfAccessToken` — initial value empty. This holds the current Salesforce bearer token.
2. `sfInstanceUrl` — initial value empty. This holds the org's base URL (e.g. `https://yourcompany.my.salesforce.com`).

Now create the token-service API Group. Go to left nav → API Calls → '+ Add' → 'Create API Group'. Name it 'SalesforceAuth'. Set the Base URL to your Firebase Cloud Function's HTTPS trigger URL (e.g. `https://us-central1-yourproject.cloudfunctions.net`). Add one API Call: Method GET, name `getToken`, endpoint `/getSalesforceToken`.

Wire the token fetch to app launch. Click your initial page → Actions panel → 'On Page Load' trigger → '+ Add Action' → 'Backend/API Call' → select `getToken` from SalesforceAuth. In 'Action Output Variable':
- Map `$.access_token` to App State → `sfAccessToken`
- Map `$.instance_url` to App State → `sfInstanceUrl`

After the token action, all subsequent Salesforce API Calls will use these values from App State.

**Expected result:** After app launch, App State shows non-empty `sfAccessToken` and `sfInstanceUrl` strings. The instance URL matches your Salesforce org URL visible in your browser when logged in to Salesforce.

### 4. Create the Salesforce REST API Group with dynamic base URL

In FlutterFlow left nav → API Calls → '+ Add' → 'Create API Group'. Name it 'SalesforceAPI'. For the Base URL, set it to `[sfInstanceUrl]/services/data/v60.0` — but FlutterFlow requires a literal string for the base URL field, not a dynamic variable. There are two ways to handle this:

Option A (simpler for most apps): hardcode the Base URL as `https://yourcompany.my.salesforce.com/services/data/v60.0` after you've confirmed your instance URL from the token response in Step 3. This works as long as your org doesn't change its My Domain subdomain.

Option B (dynamic, correct for multi-org or changeable instance): keep a single API Group with a hardcoded interim base URL, and in each individual API Call, add a Variable for the full URL (override the base URL via a Custom Function that concatenates App State → sfInstanceUrl + the endpoint path). This is more advanced but handles sandbox refreshes gracefully.

For most single-org FlutterFlow apps, Option A is fine. Set the base URL to your instance URL and add shared headers: `Authorization: Bearer [accessToken]` (add a group-level Variable `accessToken` of type String), and `Content-Type: application/json`.

Add individual API Calls:
1. `queryOpportunities` — Method GET, endpoint `/query/?q=SELECT+Id,Name,StageName,Amount,CloseDate+FROM+Opportunity+WHERE+IsClosed=false+ORDER+BY+CloseDate+ASC+LIMIT+50`. Parse `$.records`.
2. `createLead` — Method POST, endpoint `/sobjects/Lead`. Body with variables: `firstName`, `lastName`, `email`, `company`, `phone`.
3. `queryContacts` — Method GET, endpoint `/query/?q=SELECT+Id,Name,Email,Phone,AccountId+FROM+Contact+LIMIT+50`. Parse `$.records`.

For the SOQL query endpoint, note that spaces are encoded as `+` and the entire query string after `?q=` must be URL-safe. FlutterFlow passes query parameters as part of the endpoint path string — so the URL encoding must be done manually when you type the endpoint.

```
{
  "method": "GET",
  "endpoint": "/query/?q=SELECT+Id,Name,StageName,Amount,CloseDate+FROM+Opportunity+WHERE+IsClosed=false+ORDER+BY+CloseDate+ASC+LIMIT+50",
  "headers": {
    "Authorization": "Bearer [accessToken]",
    "Content-Type": "application/json"
  }
}
```

**Expected result:** Calling `queryOpportunities` in the Response & Test tab with a valid access token returns a 200 with `$.records` containing your Salesforce Opportunities.

### 5. Build an Opportunities ListView and a create-lead form

With the API Group configured, wire the Salesforce data to your FlutterFlow UI.

On the main page, drag a ListView widget onto the canvas. Select it → Backend Query → API Call → choose `queryOpportunities` from SalesforceAPI. Under Variables, pass `App State → sfAccessToken` as `accessToken`. Under Response JSON Path, enter `$.records` so the ListView iterates over the Opportunities array.

Inside the ListView, add a Column. Add Text widgets bound to `$.Name` (Opportunity name), `$.StageName`, and `$.Amount`. You can use a formatted Amount display by adding a String conversion custom function if you want currency symbols.

For creating a Lead, add a 'NewLeadPage'. Add TextFields for First Name, Last Name, Email, Company, and Phone. Add a Submit button. In the button's Actions:
1. 'Backend/API Call' → `createLead` from SalesforceAPI.
2. Map TextFields to variables: `firstName` → First Name, `lastName` → Last Name, etc.
3. Pass `App State → sfAccessToken` as `accessToken`.
4. Map the body as `{"FirstName":"[firstName]","LastName":"[lastName]","Email":"[email]","Company":"[company]","Phone":"[phone]","LeadSource":"Mobile App"}`.
5. After success (check for status code 201), show a Snack Bar: 'Lead created in Salesforce — ID: [$.id]'. The response body contains `{"id": "...", "success": true}`.

For error handling: if status is 400, show an Alert Dialog. MALFORMED_QUERY and REQUIRED_FIELD_MISSING are the most common error codes — handle them with user-friendly messages like 'Check that all required fields are filled in'.

```
{
  "method": "POST",
  "endpoint": "/sobjects/Lead",
  "headers": {
    "Authorization": "Bearer [accessToken]",
    "Content-Type": "application/json"
  },
  "body": {
    "FirstName": "[firstName]",
    "LastName": "[lastName]",
    "Email": "[email]",
    "Company": "[company]",
    "Phone": "[phone]",
    "LeadSource": "Mobile App"
  }
}
```

**Expected result:** The Opportunities ListView populates with real Salesforce data in FlutterFlow Run mode. Submitting the create-lead form returns 201 with a Salesforce record ID, and the lead appears in Salesforce Leads within seconds.

### 6. Handle INVALID_SESSION_ID expiry and daily API call limits

Two runtime conditions need handling in production: token expiry (`INVALID_SESSION_ID` / 401) and daily API call limits.

For token expiry: when Salesforce returns a 401 response, the response body contains `[{"errorCode":"INVALID_SESSION_ID"}]`. In your Action Flow, add a Conditional Action after any Salesforce API Call that checks 'Status Code equals 401'. If true, call `getToken` from SalesforceAuth, update both `App State → sfAccessToken` and `App State → sfInstanceUrl`, and retry the original API Call. This two-step update is important: if a sandbox refresh changed the instance URL, the token response captures the new URL.

For daily API limits: Salesforce's lower editions cap API calls at approximately 1,000 per user per 24 hours (org-wide total). A FlutterFlow ListView fetching Opportunities every few seconds on 10 devices simultaneously can consume thousands of calls per day. In FlutterFlow, disable any auto-refresh on ListViews and fetch only on: app launch, explicit pull-to-refresh, or after a successful write operation. For web builds, if the Salesforce REST API blocks cross-origin browser requests, route all API calls through the Firebase Cloud Function (not just the token endpoint) — though direct REST calls often work fine from browsers on modern Salesforce instances.

If you'd rather not manage the Connected App setup and Cloud Function proxy yourself, RapidDev's team builds FlutterFlow CRM integrations routinely — book a free scoping call at rapidevelopers.com/contact.

**Expected result:** When a 401 INVALID_SESSION_ID occurs, the app silently refreshes both the token and instance URL before retrying. The daily API call counter in Salesforce Setup → System Overview stays well within limits.

## Best practices

- Never hardcode the Salesforce instance URL in the FlutterFlow API Group base URL — always retrieve it from the OAuth token endpoint and store it in App State, as sandbox refreshes and org migrations can change it.
- Keep the consumer secret and refresh token exclusively in Firebase Cloud Function environment variables — these are server credentials that cannot safely live in a compiled Flutter app.
- URL-encode SOQL queries properly in API Call endpoint paths: spaces as `+`, single quotes as `%27`, operators and keywords in uppercase to avoid MALFORMED_QUERY errors.
- Respect Salesforce's daily API call limits (edition-dependent, typically 1,000 per user per 24h on lower editions) by fetching only on demand and caching results in App State between navigations.
- Use `$select` equivalent (SOQL field list) to query only the fields your UI displays — Salesforce objects can have hundreds of fields, and unbounded queries waste bandwidth and latency.
- Build a 401 → silent token refresh → retry pattern in your Action Flows so users never see a raw Salesforce error code — the INVALID_SESSION_ID error should be invisible to end users.
- Test your integration on a Salesforce Developer Edition org (free) before deploying against a production org — this avoids contaminating production data during development and lets you test error handling safely.
- For web builds of your FlutterFlow app, test whether the Salesforce API allows cross-origin requests from your hosting domain — if not, route all REST calls through the Firebase Cloud Function, not just the token endpoint.

## Use cases

### Mobile pipeline manager — browse and update Opportunities on the go

A FlutterFlow mobile app lets sales reps view their open Opportunities sorted by close date, update the Stage from a dropdown, and log call notes — all syncing to Salesforce instantly. Because FlutterFlow builds a native iOS and Android app, reps get a faster, more polished experience than the Salesforce mobile app while the data stays in your org's Salesforce instance.

Prompt example:

```
Build a Flutter mobile app that fetches open Salesforce Opportunities using a SOQL query, displays them in a card list sorted by close date, and lets the user tap a card to update the Stage field from a dropdown. Changes should be PATCHed back to Salesforce via the REST API.
```

### Trade show lead capture — create Leads directly in Salesforce

A FlutterFlow form app lets staff at events quickly enter prospect information. On submit, the app creates a new Lead in Salesforce with a custom source field set to the event name. The Cloud Function handles auth so the rep just opens the app and starts capturing without any login flow.

Prompt example:

```
Build a Flutter lead-capture form with fields for first name, last name, email, company, phone, and lead source. On submit, POST a new Lead to Salesforce via the REST API and show a success confirmation with the Salesforce record ID.
```

### Customer service lookup — find Contact records by email

A FlutterFlow app for customer support teams lets agents enter a customer email and pull up the matching Salesforce Contact record, including account name, phone, and recent activity. The SOQL query filters by email: `SELECT Id, Name, Email, Phone FROM Contact WHERE Email = 'input@email.com'`.

Prompt example:

```
Build a Flutter app with a search field that queries Salesforce Contacts by email using SOQL. Display the matching contact's name, account, phone, and email in a detail card. Show 'No contact found' if the query returns no records.
```

## Troubleshooting

### `INVALID_SESSION_ID` error in Salesforce API response even with a freshly fetched token

Cause: The access token was obtained from a different Salesforce environment (e.g. login.salesforce.com for a sandbox that requires test.salesforce.com), or the Connected App session timeout is shorter than expected and the token has already expired.

Solution: Verify the token endpoint URL in your Cloud Function: production orgs use `login.salesforce.com`, sandboxes use `test.salesforce.com`. Also check your Salesforce org's 'Session Settings' (Setup → Security → Session Settings) for the 'Timeout Value' — if it is set to 15 or 30 minutes, tokens expire faster than Salesforce's default 2-hour window. Reduce the time between token refreshes or add proactive refresh logic.

### SOQL query returns `MALFORMED_QUERY` error

Cause: The SOQL string is not properly URL-encoded in the FlutterFlow endpoint path, or contains syntax errors such as unquoted string literals or invalid field names.

Solution: In the FlutterFlow API Call endpoint field, ensure spaces in the SOQL are encoded as `+` and single quotes are encoded as `%27`. Validate the SOQL separately in Salesforce's Developer Console (Setup → Developer Console → Query Editor) before putting it in FlutterFlow. Also verify all field API names exist on the object — use Object Manager to confirm.

### FlutterFlow ListView shows empty results — Opportunities list does not populate

Cause: The JSON Path for the ListView is set to `$` or another incorrect path. Salesforce REST query responses wrap records in a `records` array (not `value`, `data`, or `results`).

Solution: In the FlutterFlow API Call → Response & Test tab, paste a real Salesforce query response and click 'Generate JSON Paths'. Set the ListView's Response JSON Path to `$.records`. Ensure the SOQL includes the fields you want to display and that the running user has at least 'Read' access to the Opportunity object in their Salesforce profile.

### Connected App returns `invalid_client_id` during the OAuth flow

Cause: The Consumer Key (Client ID) in the Cloud Function does not match the Connected App, or the Connected App changes are still propagating (Salesforce Connected App changes take 2–10 minutes to take effect).

Solution: In Salesforce Setup → App Manager → your Connected App → Manage Consumer Details, re-copy the Consumer Key and update the Firebase Cloud Function environment variable. If you just created the Connected App, wait 10 minutes before testing. Also ensure 'Enable OAuth Settings' is checked in the Connected App and the correct scopes are selected.

## Frequently asked questions

### Does FlutterFlow have a native Salesforce connector?

No. FlutterFlow does not include a built-in Salesforce integration. You connect using the API Calls panel, which lets you configure Salesforce REST endpoints manually. Because Salesforce's OAuth requires a consumer secret and the instance URL is dynamic, you also need a Firebase Cloud Function to handle authentication and return the correct base URL.

### What is SOQL and how do I write it in FlutterFlow?

SOQL (Salesforce Object Query Language) is SQL-like syntax for reading Salesforce data. A query looks like `SELECT Id, Name FROM Opportunity WHERE IsClosed = false`. In FlutterFlow, you put the SOQL as a URL-encoded query parameter in the API Call endpoint path: `/query/?q=SELECT+Id,Name+FROM+Opportunity+WHERE+IsClosed=false`. Spaces become `+`, single quotes become `%27`. Validate SOQL syntax in Salesforce's Developer Console before putting it in FlutterFlow.

### Why does Salesforce's instance URL matter and can I just hardcode it?

Each Salesforce organization has a unique instance URL like `https://yourcompany.my.salesforce.com`. This URL can change if your org migrates servers, if you switch between sandbox and production, or if the company's My Domain subdomain changes. Hardcoding it works for stable production orgs but breaks if the URL ever changes. The safest approach is to retrieve it from the OAuth token response and store it in App State, replacing it each time the app re-authenticates.

### Can I use Salesforce Communities or Experience Cloud instead of the REST API?

For FlutterFlow integrations, the Salesforce REST API is the correct approach — it gives you direct access to all standard and custom objects. Salesforce Experience Cloud (formerly Communities) is designed for web portals, not mobile API integrations. The REST API combined with a Connected App and OAuth is the standard path for mobile development.

### What happens if the FlutterFlow app hits Salesforce's daily API limit?

When your organization exceeds its daily API call limit, Salesforce returns a 503 error with the message 'REQUEST_LIMIT_EXCEEDED'. To avoid this, fetch data only on demand (not on an auto-refresh timer), cache results in App State between page navigations, and minimize the number of API calls per user session. Monitor API usage in Salesforce Setup → System Overview → API Calls Used.

### Can I write to custom Salesforce objects from FlutterFlow?

Yes. Custom objects in Salesforce are accessible via the same REST API at `/services/data/v60.0/sobjects/CustomObject__c` (with the `__c` suffix for custom objects). The SOQL query syntax also works for custom objects: `SELECT Id, CustomField__c FROM CustomObject__c`. Use Object Manager in Salesforce Setup to get the exact API names for your custom objects and fields.

---

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