# Google Ads

- Tool: Bubble
- Difficulty: Advanced
- Time required: 2–3 hours (plus 1–5 business days for developer token approval)
- Last updated: July 2026

## TL;DR

Connect Bubble to Google Ads by building a token-refresh Backend Workflow that exchanges your OAuth refresh token for a short-lived access token, then configuring the API Connector with Authorization (Private), developer-token (Private), and Content-Type headers. Use GAQL POST queries to pull campaign and keyword metrics into Bubble repeating groups. Expect a 1–5 business day wait for developer token approval before accessing real account data.

## Why Google Ads API Needs Three Credential Layers — and How Bubble Handles Them

Google Ads API is the most credential-intensive integration in the marketing category. Unlike SendGrid (one API key) or Mailchimp (one API key in Basic Auth), Google Ads requires you to manage three separate auth layers simultaneously: a Google Cloud OAuth client, a developer token, and per-account user tokens. This complexity is intentional — Google tightly controls programmatic access to ad account data.

Here is what each layer does. The Google Cloud OAuth client (client_id and client_secret) identifies your application to Google's auth servers. The developer token grants your application permission to use the Google Ads API at all — it requires manual approval from Google, which takes 1–5 business days. The refresh token represents the specific Google Ads account you are querying, obtained when the account owner grants your app access via OAuth.

Bubble's approach to handling this is more manual than tools like Retool (which has a native Google Ads connector) or Zapier (which manages OAuth flows for you), but it gives you full control. The key architectural pattern is a Backend Workflow in Bubble that exchanges the refresh token for a new access token before dashboard data loads — because access tokens expire after one hour and Bubble does not auto-refresh them.

Once the auth plumbing is in place, the querying model is clean: you POST GAQL (Google Ads Query Language) queries to a single endpoint and get back structured JSON with campaign names, click counts, impression totals, and spend data. Bubble can display this in repeating groups, update it on a schedule, and let team members filter by date range — all without them needing Google Ads account access.

## Before you start

- A Bubble account on a paid plan — Backend Workflows (required for token refresh) are not available on the free plan
- A Google account with access to the Google Ads account you want to connect
- A Google Cloud Console project with the Google Ads API enabled (console.cloud.google.com → APIs & Services → Enable APIs → search 'Google Ads API')
- An OAuth 2.0 client created in Google Cloud Console (APIs & Services → Credentials → Create Credentials → OAuth client ID → Web application)
- A developer token from Google Ads API Center (In Google Ads: Tools → API Center → apply for Basic access) — approval takes 1–5 business days; test token available immediately but only queries test accounts
- An OAuth refresh token for the target Google Ads account — obtained by completing an OAuth flow using your client_id, client_secret, and the accounts.google.com authorization URL with scope https://www.googleapis.com/auth/adwords

## Step-by-step guide

### 1. Set Up Google Cloud, Enable the API, and Apply for a Developer Token

Start in Google Cloud Console (console.cloud.google.com). Create a new project for your Bubble app or use an existing one. Navigate to APIs & Services → Library, search for 'Google Ads API', and click Enable.

Next, create your OAuth client. Go to APIs & Services → Credentials → Create Credentials → OAuth client ID. Choose 'Web application' as the application type. Add a redirect URI — for testing, you can use `https://developers.google.com/oauthplayground`. Download the client JSON and note your `client_id` and `client_secret`.

Now apply for the developer token. Log in to your Google Ads account (the Manager Account / MCC level if you manage multiple accounts). Navigate to Tools → API Center. Fill out the application form describing your use case — 'Building a performance dashboard in a Bubble web app to display campaign metrics for internal team members.' Submit.

Google typically approves Basic access in 1–5 business days. Until approval, you receive a test developer token. This test token can only query test accounts (sandbox accounts) — it will return PERMISSION_DENIED errors on real production ad accounts. Plan your build timeline accordingly and apply for the token before writing any Bubble code.

**Expected result:** Google Cloud project has Google Ads API enabled. You have client_id and client_secret from OAuth 2.0 credentials. Developer token application is submitted (or approved). You have a test token for development.

### 2. Obtain an OAuth Refresh Token for the Google Ads Account

The refresh token represents the specific Google Ads account's authorization for your app to access its data. The easiest way to get this for a Bubble build is using Google's OAuth 2.0 Playground.

Go to developers.google.com/oauthplayground. Click the gear icon in the top right → check 'Use your own OAuth credentials' → enter your client_id and client_secret. In the left panel under 'Step 1', find the scope field and paste: `https://www.googleapis.com/auth/adwords`. Click 'Authorize APIs'. Log in with the Google account that has access to the Google Ads account.

After authorization, click 'Exchange authorization code for tokens' in Step 2. You'll see a JSON response containing `access_token` and `refresh_token`. Copy and securely store the `refresh_token` — this is a long-lived token that doesn't expire unless revoked. The `access_token` expires in 1 hour.

Also note your Google Ads Customer ID (the 10-digit number at the top of your Google Ads account, formatted as XXX-XXX-XXXX). Remove the dashes — the API uses it as a plain 10-digit number. Store this value in a Bubble App Data configuration table (create a 'Config' data type with a text field 'ads_customer_id' and one record with your Customer ID).

```
{
  "POST to exchange auth code": "https://oauth2.googleapis.com/token",
  "body": {
    "code": "<authorization_code_from_step_1>",
    "client_id": "<your_client_id>",
    "client_secret": "<your_client_secret>",
    "redirect_uri": "https://developers.google.com/oauthplayground",
    "grant_type": "authorization_code"
  },
  "response_contains": {
    "access_token": "<expires in 1 hour>",
    "refresh_token": "<store this — long-lived>",
    "expires_in": 3599
  }
}
```

**Expected result:** You have a refresh_token stored in your Bubble database (Config table). You have the customer ID without dashes stored as well. The OAuth Playground flow was successful.

### 3. Build the Token-Refresh Backend Workflow

Because Google OAuth access tokens expire after 1 hour, Bubble needs a Backend Workflow that fetches a fresh access token before each Google Ads API call. This is the most Bubble-specific step in this entire integration.

First, create a data structure to store the token. In the Data tab, add a field to your Config data type: 'google_ads_access_token' (text) and 'google_ads_token_expiry' (date).

Next, create the refresh API call. In the API Connector, add a new API named 'Google OAuth'. Add a call named 'Refresh Access Token' with method POST and path `/token`. Set the base URL of this API to `https://oauth2.googleapis.com`. Set Body type to JSON and add the body shown in the code block.

The `refresh_token`, `client_id`, and `client_secret` fields should be set as dynamic parameters so they can be passed in from the Backend Workflow. Initialize the call by entering test values and confirming you get a 200 response with an `access_token` field.

Now go to Settings → Backend workflows (if you don't see this, you are on the free plan — upgrade to Starter). Create a new API Workflow named 'Refresh Google Ads Token'. Add steps:
1. Call the Google OAuth 'Refresh Access Token' action with your stored values from the Config table.
2. 'Make changes to a Thing' → modify Config → set google_ads_access_token = Result of Step 1's access_token, set google_ads_token_expiry = Current date/time + 55 minutes.

In your dashboard page workflow (Page is loaded), add a step: 'Only when Config's google_ads_token_expiry < Current date/time + 5 minutes' → Schedule API Workflow 'Refresh Google Ads Token'. RapidDev's team has built this refresh pattern across hundreds of Bubble apps connecting to OAuth-based APIs — if you need a more robust implementation (retry logic, error notifications), our team is available at rapidevelopers.com/contact.

```
{
  "method": "POST",
  "url": "https://oauth2.googleapis.com/token",
  "body": {
    "refresh_token": "<dynamic: refresh_token>",
    "client_id": "<dynamic: client_id>",
    "client_secret": "<dynamic: client_secret>",
    "grant_type": "refresh_token"
  }
}
```

**Expected result:** The 'Refresh Google Ads Token' Backend Workflow runs successfully and updates the Config record with a fresh access_token and a new expiry timestamp 55 minutes from now.

### 4. Configure the Google Ads API Connector with Three Private Headers

Now configure the main Google Ads API Connector. In Plugins → API Connector, add a new API and name it 'Google Ads'.

Set the base URL to `https://googleads.googleapis.com`.

Add three shared headers — all three must be marked as 'Private':

1. `Authorization`: value `Bearer [dynamic]` — this will be replaced at the call level with the access token from your Config table. For now, enter a placeholder value; you'll map the real token when creating the actual call.
2. `developer-token`: value is your Google Ads API developer token string. Mark as Private.
3. `Content-Type`: value `application/json` — this doesn't need to be Private, but adding it here ensures all calls inherit it.

For calls that access a manager account's sub-accounts, add a fourth header: `login-customer-id` with the manager account's customer ID (without dashes). This is only needed if you're managing multiple sub-accounts through one manager account.

Now create the main data call. Click 'Add a new call'. Name it 'Get Campaign Metrics'. Method = POST. Path = `/v17/customers/<dynamic: customerId>/googleAds:search`.

Set Body type to JSON. The body is a GAQL query string (see code block). Change 'Use as' to 'Data'. Initialize call by entering your customer ID and a simple GAQL query. If your developer token is still in test mode, you must use a test account customer ID — real account IDs return PERMISSION_DENIED until Basic access is approved.

```
{
  "method": "POST",
  "url": "https://googleads.googleapis.com/v17/customers/<dynamic: customerId>/googleAds:search",
  "headers": {
    "Authorization": "Bearer <private: from Config access_token>",
    "developer-token": "<private>",
    "Content-Type": "application/json"
  },
  "body": {
    "query": "SELECT campaign.name, campaign.status, metrics.clicks, metrics.impressions, metrics.cost_micros, metrics.ctr FROM campaign WHERE campaign.status = 'ENABLED' AND segments.date DURING LAST_30_DAYS"
  }
}
```

**Expected result:** The Google Ads API shows in your API Connector with three Private headers. The 'Get Campaign Metrics' call initializes successfully and Bubble detects the 'results' array in the response with campaign name, status, and metric fields.

### 5. Parse Campaign Data and Build the Bubble Dashboard

With the API call initialized, build the reporting interface in Bubble.

On your admin page, add a Repeating Group. Set Type to 'External API response' and Data Source to 'Get data from external API → Google Ads - Get Campaign Metrics'. Map the customer ID input to your Config table's 'ads_customer_id' field, and the Authorization header dynamic value to 'Config's google_ads_access_token'.

In the Repeating Group cells, add text elements and map them:
- Campaign name: `Current cell's campaign > name`
- Clicks: `Current cell's metrics > clicks`
- Impressions: `Current cell's metrics > impressions`
- CTR: `Current cell's metrics > ctr` (multiply by 100 and format with 2 decimal places to show as percentage)
- Spend: `Current cell's metrics > cost_micros` — divide by 1,000,000 to get the dollar amount. In Bubble, create a formula: `Current cell's metrics cost_micros / 1000000`. Format as currency.

The cost_micros conversion is critical — the raw value looks like 4523000, which is actually $4.52. Displaying the raw number makes spend look 1,000 times higher than it is. Always divide by 1,000,000 before display.

Add a page-load workflow: on Page is loaded → Only when Config's token expiry < Now + 5 minutes → Schedule Backend Workflow 'Refresh Google Ads Token'. This ensures fresh tokens before the repeating group loads data.

```
{
  "bubble_expression_for_cost": "Current cell's metrics cost_micros / 1000000",
  "bubble_expression_for_ctr": "Current cell's metrics ctr * 100",
  "example_raw_cost_micros": 4523000,
  "example_actual_cost_dollars": 4.52,
  "example_raw_ctr": 0.0234,
  "example_display_ctr_percent": "2.34%"
}
```

**Expected result:** The dashboard repeating group displays campaign names, clicks, impressions, formatted CTR percentages, and correct dollar-denominated spend values. Data refreshes on page load after a token refresh check.

## Best practices

- Store the OAuth refresh token in Bubble's database (not hardcoded in the API Connector) so it can be updated without modifying your API configuration if the token is ever revoked.
- Always divide cost_micros by 1,000,000 before displaying monetary values — displaying raw micros makes spend look 1 million times higher than it is.
- Validate GAQL queries in Google's Field Explorer before adding them to Bubble — incompatible field combinations cause FIELD_ERROR responses that can be hard to diagnose.
- Apply for the developer token before starting to build — the 1–5 business day approval window is the longest lead time in this integration and cannot be sped up.
- Set up token expiry monitoring with a 5-minute buffer: refresh when token expiry is within 5 minutes of Now, not exactly at 60 minutes, to avoid race conditions.
- Use GAQL's date range filtering (segments.date DURING LAST_30_DAYS or BETWEEN) to limit query response sizes — querying all historical data on every page load is expensive in WU and slow.
- Mark all three credential headers (Authorization, developer-token, client credentials) as Private in Bubble's API Connector — none of these should ever reach the browser.
- Check the Logs tab → Workflow logs after the first successful run to verify WU usage. Token refresh workflows add WU cost each time they run — schedule refreshes efficiently.

## Use cases

### Campaign Performance Dashboard

Build a Bubble admin page that shows all active campaigns with their clicks, impressions, CTR, and spend — refreshed on page load using the GAQL POST endpoint. Team members can see ad performance without needing Google Ads account access, and you can add Bubble logic to flag underperforming campaigns automatically.

Prompt example:

```
On Admin Dashboard page load, first run the 'Refresh Google Ads Token' Backend Workflow. Then get data from external API → Google Ads 'Get Campaign Metrics' call. Bind results to a Repeating Group showing Campaign Name, Clicks, Impressions, and Cost (Current cell's cost_micros / 1000000 formatted as currency).
```

### Keyword Performance Tracker

Query keyword-level metrics to see which search terms are driving conversions. Use a GAQL query filtered to specific campaigns, displaying keyword text, quality score, average CPC, and conversion count in a sortable Bubble repeating group. Add a date range picker to let users filter by time period.

Prompt example:

```
Create an API Connector call 'Get Keyword Metrics' with GAQL: SELECT ad_group_criterion.keyword.text, metrics.clicks, metrics.average_cpc, metrics.conversions FROM ad_group_criterion WHERE ad_group_criterion.type = 'KEYWORD' AND campaign.status = 'ENABLED'. Display results in a repeating group sortable by Clicks descending.
```

### Budget Utilization Monitor

Pull daily spend data and compare it against campaign budgets to surface over-spending or underspending campaigns. Use GAQL to query campaign_budget.amount_micros alongside metrics.cost_micros, calculate the spend percentage in Bubble expressions, and highlight campaigns where spend exceeds 90% of budget with a conditional color change.

Prompt example:

```
Create a GAQL call: SELECT campaign.name, campaign_budget.amount_micros, metrics.cost_micros FROM campaign WHERE segments.date DURING TODAY. In the repeating group, add a conditional: When (Current cell's metrics_cost_micros / Current cell's campaign_budget_amount_micros) > 0.9 → change background color to orange.
```

## Troubleshooting

### PERMISSION_DENIED error when querying a real Google Ads account

Cause: The developer token is still in test mode. Test tokens can only query test accounts created in Google Ads API Center, not real production ad accounts. Basic access approval from Google is required for real account data.

Solution: If you haven't applied for Basic access yet, do so at Tools → API Center in your Google Ads account. While waiting for approval (1–5 business days), develop and test against a Google Ads test account (API Center → Test accounts). Once Basic access is approved, your developer token works against real accounts.

### 401 Unauthorized errors on every Google Ads API call even though credentials seem correct

Cause: The access token has expired (tokens last only 1 hour) or the token-refresh Backend Workflow has not run before the API call fired.

Solution: Verify that the token-refresh Backend Workflow runs before data loads. Check the Logs tab → Workflow logs to see if the refresh workflow ran and what response it received. If the refresh token itself is invalid, you need to go through the OAuth flow again to get a new refresh token.

### GAQL query returns a FIELD_ERROR about incompatible resources

Cause: GAQL has resource compatibility rules — not all field combinations can appear in the same SELECT statement. For example, campaign_budget fields and metrics fields cannot always be combined in the same query.

Solution: Test your GAQL query in Google's API Field Explorer at developers.google.com/google-ads/api/fields/v17 before using it in Bubble. The Field Explorer shows which fields are compatible with each other and will highlight incompatible combinations. Split complex queries into two separate API calls if needed.

### Campaign spend values appear to be about 1,000,000 times higher than expected

Cause: Google Ads API returns monetary values in micros — millionths of the account's currency unit. A spend of $4.52 is returned as 4520000. Displaying the raw cost_micros value without dividing by 1,000,000 makes $4.52 look like $4,520,000.

Solution: In every Bubble text element displaying monetary values from the Google Ads API, divide the field value by 1000000. In Bubble's formula editor: 'Current cell's metrics cost_micros / 1000000'. Format the result as currency with 2 decimal places.

### Backend Workflows option is missing from Settings menu

Cause: Backend Workflows are only available on paid Bubble plans. The free plan does not include this feature.

Solution: Upgrade to Bubble's Starter plan ($32/month) or higher to access Backend Workflows. The token-refresh pattern requires Backend Workflows — there is no workaround on the free plan for automated token refresh. Without Backend Workflows, you could trigger a page-level workflow to call the refresh endpoint, but this would only work when a user is actively on the page.

## Frequently asked questions

### How long does it take to get approved for the Google Ads API developer token?

Basic access approval typically takes 1–5 business days after submitting your application in Google Ads → Tools → API Center. During this period, you receive a test developer token that works against test accounts only. Real production ad account data requires Basic (or higher) access. Plan your build timeline to account for this wait.

### Why do I need three different credentials for Google Ads, when most APIs only need one key?

Google Ads API uses three layers for security: the OAuth client (client_id + client_secret) identifies your application, the developer token grants API access at the developer level, and the refresh token/access token represents the specific ad account owner's authorization. This multi-layer model lets Google revoke access at any layer independently and ensures each level of access is explicitly granted.

### Does Bubble's free plan support Google Ads integration?

Partially. The API Connector works on all plans, so you can make GAQL calls from page-level workflows. However, the token-refresh Backend Workflow (essential for production use) requires a paid Bubble plan. On the free plan, tokens expire after 1 hour with no automated way to refresh them server-side. The Starter plan ($32/mo) enables Backend Workflows.

### Can I use this integration to create or modify Google Ads campaigns, not just read them?

Yes — the Google Ads API supports campaign creation, budget changes, ad group modifications, and keyword management. However, write operations require Standard or higher developer token access (beyond Basic), and the approval process is more rigorous. For a Bubble dashboard use case (read-only reporting), Basic access is sufficient. Discuss your use case with Google when applying.

### What does cost_micros mean and why do all the spend values look wrong?

cost_micros is Google's way of representing monetary values without floating-point precision errors. The value is in millionths of your account's currency unit — so $4.52 is returned as 4520000. Always divide by 1,000,000 before displaying in Bubble. Build this conversion into your repeating group formula from the start.

### My GAQL query works in the API Explorer but fails in Bubble with a 400 error

The most common cause is that the GAQL query string needs to be sent as a JSON property, not as a raw string. Confirm that your API Connector call body is set to 'JSON body' type and the query is the value of a 'query' key: {"query": "SELECT..."}. Also ensure special characters like single quotes in GAQL filters are properly escaped in the JSON body.

---

Source: https://www.rapidevelopers.com/bubble-integrations/google-ads
© RapidDev — https://www.rapidevelopers.com/bubble-integrations/google-ads
