# SEMrush

- Tool: FlutterFlow
- Difficulty: Intermediate
- Time required: 50 minutes
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to SEMrush by creating an API Group pointing at https://api.semrush.com with your API key passed as a ?key= query parameter. Because SEMrush returns semicolon-delimited CSV instead of JSON, you need a Dart Custom Action to parse the response into a list. Route the key through a Firebase Cloud Function proxy so it never ships inside the compiled app.

## Build a SEMrush Analytics App in FlutterFlow — Including CSV Parsing

SEMrush's Analytics API is genuinely unusual compared to most REST APIs you will encounter. Instead of returning JSON, it returns plain-text semicolon-delimited CSV — a row of header names followed by rows of data, each value separated by a semicolon. That means FlutterFlow's built-in JSON Path extractor cannot parse the response directly, and you need to handle the CSV in a Dart Custom Action. This extra step is the signature challenge of the SEMrush integration, and getting it right is what makes the tutorial non-interchangeable with the Ahrefs or Moz pages.

Authentication is also different from bearer-token APIs. SEMrush identifies your account via an ?key= query parameter appended to the URL, alongside other parameters like ?type=domain_ranks&domain=example.com&export_columns=Or,Ot,Oc,Ad,At,Ac. Every call costs API units from your monthly quota — domain overview reports consume roughly 10 units per data row returned, and keyword reports are higher. You therefore need the same unit-conservation discipline as with Ahrefs: call on demand, not on every widget rebuild.

The API key in a URL query string is arguably even more exposed than a Bearer header, because query strings appear in server logs, browser history, and network proxies by default. In a FlutterFlow app, a key in a query variable is embedded in the compiled Dart code. Routing all calls through a Firebase Cloud Function proxy is the correct pattern: the proxy URL is public, but the actual SEMrush key never leaves your Cloud Function's environment.

## Before you start

- A SEMrush subscription with the Analytics API add-on enabled (API units are a separate add-on on top of your seat plan)
- Your SEMrush API key from SEMrush account settings → API
- A FlutterFlow project with at least one screen for displaying analytics data
- A Firebase project for the Cloud Function proxy that will hold your SEMrush API key
- Basic familiarity with FlutterFlow's API Calls panel and Custom Code (Custom Actions)

## Step-by-step guide

### 1. Deploy a Firebase Cloud Function proxy to protect the SEMrush API key

SEMrush authenticates via an API key passed as a query parameter (?key=...) in the URL. In a compiled Flutter app, any query variable value is embedded in the binary and is recoverable by decompiling the APK or IPA. Beyond app security, the key also appears in server logs at any intermediate HTTP proxy. Routing calls through a Firebase Cloud Function eliminates both risks: the function URL is public, but the SEMrush key stays in the function's environment.

In the Firebase console, create a new Cloud Functions project (or add a function to an existing project). Write an HTTP function that receives the report type, domain (or keyword), and optional column parameters from FlutterFlow as query parameters. The function reads the SEMrush API key from its environment (process.env.SEMRUSH_KEY), builds the full SEMrush URL with ?key=, calls it using the built-in https module or axios, and returns the response body (CSV text) to FlutterFlow with a content-type of text/plain.

Set the key with: firebase functions:config:set semrush.key='YOUR_KEY_HERE'. Also add Access-Control-Allow-Origin: * headers to the function response so FlutterFlow's web build (Run mode) can call it from the browser without a CORS error. Deploy with firebase deploy --only functions and confirm the function URL is active in the Firebase console.

```
// Firebase Cloud Function proxy for SEMrush (functions/index.js)
const functions = require('firebase-functions');
const https    = require('https');

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

  const key     = process.env.SEMRUSH_KEY;
  const type    = req.query.type    || 'domain_ranks';
  const domain  = req.query.domain  || '';
  const phrase  = req.query.phrase  || '';
  const columns = req.query.columns || 'Or,Ot,Oc,Ad,At,Ac';
  const db       = req.query.db      || 'us';

  const param = type.startsWith('phrase') ? `phrase=${phrase}` : `domain=${domain}`;
  const url = `https://api.semrush.com/?type=${type}&key=${key}&${param}&export_columns=${columns}&database=${db}&display_limit=10`;

  https.get(url, (apiRes) => {
    let data = '';
    apiRes.on('data', chunk => data += chunk);
    apiRes.on('end', () => {
      if (data.startsWith('ERROR')) {
        return res.status(400).send(data);
      }
      res.set('Content-Type', 'text/plain');
      res.send(data);
    });
  }).on('error', err => res.status(500).send(err.message));
});
```

**Expected result:** The Cloud Function is deployed and returns raw CSV text when you access its URL in a browser with ?type=domain_ranks&domain=example.com.

### 2. Create the SEMrush API Group in FlutterFlow

With the proxy deployed, you will now create an API Group in FlutterFlow that points to your Cloud Function URL. All API Calls you create inside this group inherit the base URL automatically, keeping configuration centralized.

Open FlutterFlow and navigate to API Calls in the left navigation panel. Click + Add → Create API Group. Name it SEMrush. In the Base URL field, paste your Cloud Function URL: https://us-central1-YOUR_PROJECT.cloudfunctions.net/semrushProxy.

You do not need to add any headers at the group level because your proxy handles authentication with the SEMrush key internally. However, if you want to restrict access to your proxy so only your app can call it, add a custom header such as X-App-Key with a random UUID that your Cloud Function checks and rejects if absent. This is optional but recommended for production deployments.

Save the group. It will appear in the API Calls list in the left nav, ready for you to add individual API Call definitions inside it.

**Expected result:** The SEMrush API Group is visible in the left nav with your Cloud Function URL as its base. No individual calls are defined inside it yet.

### 3. Add API Calls for domain_ranks and phrase_this

Inside the SEMrush group, create individual API Calls for each report type you need. Because your proxy accepts a type parameter, you can design a single flexible API Call or separate calls per report type — separate calls are cleaner for widget binding.

Click + inside the SEMrush group → Create API Call. Name it Get Domain Ranks. Set the method to GET. In the URL field (relative to the base), leave it as / or blank (since the proxy accepts everything on its root). Go to the Variables tab and add:
  - type (String, default: domain_ranks)
  - domain (String, required)
  - db (String, default: us — the regional database: us, uk, de, etc.)
  - columns (String, default: Or,Ot,Oc,Ad,At,Ac)

In the URL field, reference them: /?type={{type}}&domain={{domain}}&db={{db}}&columns={{columns}}.

Switch to Response & Test. Fill in a domain and click Test. You will get CSV text back in the response body — something like:
Or;Ot;Oc;Ad;At;Ac
4200;82000;45000;120;3200;1800

FlutterFlow's JSON Path tool will not be useful here because there is no JSON. Instead, note the raw response text in the test panel. You will parse it in a Custom Action in the next step. For now, just verify that the call returns CSV data (not an error string) and save.

```
// API Call URL pattern (set in FlutterFlow Variables tab)
// Base: https://us-central1-YOUR_PROJECT.cloudfunctions.net/semrushProxy
// Relative URL: /?type={{type}}&domain={{domain}}&db={{db}}&columns={{columns}}
//
// Example resolved URL (domain_ranks, US database):
// /?type=domain_ranks&domain=example.com&db=us&columns=Or,Ot,Oc,Ad,At,Ac
//
// Example resolved URL (phrase_this, keyword lookup):
// /?type=phrase_this&phrase=flutter+app+builder&db=us&columns=Ph,Nq,Cp,Co,Nr,Td
```

**Expected result:** The Get Domain Ranks API Call is saved and the test panel shows raw CSV rows when tested with a real domain.

### 4. Write a Dart Custom Action to parse the CSV response

This step is unique to SEMrush and is the key differentiator from the other SEO tool integrations on this page. Because SEMrush returns CSV, you need a Dart Custom Action that takes the raw response string, splits it into lines, splits each line by semicolon, maps the values to field names from the header row, and returns a list of Data Type objects that FlutterFlow can bind to widgets.

In FlutterFlow, navigate to Custom Code in the left nav → + Add → Action. Name it parseSEMrushCSV. Set the return type to a list of your custom Data Type (see below). Add one argument: csvText (String) — this is the raw response from the API Call.

First, define a Data Type. Go to Data Types (left nav) and create SemrushDomainRow with fields matching the export columns: organicKeywords (int), organicTraffic (int), organicCost (double), paidKeywords (int), paidTraffic (int), paidCost (double).

Then write the Custom Action Dart code. The function splits the CSV on newline to get rows, takes the first row as headers, and maps each subsequent row to a SemrushDomainRow Data Type. Early return if the response starts with 'ERROR' to surface the SEMrush error message in the UI rather than crashing.

In the Action Flow Editor on your Search button, add the sequence: first call the API, then pass the response body string to parseSEMrushCSV, then store the returned list in App State, then update the ListView data source from App State.

```
// Custom Action: parseSEMrushCSV
// Arguments: csvText (String)
// Return: List<SemrushDomainRow>

import 'package:flutter/material.dart';
import '/flutter_flow/flutter_flow_util.dart';

Future<List<SemrushDomainRowStruct>> parseSemrushCsv(
  String csvText,
) async {
  final List<SemrushDomainRowStruct> results = [];

  // SEMrush error responses start with 'ERROR'
  if (csvText.trim().isEmpty || csvText.startsWith('ERROR')) {
    return results;
  }

  final lines = csvText.trim().split('\n');
  if (lines.length < 2) return results; // header only, no data

  // lines[0] is the header row: Or;Ot;Oc;Ad;At;Ac
  // lines[1+] are data rows
  for (int i = 1; i < lines.length; i++) {
    final cols = lines[i].split(';');
    if (cols.length < 6) continue;

    results.add(SemrushDomainRowStruct(
      organicKeywords: int.tryParse(cols[0]) ?? 0,
      organicTraffic:  int.tryParse(cols[1]) ?? 0,
      organicCost:     double.tryParse(cols[2]) ?? 0.0,
      paidKeywords:    int.tryParse(cols[3]) ?? 0,
      paidTraffic:     int.tryParse(cols[4]) ?? 0,
      paidCost:        double.tryParse(cols[5]) ?? 0.0,
    ));
  }

  return results;
}
```

**Expected result:** The parseSEMrushCSV custom action is saved and shows no compile errors in FlutterFlow. When triggered in an action sequence after the API Call, it returns a list of SemrushDomainRow objects.

### 5. Build the results UI and wire the full action flow

With the API Group, API Calls, CSV parser Custom Action, and Data Types all in place, you can now wire everything together in a complete UI.

On your main screen, add a TextField for domain input and a Search button. In the Search button's Actions panel, add the following action sequence:
  1. Update a loading state variable (isLoading = true) to show a progress spinner.
  2. Backend/API Call → SEMrush → Get Domain Ranks, passing the TextField value as domain.
  3. Custom Action → parseSEMrushCSV, passing the API Call's response body as the csvText argument.
  4. Update App State to store the returned list in an app state variable called semrushRows (type: List of SemrushDomainRow).
  5. Set isLoading = false.
  6. If the API Call returned an error or if semrushRows is empty, show an Alert dialog with a user-friendly message.

For the display, add a Column with metric Text widgets bound to AppState.semrushRows[0].organicKeywords, organicTraffic, etc. (use the first row, since domain_ranks returns a single aggregate row). Below, add a ListView for keyword reports (phrase_this) that maps multiple rows to list items. Use Conditional Visibility on the results column so it only appears after data is loaded.

**Expected result:** The full action flow works end to end: user types a domain, taps Search, the proxy is called, CSV is parsed, and metric cards show organic keywords, traffic, and cost figures from SEMrush.

## Best practices

- Never put the SEMrush API key in a FlutterFlow query variable — it will appear in the app bundle and in your Cloud Function's server logs as a plaintext URL query string. Always proxy through Firebase Cloud Functions or Supabase Edge Functions.
- Handle the 'ERROR' prefix responses from SEMrush explicitly in your Dart Custom Action — SEMrush returns text errors, not HTTP error codes, so your parser must detect and surface them gracefully rather than returning an empty list.
- Limit display_limit in the proxy URL (set it to 10-25 rows) to control the volume of data and units consumed per request, especially for keyword list reports.
- Cache parsed results in App State with a timestamp, and only re-fetch on explicit user action — each SEMrush call consumes units, and accidental double-fetches add up quickly.
- Add a regional database selector (us, uk, de, etc.) to your UI as a dropdown bound to an App State variable, so users can switch regional data without modifying any code.
- Show the raw error string from SEMrush in a Snackbar or Dialog when the parser detects an 'ERROR' response — this saves hours of debugging when units are exhausted or a domain is misspelled.
- Test your CSV parser with edge-case inputs: an empty body, a single header row with no data rows, and a response that starts with 'ERROR' — all are valid SEMrush responses.

## Use cases

### Domain organic traffic overview app

Build an internal marketing app where your team types any domain and instantly sees its SEMrush domain_ranks data — organic keyword count, organic traffic estimate, organic cost, and paid traffic. The app calls the proxy which fires the type=domain_ranks query, receives the CSV, parses it in a Dart Custom Action, and displays the result in a clean metric card. Results are cached in App State after the first call.

Prompt example:

```
An app where a user enters a domain and sees its organic keywords count, estimated monthly traffic, and ad traffic pulled from SEMrush domain overview data.
```

### Keyword difficulty and volume lookup tool

Build a keyword research companion app that lets a user type a search phrase and see monthly volume, CPC, competition level, and trend data from SEMrush's phrase_this report. The Dart Custom Action splits the CSV response into rows and columns, maps each column to a field in a Data Type, and displays results in a card list. Users can save keywords to a local Firestore or Supabase table for later review.

Prompt example:

```
A keyword research app where a user searches a keyword phrase and sees monthly search volume, CPC, competition score, and trend data from SEMrush.
```

### Competitor organic keyword gap report

Build a screen that accepts two domains and fetches domain_ranks for each in parallel via two API Calls, then displays a side-by-side comparison of organic keyword count and traffic. The app parses both CSV responses with the same Custom Action and renders a comparison table, giving an instant read on which domain dominates organic search for a given market.

Prompt example:

```
A competitor comparison app that shows organic keyword count and estimated traffic for two domains side by side, pulled from SEMrush domain overview data.
```

## Troubleshooting

### 'ERROR 50 :: NOTHING FOUND' returned instead of data

Cause: SEMrush returns this string (not an HTTP error code) when the domain has no data in the selected regional database, or when your API units are fully exhausted for the month.

Solution: Check that the domain is spelled correctly and exists in the regional database you selected (try db=us if you used a regional variant). Log into your SEMrush account and verify your remaining API units under Account → API. If units are zero, the API returns this error for all requests until the next billing cycle.

### XML​HttpRequest error in FlutterFlow Run mode — no response from the API Call

Cause: CORS error. Calling api.semrush.com directly from a browser (FlutterFlow's web Run mode) is blocked. This error appears when the API Group base URL points to api.semrush.com instead of your Cloud Function proxy.

Solution: Ensure the API Group's base URL is your Firebase Cloud Function URL, not https://api.semrush.com. The Cloud Function sets Access-Control-Allow-Origin: * so browser-based calls succeed. Update the base URL in the API Group settings if necessary.

### CSV parser Custom Action fails to compile — 'SemrushDomainRowStruct is not defined'

Cause: The SemrushDomainRowStruct type is generated by FlutterFlow from your custom Data Type definition. If the Data Type was not saved before writing the Custom Action, or if the type name was spelled differently, the generated struct name won't match.

Solution: Go to Data Types in the left nav and confirm a type called SemrushDomainRow exists with the expected fields. FlutterFlow appends 'Struct' to the type name when generating Dart code, so SemrushDomainRow becomes SemrushDomainRowStruct. Check spelling carefully and ensure you have saved the Data Type before compiling the Custom Action.

### 'ERROR 110 :: INCORRECT PERIOD' returned for keyword reports

Cause: SEMrush phrase-level reports (phrase_this, phrase_related) require a database parameter that matches a supported regional database string. An invalid or missing database parameter triggers this error.

Solution: Confirm the db parameter is set to a valid SEMrush database code — common ones are us, uk, de, fr, es, au. Do not pass an empty string for db. Update your API Call variable to default to us if no value is supplied.

## Frequently asked questions

### Why does SEMrush return CSV instead of JSON — can I make it return JSON?

SEMrush's Analytics API is a legacy API that returns CSV by design. There is no JSON output option for the core analytics endpoints (domain_ranks, phrase_this, etc.). The SEMrush Position Tracking and Projects APIs do return JSON, but those are different APIs for different use cases. For the analytics data described in this guide, you must parse the CSV response, which is why the Dart Custom Action is a required step.

### Do I need an API units add-on or is it included in my SEMrush subscription?

API units are a separate add-on. A standard SEMrush Pro, Guru, or Business subscription does not include API access by default — you need to purchase the API Units add-on. The cost and unit allotment vary by plan; check your SEMrush account under Subscription or contact SEMrush support for current pricing. Running the API from a plan without the add-on will return 'ERROR' responses for all calls.

### Can I use FlutterFlow's built-in JSON Path to parse SEMrush responses?

No. FlutterFlow's JSON Path extractor in the API Call Response & Test tab only works with JSON-formatted responses. Since SEMrush returns semicolon-delimited CSV text, JSON Path has nothing to parse. You must write a Dart Custom Action (as shown in Step 4) to split the CSV into rows and map field values to a FlutterFlow Data Type.

### Will this integration work in FlutterFlow's web Run mode?

Yes, as long as you use the Cloud Function proxy. Direct calls to api.semrush.com from the browser fail due to CORS. The proxy sets permissive CORS headers and returns the CSV text, allowing FlutterFlow's browser-based Run mode and web exports to work identically to native mobile builds.

### How many API units does a typical domain overview call cost?

A domain_ranks call costs approximately 10 units per row of data returned. At display_limit=10, that is roughly 100 units per call. Verify the exact unit cost in your SEMrush account documentation, as it varies by report type and can change with SEMrush plan updates. Monitor your unit consumption in the SEMrush API dashboard to avoid unexpected exhaustion.

---

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