Google Ads connector
Advertising platform for campaigns, ad groups, performance, and spend data.
Use this as an implementation starting point: adapt the worked example to the requested dataset and the source documentation. See Custom API for the shared SDK pattern and document-upload workflow.
What this connector is for
Advertising platform for campaigns, ad groups, performance, and spend data.
A connector owns access to the source. Separate syncs shape selected resources into tables for analysis and downstream transformations using the Python SDK.
Data and tables you can pull
These are representative dataset families to consider, not a claim that the worked example implements every resource. Access, fields, and availability depend on the source account, product edition, and permissions. Use the vendor reference to select concrete resources.
| Suggested table | One row represents |
|---|---|
customers | a customer |
campaigns | a campaign |
ad_groups | a ad group |
ads | a ad |
keyword_metrics | a keyword/date/segment metric group |
campaign_daily_metrics | a campaign/date/segment metric group |
Relationships and extraction considerations
Performance tables are reports at a defined date and resource grain. Resource access depends on the customer/manager hierarchy, OAuth credentials, and developer-token approval. Do not sum differently segmented reports as independent spend.
Connecting to Google Ads
Uses the official Google Ads Python SDK, developer token, OAuth client credentials, and refresh token. A manager login_customer_id is optional. The SDK refreshes access tokens; developer-token access tier and linked account permissions apply. Search pages are provider-controlled and output is rebatched locally.
All credentials arrive through config: developer_token, client_id,
client_secret, and refresh_token, along with the target customer_id. Obtain
the refresh token through Google's offline consent flow with the
https://www.googleapis.com/auth/adwords scope.
client() passes those values to GoogleAdsClient.load_from_dict(). Its Google
authentication library obtains and refreshes access tokens as needed for API
calls, including pagination during a long run. Access tokens are cached by the
Google client; they are not sync checkpoints or required configuration fields.
The connector keeps the same authenticated service client throughout the run.
This uses the vendor SDK's authentication rather than an HTTPX request hook.
Additional resources may require additional scopes. Connection settings belong to the connector; dataset filters and resource selection belong to each sync. See the CLI workflow for registration and configuration.
Connection settings used by the example
| Setting | Requirement | Purpose |
|---|---|---|
developer_token | Required; secret | Google Ads API developer token for an approved access tier. |
client_id | Required | Google OAuth client ID used to obtain the refresh token. |
client_secret | Required; secret | OAuth Client Secret |
refresh_token | Required; secret | Refresh token authorized for https://www.googleapis.com/auth/adwords with offline access. The Google Ads client refreshes access tokens automatically. |
customer_id | Required | Numeric account ID, with or without hyphens. |
login_customer_id | Optional | Optional manager account ID. |
requirements.in
Save these dependencies in requirements.in alongside connector.py; the example includes packages used by its sync. Install the published vortexify package for local development. Keep sync-specific dependencies in the sync's own requirements.in when splitting the implementation into separate resources.
vortexify==0.1.1
google-ads>=25,<50
pandas>=2,<3Google client libraries use the configured credentials and scopes. Installing the library does not grant project/account access; select a supported Python runtime and compatible library version.
Worked example: sync campaigns
campaigns — Campaign resource queried with GAQL and native GoogleAdsService pagination.
The complete files below implement only this example resource. They illustrate the current SDK signatures, a separate preview, bounded DataFrame output, and source-specific extraction. They do not establish complete source coverage. Live credentials and platform ingestion have not been used to validate this guide.
With default paths, place the connector files in connectors/google-ads/ and sync files in connectors/google-ads/syncs/campaigns/. Use your project configuration if it specifies different paths.
Connection schema: config.json
This file contains a JSON Schema, not actual credential values. Provide those through platform configuration.
{
"type": "object",
"properties": {
"developer_token": {
"type": "string",
"title": "Developer Token",
"minLength": 1,
"x-vortexify-secret": true,
"order": 0,
"description": "Google Ads API developer token for an approved access tier."
},
"client_id": {
"type": "string",
"minLength": 1,
"title": "OAuth Client ID",
"order": 1,
"description": "Google OAuth client ID used to obtain the refresh token."
},
"client_secret": {
"type": "string",
"minLength": 1,
"x-vortexify-secret": true,
"title": "OAuth Client Secret",
"order": 2
},
"refresh_token": {
"type": "string",
"minLength": 1,
"x-vortexify-secret": true,
"title": "Refresh Token",
"order": 3,
"description": "Refresh token authorized for https://www.googleapis.com/auth/adwords with offline access. The Google Ads client refreshes access tokens automatically."
},
"customer_id": {
"type": "string",
"title": "Customer ID",
"description": "Numeric account ID, with or without hyphens.",
"minLength": 1,
"order": 4
},
"login_customer_id": {
"type": "string",
"title": "Login Customer ID",
"description": "Optional manager account ID.",
"minLength": 1,
"order": 5
}
},
"required": [
"developer_token",
"client_id",
"client_secret",
"refresh_token",
"customer_id"
]
}Connector: connector.py
from google.ads.googleads.client import GoogleAdsClient
from vortexify import Column, Connector, DiscoveredTable
class GoogleAdsConnector(Connector):
vendor_domain = "google.com"
def client(self):
if self._client is None:
settings = {
"developer_token": self.config["developer_token"],
"client_id": self.config["client_id"],
"client_secret": self.config["client_secret"],
"refresh_token": self.config["refresh_token"],
"use_proto_plus": True,
}
if self.config.get("login_customer_id"):
settings["login_customer_id"] = self.config[
"login_customer_id"
].replace("-", "")
self._client = GoogleAdsClient.load_from_dict(settings).get_service(
"GoogleAdsService"
)
return self._client
def check(self) -> bool:
rows = self.client().search(
customer_id=self.config["customer_id"].replace("-", ""),
query="SELECT customer.id FROM customer LIMIT 1",
)
next(iter(rows), None)
return True
def discover(self) -> list[DiscoveredTable]:
return [
DiscoveredTable(
name="campaigns",
schema=[
Column(name="id", type="integer"),
Column(name="name", type="string"),
Column(name="status", type="string"),
],
)
]
def close(self) -> None:
if self._client is not None:
try:
self._client.transport.close()
finally:
self._client = NoneSync schema: syncs/campaigns/config.json
{
"type": "object",
"properties": {},
"required": []
}Sync: syncs/campaigns/sync.py
from collections.abc import Iterator
from math import isfinite
import pandas as pd
from vortexify import Sync
class GoogleAdsCampaignsSync(Sync):
def preview(self) -> pd.DataFrame:
records = self.client.search(
customer_id=self.connector.config["customer_id"].replace("-", ""),
query="SELECT campaign.id, campaign.name, campaign.status FROM campaign ORDER BY campaign.id LIMIT 100",
)
rows = []
for record in records:
rows.append(
{
"id": record.campaign.id,
"name": record.campaign.name,
"status": record.campaign.status.name,
}
)
if len(rows) == 100:
break
return pd.DataFrame(rows, columns=["id", "name", "status"], dtype=object)
def extract(
self, *, avg_row_size_bytes: float | None = None
) -> Iterator[pd.DataFrame]:
memory_mib = self.compute.memory if self.compute else None
memory_mib = memory_mib if memory_mib is not None and memory_mib > 0 else 1024
target_bytes = min(16 * 1024 * 1024, max(1, memory_mib * 1024 * 1024 // 20))
batch_size = 100
if avg_row_size_bytes is not None:
if not isfinite(avg_row_size_bytes) or avg_row_size_bytes <= 0:
raise ValueError("avg_row_size_bytes must be finite and positive")
batch_size = max(1, int(target_bytes / max(1.0, avg_row_size_bytes) / 4))
batch_size = min(batch_size, 10000)
records = self.client.search(
customer_id=self.connector.config["customer_id"].replace("-", ""),
query="SELECT campaign.id, campaign.name, campaign.status FROM campaign ORDER BY campaign.id",
)
rows = []
for record in records:
rows.append(
{
"id": record.campaign.id,
"name": record.campaign.name,
"status": record.campaign.status.name,
}
)
if len(rows) == batch_size:
yield pd.DataFrame(rows, columns=["id", "name", "status"], dtype=object)
rows = []
if rows:
yield pd.DataFrame(rows, columns=["id", "name", "status"], dtype=object)Running and extending the example
Register the connector before the sync and commit each resource separately:
vortexify create connector google-ads
vortexify create sync google-ads/campaignsEdit the files, inspect vortexify status, and use the CLI guide to commit and configure them. Registration alone does not run extraction or deploy the connector.
Preview must run with a fresh connector and disposable state. The extraction implementation determines full/incremental behavior; merely setting a mode does not implement change capture. API/driver failures propagate unless explicitly handled in the code. Check source limits and single-record/response sizes as well as output batch size.
Sources and further reading
- Vendor API and data reference — Resource definitions, authentication, fields, and source-specific limits. Check the selected resource's reference when extending the example.
- Vortexify connector and sync contracts — Client ownership, discovery, preview isolation, and DataFrame extraction.
- Vortexify CLI workflow — Project paths, resource registration, source commits, and credential configuration.