# Facebook Ads

- Tool: FlutterFlow
- Difficulty: Advanced
- Time required: 2 to 3 hours
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to Facebook Ads by routing Meta Marketing API calls through a Firebase Cloud Function or Supabase Edge Function that holds your System User access token. The token never touches the Flutter client bundle. Your FlutterFlow API Group calls the proxy, which queries the Meta Insights API and returns campaign performance data you parse and bind to dashboard widgets.

## Build a Facebook Ads Dashboard in FlutterFlow

The Meta Marketing API's Insights endpoint lets you pull campaign-level, ad-set-level, and ad-level performance metrics — spend, impressions, clicks, CPM, CTR, ROAS — for any date range. For a FlutterFlow founder this means building a branded in-app dashboard where you or your clients see Facebook and Instagram campaign performance without opening Ads Manager. The API itself is free; you pay only for actual ad spend billed by Meta.

The architectural constraint that shapes this entire integration is client-side security. FlutterFlow generates a Flutter app that runs on the user's device. Anything placed in an API Call header — including a Meta access token — is compiled into the app binary. Anyone can inspect that binary. Meta's System User access token is especially sensitive because it grants read access to all campaigns, ad accounts, and audiences linked to your Business Manager. It must never appear in FlutterFlow's API Call configuration. Instead, you create a thin proxy — a Firebase Cloud Function or Supabase Edge Function — that holds the token in server environment variables and relays Insights requests on behalf of your app.

The standout advantage of the System User token (versus a standard OAuth user token) is longevity. Standard user tokens expire every 60 days and require a re-auth flow. A System User token from Meta Business Manager does not expire, which means your proxy keeps working indefinitely without any token refresh logic. The tradeoff is setup time: you need Business Manager admin access to create the System User, assign it the correct ad account permissions, and generate the token. Once done, the integration is stable. The Insights API caps at 200 calls per hour per ad account, shared across all sessions of your app — aggressive caching in Firestore or Supabase is mandatory for any app with multiple concurrent users.

## Before you start

- A Meta Business Manager account with at least one active ad account
- Business Manager admin access to create a System User and generate a non-expiring access token
- A Firebase project with Cloud Functions on Blaze plan or a Supabase project with Edge Functions enabled
- Your ad account ID in the format act_XXXXXXXXXX (visible in Ads Manager URL or Business Settings)
- A FlutterFlow project with an authenticated user flow already working

## Step-by-step guide

### 1. Create a System User in Meta Business Manager and generate a non-expiring token

Go to business.facebook.com, open your Business Manager, and navigate to Business Settings → Users → System Users. Click Add and create a new system user — give it a descriptive name like 'FlutterFlow Dashboard'. Set the role to Employee (not Admin) unless you need write access. After creating the system user, click Add Assets, select Ad Accounts, choose your ad account, and grant at least Analyze permissions (read-only). Now click Generate New Token next to the system user. In the token dialog, select your app (or create a new Meta developer app at developers.facebook.com/apps) and check the ads_read permission scope at minimum. Generate the token. This token is non-expiring — it does not rotate after 60 days like a standard user OAuth token — which means your Firebase proxy will keep working indefinitely without any refresh logic. Copy the token immediately; you will store it only in your backend environment variables, never anywhere a client app can access. Copy your ad account ID as well (found in Business Settings → Ad Accounts, formatted as act_XXXXXXXXXX). You will pass the ad account ID as a variable from FlutterFlow; the token stays server-side.

**Expected result:** You have a System User with ads_read permission on your ad account, a non-expiring access token copied to a secure location, and your ad account ID ready for the next step.

### 2. Deploy a Firebase Cloud Function proxy for the Meta Insights API

In your Firebase project, create an HTTP Cloud Function that accepts ad account ID and date range parameters from FlutterFlow, appends the System User access token from environment variables, and queries the Meta Graph API Insights endpoint. The Meta Insights endpoint URL format is https://graph.facebook.com/v19.0/{ad_account_id}/insights. Your proxy must include CORS headers because FlutterFlow web builds run in a browser. The function requests only the fields your dashboard needs — requesting more fields wastes quota from the 200-calls-per-hour ceiling. Use time_increment=1 in the query parameters to get one data row per day, which is the right granularity for trend charts. After writing the function, deploy it from your local machine using the Firebase CLI. Copy the HTTPS trigger URL from the Firebase Console after deployment — this is what you will paste into FlutterFlow as the API Group Base URL. Store the access token using Firebase Secret Manager or the functions config system so it appears nowhere in your code. Alternatively, use a Supabase Edge Function with identical fetch logic — the Deno runtime and CORS headers work the same way, only the deployment method differs.

```
// functions/index.js (Firebase Cloud Functions — Node.js)
const functions = require('firebase-functions');
const fetch = require('node-fetch');

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

  const ACCESS_TOKEN = process.env.META_SYSTEM_USER_TOKEN;
  const { ad_account_id, start_date, end_date, level } = req.body;

  if (!ad_account_id || !start_date || !end_date) {
    res.status(400).json({ error: 'Missing required fields' });
    return;
  }

  const params = new URLSearchParams({
    level: level || 'campaign',
    fields: 'campaign_name,spend,impressions,clicks,ctr,cpm,reach',
    time_range: JSON.stringify({ since: start_date, until: end_date }),
    time_increment: '1',
    limit: '100',
    access_token: ACCESS_TOKEN
  });

  const metaRes = await fetch(
    `https://graph.facebook.com/v19.0/${ad_account_id}/insights?${params}`
  );
  const data = await metaRes.json();
  res.json(data);
});
```

**Expected result:** The Cloud Function is deployed and returns Meta Insights campaign data as JSON when called with a POST body containing ad_account_id, start_date, and end_date.

### 3. Create a FlutterFlow API Group pointing at your proxy

In FlutterFlow, open the left navigation panel and click API Calls. Click + Add and choose Create API Group. Name it MetaInsights and paste your Firebase Cloud Function HTTPS trigger URL as the Base URL. Do not add any Authorization headers to the API Group — the System User token lives on the server and never appears in FlutterFlow's configuration. Click + Add inside the group to create an individual API Call. Name it getCampaignInsights, set the method to POST, and leave the endpoint path blank since the Base URL is the full endpoint. Switch to the Body tab and select JSON. Add four body variables: ad_account_id (String), start_date (String), end_date (String), and level (String — default value 'campaign'). Your JSON body template: { "ad_account_id": "{{ad_account_id}}", "start_date": "{{start_date}}", "end_date": "{{end_date}}", "level": "{{level}}" }. Go to the Response & Test tab and fill in a real ad account ID (in act_XXXXXXXXXX format), real date values, and click Test. The response should include a data array with objects containing campaign_name, spend, impressions, clicks, and date_start. Click Generate from Response to auto-create JSON Path expressions. Review the generated paths and confirm they point to the correct fields inside the data array.

```
{
  "ad_account_id": "{{ad_account_id}}",
  "start_date": "{{start_date}}",
  "end_date": "{{end_date}}",
  "level": "{{level}}"
}
```

**Expected result:** The getCampaignInsights API Call test returns a 200 response with a populated data array of campaign metrics in the FlutterFlow test panel.

### 4. Build the Data Type and bind Insights data to dashboard widgets

Create a new custom Data Type in FlutterFlow called CampaignInsight with these fields: campaignName (String), spend (Double), impressions (Integer), clicks (Integer), ctr (Double), cpm (Double), reach (Integer), dateStart (String). Return to your getCampaignInsights API Call and use the JSON Path section to map each path to the corresponding Data Type field: $.data[*].campaign_name to campaignName, $.data[*].spend to spend, $.data[*].impressions to impressions, $.data[*].date_start to dateStart, and so on. Note that Meta returns spend and ctr as strings in the Insights response — cast them to Double in FlutterFlow's type selector. Build a new dashboard screen. Add a Row at the top with a DateRangePicker widget or two date text fields so users can select the reporting period, storing the selected dates in AppState variables. Below that, add a ListView with its source set to Backend Query → getCampaignInsights, passing the ad_account_id from the authenticated user's data, and the date values from AppState. Inside each ListView item, add a Card with Text widgets bound to campaignName (bold, large) and a subtitle showing 'Spend: $' + spend formatted to two decimal places. Add a bar Chart widget above the list binding the spend series to the selected date range for a visual trend. For the ad account ID, store it in the user's Supabase or Firestore profile record and read it from the logged-in user's data so each user sees their own account.

**Expected result:** The dashboard screen shows a list of Facebook campaigns with spend, impressions, and CTR for the selected date range, loaded from your proxy.

### 5. Add Firestore caching to respect the 200-calls/hour Insights limit

The Meta Insights API enforces a hard limit of 200 calls per hour per ad account, shared across all users and sessions of your app. If ten users open the dashboard simultaneously without caching, that is ten API calls for the same data — and 20 requests per hour easily consumes 10% of your hourly quota. When the limit is reached, Meta returns error code 17 or 32: 'User request limit reached'. The solution is to cache the Insights response in Firestore inside your Cloud Function and serve cached data to concurrent sessions. Check the cache document before calling Meta; if it is under 30 minutes old, return it immediately. If stale, fetch fresh data, update Firestore, and return the new response. Document the cached_at timestamp in the response so the FlutterFlow app can display a 'Data as of {time}' indicator. Add a force_refresh Boolean body variable to the API Call so a pull-to-refresh gesture can bypass the cache when the user explicitly needs current numbers. Update your Cloud Function to implement the caching check as shown in the code block. If you want help setting up the caching layer or handling multiple ad accounts, RapidDev's team builds FlutterFlow integrations like this every week — free scoping call at rapidevelopers.com/contact.

```
// Caching layer to add inside facebookInsights function
const admin = require('firebase-admin');
if (!admin.apps.length) admin.initializeApp();
const db = admin.firestore();

const { ad_account_id, start_date, end_date, force_refresh } = req.body;
const cacheKey = `${ad_account_id}_${start_date}_${end_date}`;
const cacheRef = db.collection('meta_insights_cache').doc(cacheKey);
const cached = await cacheRef.get();

if (cached.exists && !force_refresh) {
  const { payload, cached_at } = cached.data();
  const ageMs = Date.now() - cached_at;
  if (ageMs < 30 * 60 * 1000) { // 30 minutes
    res.json({ ...payload, from_cache: true, cached_at });
    return;
  }
}

// ... Meta API fetch ...
// After receiving response:
await cacheRef.set({ payload: metaData, cached_at: Date.now() });
res.json({ ...metaData, from_cache: false });
```

**Expected result:** Concurrent user sessions return cached campaign data immediately, and Meta API calls are reduced to at most one per 30 minutes per unique account-and-date-range combination.

## Best practices

- Use a System User token from Meta Business Manager rather than a standard OAuth user token — it never expires after 60 days and eliminates the need for a token refresh loop in your proxy.
- Never place the System User token in a FlutterFlow API Call header — it compiles into the app binary and can be extracted by decompiling the APK or IPA.
- Request only the Insights fields you actually display in your dashboard — the 'fields' parameter directly controls response size and API quota consumption.
- Cache every Insights response for at least 15 to 30 minutes to stay within the 200-calls-per-hour-per-ad-account ceiling across concurrent users.
- Include a 'Last updated' timestamp in your cached response and display it on the dashboard so users know data freshness at a glance.
- Use time_increment=1 in Insights queries to get daily rows — it enables trend charts without making separate per-day requests.
- For iOS 14.5+ in-app event attribution, implement App Tracking Transparency consent before firing any Meta pixel or conversion events — attribution is severely limited without it.
- Create a dedicated Meta developer app for this integration so you can manage permissions independently from any personal apps or other projects.

## Use cases

### Multi-client ad performance portal for a marketing agency

A marketing agency builds a FlutterFlow mobile app where each client logs in with their own account and sees their Facebook and Instagram campaign performance — spend, CPM, CTR, ROAS — refreshed every 30 minutes from a cached proxy. The System User token gives the agency access to all client ad accounts through one non-expiring credential.

Prompt example:

```
Build a screen with a dropdown to select an ad account, then a list of campaigns below it showing spend and impressions for the last 30 days, loaded from a Firebase proxy.
```

### E-commerce app with built-in ad ROI monitoring

A DTC brand builds a FlutterFlow app that shows their Meta ad spend alongside Shopify revenue in a single dashboard, so the founder can check ROAS on their phone each morning without switching between platforms. Both data sources hit different proxy endpoints and merge into a unified summary screen.

Prompt example:

```
Create a dashboard with a spend vs revenue bar chart for the last 14 days, where spend comes from the Facebook Ads proxy and revenue comes from a Supabase table.
```

### Lead-gen app tracking Facebook campaign performance by campaign name

A real estate agent builds a FlutterFlow app that shows which Facebook ad campaigns are generating the most lead form fills, with daily spend and lead count per campaign, updated each morning from the Insights API through a scheduled cache refresh.

Prompt example:

```
Show a list of Facebook campaigns sorted by lead count descending, with spend and CPL for the current month, refreshed daily from a backend proxy.
```

## Troubleshooting

### Meta API returns error code 17 or 32: 'User request limit reached'

Cause: The 200-calls-per-hour-per-ad-account Insights API ceiling has been exhausted. This happens when multiple users load the dashboard simultaneously without a caching layer, or when a developer tests the integration repeatedly in a short window.

Solution: Implement the Firestore caching layer described in Step 5. Serve cached data for at least 30 minutes to reduce upstream calls. If the error occurs despite caching, check that all paths through your proxy code actually read from the cache — a code bug might be bypassing it. Reduce the number of fields in the 'fields' query parameter to lessen response size and keep per-call weight lower.

### 'XMLHttpRequest error' in the FlutterFlow test panel or on web build

Cause: CORS is blocking the request. This happens when the FlutterFlow API Group Base URL points directly to graph.facebook.com instead of your Cloud Function proxy, or when the proxy is missing the Access-Control-Allow-Origin response header.

Solution: Ensure the Base URL in your FlutterFlow API Group is your Cloud Function URL, not graph.facebook.com. Confirm the function includes 'res.set("Access-Control-Allow-Origin", "*")' on every response path including error returns. Also confirm the function handles OPTIONS preflight with a 204 response and the same headers.

### The Insights response data array is empty even though campaigns have spend

Cause: The date range passed to the Insights API falls outside any campaign activity, or the ad_account_id format is wrong (missing the 'act_' prefix), or the System User does not have Analyze permission on the correct ad account.

Solution: In Meta Business Settings → System Users, click your system user and check its Ad Account assets — confirm the specific ad account is listed with Analyze or higher permission. Verify the ad_account_id format is act_XXXXXXXXXX, not just the numeric ID. Test with a date range when you know campaigns were active and had spend.

### Token works in Postman but Meta returns 190 'Invalid OAuth access token' from the proxy

Cause: The access token string contains extra whitespace, a newline character, or was truncated when copied from Business Manager. This is especially common when the token is set via an environment variable with trailing spaces.

Solution: In your Firebase function, add a console.log of ACCESS_TOKEN.length and compare it against the token length you copied. Trim whitespace: const token = process.env.META_SYSTEM_USER_TOKEN?.trim(). Re-set the environment variable being careful not to include any surrounding quotes or spaces in the value.

```
const ACCESS_TOKEN = process.env.META_SYSTEM_USER_TOKEN?.trim();
```

## Frequently asked questions

### Why use a System User token instead of a regular Facebook login OAuth token?

A standard user OAuth token from the Facebook Login flow expires every 60 days and requires the user to re-authenticate. A System User token from Meta Business Manager is non-expiring — once generated, it works indefinitely without any refresh logic in your proxy. This makes your FlutterFlow dashboard integration maintenance-free once deployed, as long as the System User retains its ad account permissions.

### Can I display data from multiple Facebook ad accounts in one FlutterFlow app?

Yes. Store each user's ad_account_id in your database linked to their authenticated profile. Pass the appropriate account ID to the proxy on each API call. The System User token can access multiple ad accounts as long as it has been granted Analyze permission on each one in Business Manager. Use the account ID as part of the Firestore cache key so each account's data is cached separately.

### What happens if a user sees stale data because of caching?

Add a pull-to-refresh gesture on the dashboard screen and pass force_refresh: true in the API Call body to bypass the cache when the user explicitly requests a refresh. Also display a 'Last updated {timestamp}' label on the screen using the cached_at value returned by your proxy. Users quickly learn to expect near-real-time data with a manual refresh option rather than instant live data.

### Can I use this integration to create or edit Facebook ads from within FlutterFlow?

The Meta Marketing API supports write operations like creating campaigns, ad sets, and ads, but building a full ad-creation interface is a significant project. You would need to add POST endpoints to your proxy for each object type and handle Meta's complex campaign hierarchy (campaign → ad set → ad → creative). Read-only reporting is the practical starting point for most FlutterFlow founders.

### Do I need to handle iOS App Tracking Transparency for this integration?

ATT consent is required if you use the Meta SDK (facebook_app_events pub.dev package) for in-app event tracking. For the reporting-only use case covered in this tutorial — pulling Insights API data into a dashboard — ATT is not required because you are reading aggregated campaign statistics, not tracking individual user identities. ATT becomes relevant only when you fire pixel events or use the native Meta SDK for attribution.

---

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