Connect Bubble to SEMrush by adding your API key as a Private URL parameter named `key` (not an Authorization header) in the API Connector. The critical challenge: SEMrush returns semicolon-delimited plain text for most reports — not JSON. Parsing this format requires the Toolbox plugin's Server Script action in a Backend Workflow. SEMrush API access requires a Pro plan or above.
| Fact | Value |
|---|---|
| Tool | SEMrush |
| Category | Marketing |
| Method | Bubble API Connector |
| Difficulty | Intermediate |
| Time required | 2–3 hours |
| Last updated | July 2026 |
SEMrush + Bubble: Competitive SEO Intelligence Dashboard
SEMrush is one of the most valuable SEO tools to surface in a Bubble app — but it's also one of the more technically unusual APIs to integrate. Two things distinguish it from standard REST APIs: the API key goes in the URL query string (not an Authorization header), and responses come back as semicolon-delimited text files, not JSON. Bubble's API Connector expects JSON responses, so the text format requires an extra parsing step.
The good news: Bubble's API Connector can still keep your API key secure. URL parameters can be marked Private just like headers — the key never reaches the browser even though it's technically a query string parameter. This is the same privacy guarantee as header-based authentication for all practical purposes.
For the text parsing challenge, the most reliable Bubble-native solution is the Toolbox plugin's Server Script action. A Server Script can receive the raw SEMrush text response, split it by newlines and semicolons, and return a structured object that subsequent workflow steps can use. This runs entirely server-side inside Bubble's infrastructure.
The use case most Bubble founders want is a competitive SEO intelligence dashboard: organic traffic estimates for your domain alongside 2-3 competitor domains, keyword ranking counts, and ad spend estimates. Since SEMrush charges API units per call (and per row for some reports), caching results in a Bubble database and refreshing daily rather than on every page load is essential for cost management.
Integration method
Call the SEMrush API at https://api.semrush.com with the API key as a Private URL parameter, then parse the semicolon-delimited text response using the Toolbox plugin's Server Script action in a Backend Workflow.
Prerequisites
- A SEMrush account on the Pro plan or above — SEMrush API access is not available on the free plan. API units are allocated per plan (Pro, Guru, Business)
- Your SEMrush API key, found at SEMrush Account Settings → API Key (requires a paid subscription)
- The API Connector plugin installed in your Bubble app (Plugins tab → Add plugins → 'API Connector' by Bubble)
- The Toolbox plugin installed in your Bubble app (Plugins tab → Add plugins → search 'Toolbox' — free, by Bubble)
- A Bubble app on a paid plan if using Backend Workflows for scheduled data refresh (Starter or above)
Step-by-step guide
Understand SEMrush API response format before configuring Bubble
Before opening Bubble, test the SEMrush API in your browser to understand what you're working with. Open this URL (replace YOUR_API_KEY and example.com with your own): `https://api.semrush.com/?type=domain_ranks&key=YOUR_API_KEY&domain=example.com&database=us&export_columns=Or,Ot,Oc,Ad,At,Ac` You'll see a response like: ``` Or;Ot;Oc;Ad;At;Ac 12345;450000;3.50;200;15000;2.00 ``` The first line is a semicolon-delimited header row. The second line (and any following lines) are the data rows. Each value maps to its column header. Bubble's API Connector cannot parse this format natively — it expects JSON. This is why parsing is a mandatory step, not optional. Also note the domain format: SEMrush requires the bare domain without protocol or trailing slash. Enter `example.com` not `https://www.example.com/`. This is the #1 cause of the 'ERROR 50: NOTHING FOUND' response — the domain format is wrong, not a data availability issue. The column codes for domain_ranks report: Or = organic keywords count, Ot = organic traffic estimate, Oc = organic traffic cost (in USD), Ad = paid keywords, At = paid traffic, Ac = paid traffic cost.
1// SEMrush API response format — semicolon-delimited text2// Example: GET https://api.semrush.com/?type=domain_ranks&key=API_KEY&domain=example.com&database=us34// Response (plain text, NOT JSON):5Or;Ot;Oc;Ad;At;Ac612345;450000;3.50;200;15000;2.0078// Column meanings for domain_ranks:9// Or = organic keyword count10// Ot = estimated monthly organic traffic11// Oc = organic traffic cost (USD)12// Ad = paid keyword count13// At = estimated paid traffic14// Ac = paid traffic cost (USD)1516// For domain_organic (keyword list) — costs units per row:17// Keyword positions report returns one data row per keyword18// Use display_limit=50 to limit rows and control unit consumption1920// ERROR 50 example (domain not found or wrong format):21// ERROR 50 :: NOTHING FOUNDPro tip: Before configuring Bubble, test your API key and domain by pasting the full URL into a browser. If you see 'ERROR 50 :: NOTHING FOUND', verify the domain format is bare (example.com), the database parameter matches the correct country (us, uk, de, etc.), and your SEMrush account has data for that domain.
Expected result: Browser shows a two-line text response with a header row and data row. The API key works, the domain returns data, and you understand the semicolon-delimited format you'll need to parse in Bubble.
Configure the API Connector with the SEMrush API key as a Private URL parameter
In Bubble, click the Plugins tab → 'API Connector'. Click 'Add another API'. Name it 'SEMrush API'. Set the base URL to `https://api.semrush.com`. Do NOT add any shared headers. SEMrush authentication is done entirely via URL parameters, not headers. The API does not accept an Authorization header. Click 'Add a call'. Name it 'Get Domain Ranks'. Set method to GET, path to `/` (the SEMrush API uses a single base endpoint with the report type as a parameter). Add URL parameters: - `type` — value: `domain_ranks` (selects the report type) - `key` — value: your SEMrush API key — IMPORTANT: enable the **Private** checkbox on this parameter. In Bubble's API Connector, URL parameters can be marked Private just like headers, which keeps the key on Bubble's server and out of the browser. Enter your actual key value here. - `domain` — leave as a dynamic parameter (you'll pass domain names from your Bubble UI) - `database` — value: `us` (or your target country: uk, de, fr, au, etc.) - `export_columns` — value: `Or,Ot,Oc,Ad,At,Ac` (select only the columns you need) - `display_limit` — value: `1` (domain_ranks always returns 1 row; for keyword-level reports, set this to 50 to limit unit consumption) IMPORTANT: Do NOT click 'Initialize call' yet. SEMrush returns plain text, not JSON. If you try to Initialize now, Bubble will fail to parse the response and show 'There was an issue setting up your call'. You'll handle initialization after adding the parsing step.
1// API Connector: SEMrush API2{3 "api_name": "SEMrush API",4 "base_url": "https://api.semrush.com",5 "authentication": "none",6 "calls": [7 {8 "name": "Get Domain Ranks",9 "method": "GET",10 "path": "/",11 "params": [12 { "name": "type", "value": "domain_ranks" },13 {14 "name": "key",15 "value": "YOUR_SEMRUSH_API_KEY",16 "private": true17 },18 { "name": "domain", "value": "<dynamic>" },19 { "name": "database", "value": "us" },20 { "name": "export_columns", "value": "Or,Ot,Oc,Ad,At,Ac" },21 { "name": "display_limit", "value": "1" }22 ]23 }24 ]25}Pro tip: To verify the API key is correctly stored without exposing it, check that the `key` parameter row shows a lock icon or 'Private' indicator in the API Connector interface after enabling the checkbox. The actual key value should be masked in the Bubble editor UI.
Expected result: API Connector shows 'SEMrush API' with a GET call named 'Get Domain Ranks'. The `key` parameter has the Private checkbox enabled. Note: Do NOT initialize the call yet — this comes after setting up the Toolbox parsing workflow.
Install Toolbox plugin and build the text parsing Backend Workflow
Go to Plugins tab → 'Add plugins' → search 'Toolbox' → Install 'Toolbox' by Bubble (free). This plugin adds a 'Server Script' action to Bubble workflows that runs JavaScript server-side, which you'll use to parse SEMrush's semicolon-delimited text. Now build the parsing workflow. In the Bubble editor, create a Backend Workflow named 'Fetch SEMrush Domain Data'. Add a text input parameter to this workflow named 'domain' (this is passed when the workflow is called from a page). Add the following actions to the workflow: Action 1: 'Plugins → SEMrush API - Get Domain Ranks'. Set the domain parameter to: This workflow's domain. This makes the API call and stores the result as Step 1's text response. Action 2: 'Toolbox - Server Script'. In the Script field, enter the JavaScript parsing code below. This script receives the SEMrush plain text, splits it into headers and values, and returns a structured JSON object. Set the 'Result type' to 'text' or 'object' depending on how you'll use the output. Action 3: 'Create a new DomainMetric' (your Bubble data type, created in the next step). Map the fields from the Server Script's output: organic_keywords = Step 2's result's Or, organic_traffic = Step 2's result's Ot, etc. To trigger this workflow from a Bubble page, add a 'Schedule API Workflow' action on a 'Refresh SEMrush Data' button, passing the domain input value as the workflow's domain parameter.
1// Toolbox Server Script — parse SEMrush semicolon-delimited text2// Action type: Server Script in Backend Workflow3// Input: Step 1's body (the raw SEMrush API text response)45var raw = properties.text_input; // the raw SEMrush response text6var lines = raw.split('\n').filter(function(l) { return l.trim().length > 0; });78if (lines.length < 2) {9 // No data or error response10 return { error: raw };11}1213var headers = lines[0].split(';');14var values = lines[1].split(';');1516var result = {};17for (var i = 0; i < headers.length; i++) {18 result[headers[i]] = values[i] || '';19}2021return result;2223// Example output for domain_ranks:24// { Or: "12345", Ot: "450000", Oc: "3.50", Ad: "200", At: "15000", Ac: "2.00" }Pro tip: SEMrush may return 'ERROR 50 :: NOTHING FOUND' as a plain text response if the domain has no data in the selected database. Add a check in your Server Script: if the raw text starts with 'ERROR', return an error object and handle it gracefully in your workflow (e.g., skip creating a DomainMetric record).
Expected result: Backend Workflow 'Fetch SEMrush Domain Data' has three actions: the API call, the Server Script parser, and a database create action. Triggering the workflow manually with a test domain returns a DomainMetric record with numeric values populated.
Create the DomainMetric data type and cache results
Before running the Backend Workflow, create the Bubble data type that will store cached SEMrush results. Go to Data tab → Data types → 'Create a new type' → name it 'DomainMetric'. Add these fields: - `domain` (text) — the queried domain (bare format, e.g., 'example.com') - `organic_keywords` (number) — from SEMrush Or column - `organic_traffic` (number) — from SEMrush Ot column - `organic_cost` (number) — from SEMrush Oc column - `paid_keywords` (number) — from SEMrush Ad column - `paid_traffic` (number) — from SEMrush At column - `fetched_at` (date) — when this data was last retrieved - `database` (text) — the SEMrush geographic database used (e.g., 'us', 'uk') In the Backend Workflow's Action 3, map the Server Script output to these fields. Note that all SEMrush values come back as strings even when they're numbers — convert them using Bubble's ':converted to number' operator on each field. Add a cache check to avoid re-fetching within 24 hours. At the start of your Backend Workflow, add a conditional: if a DomainMetric already exists for this domain with fetched_at within the last 24 hours, stop the workflow (Only when: search for DomainMetrics where domain = this workflow's domain, fetched_at > Current date/time - 1 day → count = 0). Set Privacy rules: Data tab → Privacy → DomainMetric. SEMrush data is paid competitive intelligence. Restrict access to admin-role users or logged-in users only. Do not allow anonymous access through Bubble's Data API. WU note: each Backend Workflow run that calls SEMrush consumes both Bubble WU (API call, workflow steps, database write) and SEMrush API units. The 24-hour cache prevents both types of consumption from adding up on high-traffic dashboards.
1// Bubble data type: DomainMetric2// domain (text)3// organic_keywords (number)4// organic_traffic (number)5// organic_cost (number)6// paid_keywords (number)7// paid_traffic (number)8// fetched_at (date)9// database (text)1011// Privacy Rule (Data tab → Privacy → DomainMetric):12// Condition: Current User is logged in13// Access: View all fields = YES1415// Cache check condition in Backend Workflow:16// Only when: Do a search for DomainMetrics17// constraint: domain = This workflow's domain18// constraint: fetched_at > Current date/time - 1 day19// :count is 020// (Skip the fetch if recent data exists)Pro tip: When deduplicating cached records, check for an existing DomainMetric where domain = the input domain AND fetched_at is within the last 24 hours. If found, update the existing record's values and fetched_at rather than creating a second record for the same domain.
Expected result: DomainMetric data type has all fields defined. After running the Backend Workflow for a test domain, Data tab shows a DomainMetric record with numeric organic_traffic and organic_keywords values, and a fetched_at timestamp.
Build the competitive SEO intelligence dashboard
Create a new Bubble page called 'seo-dashboard'. This page displays SEMrush metrics for your domain and competitor domains side by side, reading from the cached DomainMetric data type. Add a Repeating Group with type 'DomainMetric'. Set the data source to 'Do a search for DomainMetrics', sorted by organic_traffic descending. In each row, display: - domain (text) - organic_keywords (formatted with commas as a number) - organic_traffic (formatted as a number with commas) - organic_cost (formatted as currency '$###,###') - paid_keywords (formatted with commas) - fetched_at (formatted as 'Last updated: MMM D, YYYY') Add a text Input element above the Repeating Group labeled 'Add domain to track'. Add a button 'Fetch SEMrush Data' with a workflow: trigger Backend Workflow 'Fetch SEMrush Domain Data' with domain = Input's value. Add validation: the Input value must not be empty and must not start with 'http' (bare domain only — strip protocol if users accidentally include it). For the bar chart comparison view, add a visual chart element. If your Bubble plan includes charts, create a bar or horizontal bar chart bound to the DomainMetric search with organic_traffic as the value axis and domain as the category axis. This gives an instant visual comparison of estimated organic traffic across tracked domains. RapidDev's team has helped agencies build Bubble-based SEO intelligence portals using SEMrush, Ahrefs, and Google Search Console in unified dashboards — if you need a multi-source competitive dashboard built, reach out at rapidevelopers.com/contact.
1// SEO Dashboard — Repeating Group layout23// Repeating Group data source:4// Search for DomainMetrics5// Sort: organic_traffic descending6// Columns: domain | organic_keywords | organic_traffic | organic_cost | fetched_at78// 'Add Domain' input validation workflow:9// Condition: Input's value is empty → show alert 'Please enter a domain'10// Condition: Input's value contains 'http' → show alert 'Enter bare domain only (e.g. example.com)'11// Otherwise: Schedule API Workflow 'Fetch SEMrush Domain Data'12// with domain = Input's value1314// Number formatting examples in text elements:15// organic_traffic: Current cell's DomainMetric's organic_traffic:formatted as ###,###16// organic_cost: Current cell's DomainMetric's organic_cost:formatted as $###,###.##17// fetched_at: Current cell's DomainMetric's fetched_at:formatted as 'MMM D, YYYY'Pro tip: Add a 'Force Refresh' button that bypasses the 24-hour cache check and re-fetches SEMrush data immediately. This is useful when you've just published a new campaign and want to see updated metrics. Implement it by adding a workflow step that deletes the existing DomainMetric for the selected domain before triggering the fetch workflow.
Expected result: Dashboard page shows a Repeating Group comparing multiple domains' SEMrush organic traffic, keyword counts, and paid metrics. The 'Add domain' flow triggers a Backend Workflow that fetches SEMrush data, parses it, and adds a new row to the dashboard.
Common use cases
Competitive Domain SEO Dashboard
Pull domain-level organic traffic estimates, keyword rankings, and paid search spend for your domain and up to 3 competitor domains into a Bubble side-by-side comparison dashboard. Data is cached in Bubble's database and refreshed daily on demand. Color-code metrics where your domain outperforms or underperforms versus competitors.
Build a Bubble dashboard comparing my domain against two competitors using SEMrush data: show estimated monthly organic traffic, number of ranking keywords, and ad spend for each. Refresh data with a button and show when data was last updated.
Copy this prompt to try it in Bubble
Keyword Position Tracker
Track how specific target keywords rank for your domain using SEMrush's phrase_this report type. Store weekly snapshots of keyword position, search volume, and CPC in a Bubble data type. Display position trend lines over time using Bubble's chart elements to show whether rankings are improving or declining.
Create a Bubble keyword tracking page where admins enter a keyword and domain, then see the current SEMrush ranking position, search volume, CPC, and a 4-week position history stored in the database. Show a trend arrow (up/down) compared to last week's position.
Copy this prompt to try it in Bubble
Client SEO Reporting Portal
For agencies, build a white-label Bubble portal where clients can see their domain's SEMrush metrics without logging into SEMrush directly. Each client record stores their domain. An admin-triggered weekly refresh pulls the latest domain_ranks data for all clients and stores results. Client-facing pages show their own data with no access to others' reports.
Build a Bubble client portal where each client sees their own SEMrush organic traffic estimate, keyword count, and top 10 ranking keywords. Admin users can refresh all client data from a single 'Update All Clients' button that runs a scheduled Backend Workflow.
Copy this prompt to try it in Bubble
Troubleshooting
API Connector Initialize call fails with 'There was an issue setting up your call' or shows a parsing error
Cause: SEMrush returns semicolon-delimited plain text, not JSON. Bubble's Initialize call attempts to parse responses as JSON and fails on SEMrush's text format. This is the most common first-step blocker for the SEMrush integration.
Solution: Do not use the Initialize call feature for SEMrush API calls. Instead, configure the API Connector call parameters manually without initializing. Use the Toolbox Server Script in a Backend Workflow to parse the raw text response — this is the correct architecture for non-JSON APIs. The API Connector call will still work for making requests even without initialization; you just won't have Bubble's auto-detected field types.
SEMrush returns 'ERROR 50 :: NOTHING FOUND' instead of data
Cause: The domain was entered with https:// prefix, www. prefix that doesn't match SEMrush's record, a trailing slash, or the domain genuinely has no data in the selected geographic database (database=us, uk, etc.).
Solution: Verify the domain format is bare: `example.com` not `https://www.example.com/`. In Bubble, strip the https:// prefix from user input using `:find & replace` with 'https://' replaced by '', and similarly for 'http://', 'www.', and trailing '/'. Also try switching the database parameter to a different country — some domains have data only in specific geographic SEMrush databases.
SEMrush API returns 'ERROR 120 :: You have run out of limits'
Cause: Monthly API unit limit for the SEMrush account has been exhausted. SEMrush API units are charged per call and per data row returned for certain report types. A domain_organic call with display_limit=200 consumes 200 units in a single request.
Solution: Reduce display_limit values for keyword-level report calls (set to 50 or less). Implement aggressive caching in Bubble — only re-fetch SEMrush data when the cached result is older than 24 hours. Check remaining API units in SEMrush Account Settings → API. Upgrade the SEMrush plan if the monthly unit allocation is consistently insufficient for your use case.
Toolbox Server Script returns undefined or throws an error when processing SEMrush response
Cause: The raw text being passed to the Server Script is not the SEMrush response body — it may be an error string, empty, or the wrong step's output is being referenced. Alternatively, the SEMrush response includes Windows-style line endings (\r\n) that split differently than Unix-style (\n).
Solution: In the Server Script, update the split to handle both line ending styles: replace `raw.split('\n')` with `raw.split(/\r?\n/)`. Also add a null check: if `!raw || raw.length === 0`, return an error object rather than letting the script crash. Check Bubble's Logs tab → Workflow logs to see the exact text value being passed to the Server Script as input.
1// Updated line splitting to handle Windows line endings:2var lines = raw.split(/\r?\n/).filter(function(l) { return l.trim().length > 0; });Best practices
- Always pass the SEMrush API key as a Private URL parameter named `key` — never in a request body or in a visible (non-Private) URL parameter. Even though it's technically a query string parameter, the Private checkbox in Bubble's API Connector keeps it server-side.
- Cache SEMrush results in a Bubble DomainMetric data type with a fetched_at timestamp. Only fetch fresh data when the cache is older than 24 hours. SEMrush data updates weekly for most metrics — fetching multiple times per day wastes API units without providing fresher data.
- Use the display_limit parameter on all keyword-level report calls (domain_organic, phrase_this). For domain_ranks, there's only ever 1 row — but for keyword reports, each row consumes API units. A display_limit=50 limits a single domain_organic call to 50 unit consumption.
- Enter domains in bare format (example.com) and strip protocols and trailing slashes from all user input before passing to the SEMrush API. Use Bubble's :find & replace operator to remove 'https://', 'http://', and trailing '/' from Input values.
- Set Privacy rules on your DomainMetric data type to restrict access to admin users. SEMrush data represents paid intelligence about competitor domains — it should not be readable through Bubble's public Data API or accessible to non-admin users.
- The Toolbox Server Script in a Backend Workflow is the recommended parsing approach for SEMrush text responses. Avoid trying to handle the parsing in client-side workflows or page-level expressions — server-side parsing is more reliable and keeps API results private.
- For large datasets (multiple domains, multiple report types), stagger Backend Workflow scheduling using the 'delay' option rather than running all SEMrush calls simultaneously. SEMrush has per-second rate limits — sequential calls with brief delays are safer than concurrent calls that may trigger rate limiting.
- Monitor your SEMrush API unit balance regularly in Account Settings → API. Build a low-unit warning into your Bubble admin dashboard by storing the unit balance response (available from the SEMrush API info endpoint) and alerting admin users when balance drops below a threshold.
Alternatives
Ahrefs API returns clean JSON (no text parsing required) and uses standard Bearer token auth — simpler to integrate with Bubble's API Connector. Ahrefs specializes in backlink data quality and link-level granularity. SEMrush covers a broader scope including paid search, content analytics, and domain-wide traffic estimates. Choose SEMrush for multi-channel competitive intelligence; choose Ahrefs for deep backlink monitoring.
Yoast SEO provides on-page metadata (meta titles, descriptions, og_images) from your own WordPress site at zero API cost. SEMrush provides external market intelligence (competitor rankings, traffic estimates, keyword discovery). The two serve different purposes: Yoast tells you what's on your pages; SEMrush tells you how you compare to the market.
Google Analytics provides actual measured traffic data from your own site's visitors — pageviews, sessions, conversions. SEMrush provides estimated traffic data based on keyword ranking models, useful for competitive analysis of sites you don't own. Use both: Google Analytics for your own site's truth, SEMrush for market-level competitive benchmarking.
Frequently asked questions
Can I use SEMrush API on Bubble's free plan?
Yes for the basic API Connector calls, but the Toolbox Server Script and Backend Workflows that handle text parsing and scheduled refreshes work best on a paid Bubble plan. More importantly, SEMrush itself requires a paid plan (Pro, Guru, or Business) to access the API — there is no API access on SEMrush's free tier.
Why does SEMrush return text instead of JSON? Is there a JSON mode?
SEMrush's API was designed before JSON became the universal standard for web APIs. Most report endpoints return semicolon-delimited text by default. Some newer SEMrush API endpoints do return JSON (the SEMrush Analytics API v3, for example), but the core domain and keyword report endpoints used for competitive intelligence remain text-based. The Toolbox Server Script parsing approach handles this consistently.
How many SEMrush API units does a domain_ranks call consume?
The domain_ranks report type consumes 1 API unit per call regardless of columns selected. Keyword-level reports like domain_organic consume units per row returned — a call with display_limit=100 consumes 100 units. For competitive dashboards tracking multiple domains, each domain_ranks refresh costs one unit per domain. Manage costs by caching results and setting appropriate display_limit values for any keyword-level calls.
Can Bubble handle SEMrush's geographic database parameter?
Yes. The database URL parameter selects the country: `us` for United States, `uk` for United Kingdom, `de` for Germany, `au` for Australia, and so on. In Bubble, make database a dynamic parameter or a dropdown selection. Store it alongside the domain in your DomainMetric data type so you know which country's data each cached record represents — especially important for multi-market agencies.
What is the domain_organic report and when should I use it instead of domain_ranks?
domain_ranks returns summary-level metrics for a domain (total organic traffic estimate, total keyword count) in a single row. domain_organic returns the actual keyword list — each keyword where the domain ranks, with its position, search volume, URL, and CPC. Use domain_ranks for dashboard summary cards. Use domain_organic when you need the keyword-level breakdown. Note that domain_organic costs one API unit per row (each keyword) — use display_limit=50 to keep costs predictable.
How do I handle the Server Script if SEMrush returns an error like 'ERROR 50 :: NOTHING FOUND'?
In your Toolbox Server Script, check whether the raw text starts with 'ERROR' before trying to parse it as a delimited response. If it does, return an error object: `{ error: raw }`. In the subsequent workflow action, add a condition: Only create a DomainMetric record if the Server Script's result does not contain an 'error' key. This prevents empty or error records from polluting your database.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation