Skip to main content

Python SDK for Agent Berlin - AI-powered SEO and AEO automation

Project description

Agent Berlin Python SDK

Installation

pip install agentberlin

Agent Berlin Python SDK provides AI-powered SEO automation.

Quick Start

from agentberlin import AgentBerlin
client = AgentBerlin()

Available APIs

  • ga4: query() - Query GA4 analytics with custom metrics, dimensions, and filters
  • pages: list(), search(), get(), get_detailed() - List pages, search pages, get page info, and get page details with similarity analysis
  • keywords: list(), search(), list_clusters(), get_cluster_keywords() - List/search keywords, explore clusters
  • brand: get_profile(), update_profile(), get_stats() - Brand profile management and precomputed aggregate stats
  • crawls: versions(), diff(), diff_url(), list_issues(), get_issue() - Crawl run timeline, URL-by-URL diffs between two crawls, and the latest crawl's SEO issue report
  • google_search: web(), news(), images(), shopping(), ai_overview(), ai_mode() - Google Search with all result types
  • bing_search: web() - Bing web search
  • google_maps: search(), reviews() - Google Maps places and reviews
  • google_trends: interest_over_time(), related_queries() - Google Trends data
  • ai_search: query() - Grounded AI-powered web search with citations (Perplexity, ChatGPT, Gemini)
  • gsc: query(), get_site(), list_sitemaps(), get_sitemap(), inspect_url() - Search analytics, sitemaps, URL inspection
  • reddit: search(), get_posts(), get_subreddit_posts(), get_post_comments() - Reddit posts, comments, and subreddit feeds (SLOW: 30-120s per call, batch your inputs)
  • linkedin: post(), status(), list_posts() - Publish posts to the connected user's personal LinkedIn profile (write-only; token expires every ~60 days — check status() and surface expiry warnings to the user)
  • bing_webmaster: query_stats(), page_stats(), traffic_stats(), crawl_stats(), get_site() - Bing Webmaster Tools analytics
  • amplitude: query(), list_events(), list_cohorts(), funnel(), retention() - Product analytics (sessions, users, events, funnels, retention)
  • posthog: query() - Run any PostHog query kind (HogQLQuery, TrendsQuery, FunnelsQuery, RetentionQuery, …) against a connected PostHog project
  • pagespeed: analyze() - Google PageSpeed Insights Lighthouse audit (performance score + Core Web Vitals)
  • market: ranked_keywords(), keyword_ideas(), domain_overview(), historical_rank_overview(), bulk_traffic_estimation(), top_pages(), technologies() - Competitive organic-search intelligence about any domain (yours or a competitor's)
  • backlinks: summary(), list(), referring_domains(), competitors() - Off-page/authority profile of any domain
  • ai_mentions: search(), summary(), top_domains(), top_pages() - How a brand/domain/keyword is mentioned inside AI answers (ChatGPT & Google AI)
  • email_marketing: capabilities(), list_audiences(), add_to_audience(), remove_from_audience(), get_contact(), upsert_contact(), create_campaign(), update_campaign(), get_campaign(), list_campaigns(), campaign_metrics(), schedule_campaign(), send_campaign(), cancel_scheduled_campaign(), create_segment(), list_templates(), create_template(), update_template(), track_event(), list_suppressed() - The project's connected email service provider (e.g. Klaviyo); campaigns are created as DRAFTS for human review in the ESP's own UI
  • google_ads: search_terms(), keywords(), campaigns(), ad_groups(), landing_pages(), coverage() - Google Ads performance reports (read-only, nightly-synced warehouse; cost values in micros)
  • openai_ads: account(), campaigns(), ad_groups(), ads(), list_campaigns(), record_hint_set(), list_hint_sets() - OpenAI (ChatGPT) Ads performance insights read live from the connected Advertiser API key (read-only; six metrics only; no keywords/search terms; insights money in account currency, not micros), plus recording/recall of generated ad-group context_hints for history
  • microsoft_ads: search_terms(), keywords(), campaigns(), ad_groups(), landing_pages(), share_of_voice(), coverage() - Microsoft Advertising performance reports (read-only, nightly-synced warehouse; money values are decimal, not micros; share-of-voice keeps ~6 months of history)
  • linkedin_ads: account(), campaign_groups(), campaigns(), creatives(), list_campaigns() - LinkedIn Ads (paid social) performance insights read live from the connected ad account via the /adAnalytics finder (read-only; no keywords/search terms; cost_in_local_currency in account currency, not micros)

API Reference

ga4

Direct access to Google Analytics 4 Data API for flexible reporting with custom metrics, dimensions, and filters.

client.ga4.query()

Query GA4 analytics data using the GA4 Data API.

This method maps directly to Google Analytics 4's runReport API, providing full flexibility for analytics queries.

Signature:

result = client.ga4.query(
    metrics=["totalUsers", "sessions"],      # Required: list of metric names
    start_date="2024-01-01",                 # Required: YYYY-MM-DD format
    end_date="2024-01-31",                   # Required: YYYY-MM-DD format
    dimensions=["country", "date"],          # Optional: breakdown dimensions
    dimension_filter={                       # Optional: filter results
        "field_name": "sessionDefaultChannelGroup",
        "string_filter": {"match_type": "EXACT", "value": "Organic Search"}
    },
    order_bys=[{"metric": "totalUsers", "desc": True}],  # Optional: sorting
    limit=100,                               # Optional: max rows
    offset=0,                                # Optional: pagination offset
    date_ranges=[                            # Optional: date comparison
        {"start_date": "2024-01-01", "end_date": "2024-01-31", "name": "current"},
        {"start_date": "2023-12-01", "end_date": "2023-12-31", "name": "previous"}
    ],
    currency_code="USD"                      # Optional: for monetary metrics
)

Args:

  • metrics: List of metric names to retrieve (required). Examples: "totalUsers", "sessions", "ecommercePurchases", "screenPageViews"
  • start_date: Start date in YYYY-MM-DD format (required)
  • end_date: End date in YYYY-MM-DD format (required)
  • dimensions: Optional list of dimension names. Examples: "country", "sessionDefaultChannelGroup", "date", "deviceCategory"
  • dimension_filter: Optional filter to restrict results:
    • Simple filter: {"field_name": "country", "string_filter": {"match_type": "EXACT", "value": "US"}}
    • In-list filter: {"field_name": "country", "in_list_filter": {"values": ["US", "CA"]}}
    • AND group: {"and_group": {"expressions": [filter1, filter2]}}
    • OR group: {"or_group": {"expressions": [filter1, filter2]}}
    • NOT: {"not_expression": filter}
  • order_bys: Optional list of ordering. Example: [{"metric": "totalUsers", "desc": True}]
  • limit: Maximum number of rows to return
  • offset: Number of rows to skip (for pagination)
  • date_ranges: Optional list of date ranges for comparison
  • currency_code: Optional currency code for monetary metrics (e.g., "USD")

Returns GA4QueryResponse with:

  • rows - List of GA4Row objects
  • row_count - Total number of rows returned
  • totals - Aggregate totals for all metrics

Each GA4Row has:

  • dimensions - Dict mapping dimension names to values
  • metrics - Dict mapping metric names to values

Note: Item-scoped dimensions (e.g., itemName, itemId, itemBrand) cannot be combined with event-scoped metrics (e.g., totalUsers, sessions). Use item-scoped metrics (itemsViewed, itemRevenue) instead.

Example:

# Get organic traffic by country
result = client.ga4.query(
    metrics=["totalUsers", "sessions"],
    dimensions=["country"],
    start_date="2024-01-01",
    end_date="2024-01-31",
    dimension_filter={
        "field_name": "sessionDefaultChannelGroup",
        "string_filter": {"match_type": "EXACT", "value": "Organic Search"}
    },
    order_bys=[{"metric": "totalUsers", "desc": True}],
    limit=10
)
for row in result.rows:
    print(f"{row.dimensions['country']}: {row.metrics['totalUsers']} users")

pages

A powerful search engine backed by yours and your competitors' crawled pages. The dataset is produced by crawl runs — see client.crawls for the run timeline, diffs between two crawls, and the latest crawl's issue report.

client.pages.list()

List pages with pagination and optional filtering.

Signature:

result = client.pages.list(
    limit=100,           # Max pages (default: 100, max: 1000)
    offset=0,            # Pagination offset (default: 0)
    domain="example.com", # Optional: filter by domain
    own_only=True        # Optional: exclude competitor pages
)

Args:

  • limit: Maximum pages to return (default: 100, max: 1000)
  • offset: Pagination offset (default: 0)
  • domain: Filter by domain (e.g., "competitor.com")
  • own_only: If True, only return your own pages

Returns PageListResponse with:

  • pages - List of PageListItem objects
  • total - Number of pages in response
  • limit - Limit used
  • offset - Offset used

Each PageListItem has:

  • url - Page URL
  • title - Page title
  • meta_description - Meta description (optional)
  • h1s - List of H1 headings (empty if none)
  • domain - Domain name (optional)
  • status_code / redirect_url / redirect_type / content_type / canonical / meta_robots / x_robots_tag / indexability / indexability_status / blocked_by_robots / word_count / response_time / last_modified - Per-page crawl-health summary (None for pages indexed before these fields existed)
  • structured_data_types / schema_errors / schema_warnings - Structured data (schema) summary (None if the page has no schema)

client.pages.search()

Search for similar pages with multiple input modes.

This unified method supports two input modes (exactly one required):

  • query: Simple text query for semantic search across all pages
  • content: Find similar pages to provided raw content

Additionally, contains can filter by URL or content pattern (regex supported):

  • Standalone: Returns all pages matching the pattern, sorted by URL
  • Combined: Filters semantic search results by the pattern

Note: For chunk-level analysis, use pages.get_detailed(url) instead.

Signature:

# Query mode - semantic search
results = client.pages.search(
    query="SEO best practices",   # Text query for semantic search
    count=10,                      # Max results (default: 10)
    offset=0,                      # Pagination offset (default: 0)
    similarity_threshold=0.60      # Min similarity 0-1 (default: 0.60)
)

# Content mode - find similar pages to raw content
results = client.pages.search(
    content="This is my page content about SEO..."
)

# Query mode with filters
results = client.pages.search(
    query="SEO guide",
    domain="competitor.com",       # Filter by domain
    status_code="200",             # Filter by HTTP status
    topic="SEO",                   # Filter by topic name
    page_type="pillar"             # Filter by page type
)

# Contains-only mode - filter by URL pattern
results = client.pages.search(
    contains="/blog/.*"            # Regex pattern for URL or content
)

# Combine semantic search with contains filter
results = client.pages.search(
    query="pricing",
    contains="enterprise"          # Also filter by pattern in URL/content
)

Args:

  • query: Text query for semantic search (mutually exclusive with content)
  • content: Raw content string (mutually exclusive with query)
  • contains: Filter by URL or content pattern (regex supported, case-insensitive). Matches if page URL or content contains the pattern. Can be used alone or with query/content.
  • count: Max recommendations (default: 10, max: 50)
  • offset: Pagination offset (default: 0)
  • similarity_threshold: Min similarity 0-1 (default: 0.60)
  • domain: Filter by domain (e.g., "competitor.com")
  • own_only: If True, filter to only your own pages (excludes competitors)
  • status_code: Filter by HTTP status code ("200", "404", "error", "redirect", "success")
  • topic: Filter by topic name
  • page_type: Filter by page type ("pillar" or "landing")

Returns PageSearchResponse with:

  • similar_pages - List of SimilarPage objects (always includes evidence and topic_info)

Each SimilarPage has:

  • target_page_url - URL of recommended page
  • similarity_score - Similarity score 0-1
  • match_type - "page_to_page"
  • page_type - "pillar", "landing", or None
  • assigned_topic - Topic if target is assigned as pillar/landing
  • title - Page title
  • h1s - List of H1 headings (empty if none)
  • meta_description - Meta description
  • evidence - List of Evidence objects (h_path, text) for top chunks
  • domain - Page domain
  • status_code - HTTP status code
  • topic_info - TopicInfo object (topics, topic_scores, page_type, assigned_topic)

client.pages.get()

Get page information.

For similarity analysis (similar_pages and similar_chunks), use pages.get_detailed instead.

Signature:

# Basic page details (metadata only)
page = client.pages.get(
    url="https://example.com/blog/article"
)

# Include page content (truncated to 500 chars)
page = client.pages.get(
    url="https://example.com/blog/article",
    content_length=500
)

# With the page's raw JSON-LD structured data
page = client.pages.get(
    url="https://example.com/blog/article",
    include_schema=True
)

Args:

  • url: The page URL to look up
  • content_length: Max characters of content to return (default: 0, no content). There is no upper limit - pass a large value (e.g., 999999) to get full content. Note: pages can have large content (thousands of words), so large values will significantly increase response size.
  • include_schema: If True, also return the page's raw JSON-LD bodies in structured_data_jsonld (can be large). The schema summary fields are always returned.

Returns PageDetailResponse with:

  • url - Page URL
  • title - Page title
  • meta_description - Meta description
  • h1s - List of H1 headings (empty if none)
  • domain - Domain name
  • links.inlinks - List of incoming links (PageLink objects)
  • links.outlinks - List of outgoing links (PageLink objects)
  • topic_info.topics - List of topic names
  • topic_info.topic_scores - List of topic relevance scores
  • topic_info.page_type - "pillar" or "landing"
  • topic_info.assigned_topic - Primary assigned topic
  • content - Page content truncated to content_length (only returned if content_length > 0)
  • content_length - Total content length in characters
  • status_code / redirect_url / redirect_type / content_type / canonical / meta_robots / x_robots_tag / indexability / indexability_status / blocked_by_robots / word_count / response_time / last_modified - Per-page crawl-health and indexability fields (None for pages indexed before these fields existed)
  • structured_data_types - Detected schema.org types, e.g. ["Organization", "WebSite"] (None if the page has no schema)
  • schema_errors - Schema validation error count (None if no schema)
  • schema_warnings - Schema validation warning count (None if no schema)
  • structured_data_jsonld - Raw JSON-LD script bodies (only when include_schema=True)

client.pages.get_detailed()

Get page details with similarity analysis.

Like get, but additionally computes similar_pages and similar_chunks (expensive, 1-10s).

Signature:

page = client.pages.get_detailed(
    url="https://example.com/blog/article"
)
page.similar_pages    # List of SimilarPage objects
page.similar_chunks   # List of SimilarChunkSet objects

Args:

  • url: The page URL to look up
  • content_length: Max characters of content to return (default: 0, no content). There is no upper limit - pass a large value (e.g., 999999) to get full content. Note: pages can have large content (thousands of words), so large values will significantly increase response size.
  • include_schema: If True, also return the page's raw JSON-LD bodies in structured_data_jsonld (can be large). The schema summary fields are always returned.

Returns PageDetailResponse with all the fields get returns, plus:

  • similar_pages - List of SimilarPage objects
  • similar_chunks - List of SimilarChunkSet objects

keywords

Search for keywords and explore keyword clusters (powered by SEMRush & DataForSEO)

client.keywords.list()

List all keywords with pagination.

Signature:

result = client.keywords.list(
    limit=100,    # Max keywords (default: 100, max: 1000)
    offset=0      # Pagination offset (default: 0)
)

Returns KeywordListResponse with:

  • keywords - List of KeywordResult objects
  • total - Total number of keywords

Each KeywordResult has:

  • keyword - The keyword text
  • intent - Search intent (optional)
  • locations - List of LocationMetrics objects with location-specific data

Each LocationMetrics has:

  • code - Location code (e.g., "us", "uk", "de")
  • volume - Monthly search volume (optional)
  • difficulty - Difficulty score 0-100 (optional)
  • cpc - Cost per click (optional)
  • position - SERP position for your domain (optional)

Example:

# Get first 100 keywords
result = client.keywords.list()
for kw in result.keywords:
    # Access location-specific metrics
    for loc in kw.locations:
        print(f"{kw.keyword} ({loc.code}): volume={loc.volume}, difficulty={loc.difficulty}")

# Paginate through all keywords
all_keywords = []
offset = 0
while True:
    result = client.keywords.list(limit=1000, offset=offset)
    all_keywords.extend(result.keywords)
    if len(result.keywords) < 1000:
        break
    offset += 1000

client.keywords.search()

Search for keywords.

Signature:

keywords = client.keywords.search(
    query="digital marketing",  # Search query
    limit=10,                   # Max results (default: 10)
    match_type="semantic"       # "semantic", "exact", or "contains" (default: "semantic")
)

Parameters:

  • query - Search query string (required)
  • limit - Maximum results to return (default: 10, max: 50)
  • match_type - Search mode (default: "semantic"):
    • "semantic" - Hybrid AI search using embeddings and BM25
    • "exact" - Case-insensitive exact string match
    • "contains" - Case-insensitive substring/contains match

Returns KeywordSearchResponse with keywords list and total count. Each keyword has:

  • keyword - The keyword text
  • intent - Search intent: "informational", "commercial", "transactional", "navigational"
  • locations - List of LocationMetrics with location-specific data:
    • code - Location code (e.g., "us", "uk")
    • volume - Monthly search volume
    • difficulty - Difficulty score (0-100)
    • cpc - Cost per click
    • position - SERP position (optional)

Examples:

# Semantic search (default) - finds related keywords
results = client.keywords.search(query="running shoes")

# Exact match - finds only "running shoes" exactly
results = client.keywords.search(query="running shoes", match_type="exact")

# Substring match - finds keywords containing "running"
results = client.keywords.search(query="running", match_type="contains")

client.keywords.list_clusters()

List all keyword clusters with representative keywords.

Keywords are grouped into clusters using HDBSCAN clustering based on semantic similarity. This method returns a summary of all clusters for topic exploration.

Signature:

clusters = client.keywords.list_clusters(
    representative_count=3  # Keywords per cluster (default: 3, max: 10)
)

Returns ClusterListResponse with:

  • clusters - List of ClusterSummary objects
  • noise_count - Number of unclustered keywords (cluster_id=-1)
  • total_keywords - Total keywords in the index
  • cluster_count - Number of clusters (excluding noise)

Each ClusterSummary has:

  • cluster_id - Unique cluster identifier
  • size - Number of keywords in cluster
  • representative_keywords - Top keywords by volume

Example:

clusters = client.keywords.list_clusters()
for cluster in clusters.clusters:
    print(f"Cluster {cluster.cluster_id}: {cluster.size} keywords")
    print(f"  Topics: {', '.join(cluster.representative_keywords)}")

client.keywords.get_cluster_keywords()

Get keywords belonging to a specific cluster.

Use this to explore keywords within a cluster. Supports pagination for large clusters. Use cluster_id=-1 to get noise (unclustered) keywords.

Signature:

result = client.keywords.get_cluster_keywords(
    cluster_id=0,   # Required: cluster ID (-1 for noise)
    limit=50,       # Max keywords (default: 50, max: 1000)
    offset=0        # Pagination offset (default: 0)
)

Returns ClusterKeywordsResponse with:

  • keywords - List of ClusterKeywordResult objects
  • total - Total keywords in this cluster
  • cluster_id - The requested cluster ID
  • limit - Limit used
  • offset - Offset used

Each ClusterKeywordResult has:

  • keyword - The keyword text
  • cluster_id - Cluster ID
  • intent - Search intent (optional)
  • locations - List of LocationMetrics with location-specific data:
    • code - Location code (e.g., "us", "uk")
    • volume - Monthly search volume (optional)
    • difficulty - Difficulty score 0-100 (optional)
    • cpc - Cost per click (optional)
    • position - SERP position (optional)

Example:

# Get top keywords from cluster 0
result = client.keywords.get_cluster_keywords(0, limit=20)
for kw in result.keywords:
    for loc in kw.locations:
        print(f"{kw.keyword} ({loc.code}): volume={loc.volume}")

# Get noise keywords (unclustered)
noise = client.keywords.get_cluster_keywords(-1, limit=100)

brand

Your brand profile including company info, competitors, industries, and target markets.

client.brand.get_profile()

Get the complete brand profile including all company information, competitors, target industries, business models, and geographic scope.

Signature:

profile = client.brand.get_profile()

Returns BrandProfileResponse with:

  • domain - Domain name
  • name - Brand name
  • brand_context - Structured markdown brand context (What we do, USP, ICP, etc.)
  • domain_authority - Domain authority score
  • competitors - List of competitor domains
  • industries - List of industries
  • business_models - List of business models
  • company_size - Company size
  • target_customer_segments - Target segments
  • geographies - Target geographies
  • topics - List of Topic objects (value, pillar_page_url, landing_page_url)
  • sitemaps - Sitemap URLs
  • profile_urls - Profile URLs

Example:

profile = client.brand.get_profile()
print(f"Domain: {profile.domain}")
print(f"Topics: {[(t.value, t.pillar_page_url) for t in profile.topics]}")

client.brand.update_profile()

Update brand profile fields.

Important behavior:

  • Only provided fields are updated
  • Fields set to None/undefined are IGNORED (not cleared)
  • To clear a list field, pass an empty list []
  • To clear a string field, pass an empty string ""

Signature:

result = client.brand.update_profile(
    name="Project Name",                          # Optional: Project name
    brand_context="Business description...",      # Optional: Brand context (markdown)
    competitors=["competitor1.com"],              # Optional: Competitor domains
    industries=["SaaS", "Technology"],            # Optional: Industries
    business_models=["B2B", "Enterprise"],        # Optional: Business models
    company_size="startup",                       # Optional: solo, early_startup, startup, smb, mid_market, enterprise
    target_customer_segments=["Enterprise"],      # Optional: Target segments
    geographies=["US", "EU"],                     # Optional: Geographic regions
    topics=["SEO", "Content Marketing"]           # Optional: Topic values
)

Returns the full updated BrandProfileResponse (same shape as get_profile), reflecting the applied changes.

Examples:

# Update multiple fields at once
result = client.brand.update_profile(
    brand_context="We are a B2B SaaS company...",
    competitors=["competitor1.com", "competitor2.com"],
    topics=["SEO", "Content Marketing", "Analytics"]
)

# Only update one field (others unchanged)
result = client.brand.update_profile(industries=["SaaS", "MarTech"])

# Clear a list field
result = client.brand.update_profile(competitors=[])

client.brand.get_stats()

Get precomputed aggregate statistics for the project's brand.

These stats are generated at build time from the crawl (ScreamingFrog) and keyword (SEMrush / DataForSEO) data, and served from disk — no worker machine is provisioned for the call, making it cheap to poll for dashboards.

Signature:

stats = client.brand.get_stats()

Returns BrandStatsResponse. Every top-level section is optional — when the underlying source data was missing at build time, that section is absent.

Top-level fields:

  • brand_name - Brand identifier
  • generated_at - Unix timestamp when the stats were produced
  • pages - PagesStats (crawl health, content structure, URL shape, redirects, performance)
  • links - LinksStats (in/out link totals, orphan/dead-end pages, top-linked pages, broken outlinks, classification & type counts)
  • keywords - KeywordsStats (volume & CPC rollups, difficulty buckets, SERP ranking buckets, intent distribution)
  • cross - CrossStats (keyword-to-page matches, top landing pages, topic coverage)

Example:

stats = client.brand.get_stats()
if stats.pages and stats.pages.crawl_health:
    sc = stats.pages.crawl_health.status_class_counts
    print(f"4xx pages: {sc.four_xx}, 5xx pages: {sc.five_xx}")
if stats.keywords and stats.keywords.ranking:
    print(f"Top-3 ranking keywords: {stats.keywords.ranking.top_3_count}")
if stats.cross:
    for lp in stats.cross.top_landing_pages[:5]:
        print(f"{lp.url}: {lp.keyword_count} kws, {lp.total_volume} vol")

crawls

The project's crawl runs and their artifacts. Crawl runs are the pipeline that produces the pages dataset queryable via client.pages (one run crawls your own domain plus competitor domains). This resource covers the run axis: the version timeline, URL-by-URL diffs of the dataset between two runs, and the SEO/structural issue report of the latest run (own domain only). While project data is still being collected, these methods raise an error with HTTP 503.

client.crawls.versions()

List the project's completed crawl versions, newest first.

Each version is one crawl run over your own domain and competitor domains; its pages are what client.pages serves once the run is indexed.

Signature:

result = client.crawls.versions()

Returns CrawlVersionsResponse with:

  • versions - List of CrawlVersion objects

Each CrawlVersion has:

  • version_id - The identifier to pass to diff()/diff_url()
  • run_id - The crawl run it came from (equal to version_id for current runs)
  • page_count - Number of pages in that crawl (optional)
  • trigger - What started the crawl (e.g., "scheduled", "manual")
  • created_at - When the crawl started
  • finished_at - When the crawl finished (optional)

client.crawls.diff()

Diff the page dataset between two crawl versions, URL-by-URL.

The diff is symmetric over the union of URLs in both versions: every URL is reported as changed, new (only in target), deleted (only in base), or unchanged.

Signature:

diff = client.crawls.diff(
    base="<older version_id>",
    target="<newer version_id>",
    include_unchanged=False,  # Optional: also list unchanged URLs
    limit=100,                # Optional: max URL rows
    offset=0                  # Optional: pagination offset
)

Returns CrawlDiffResponse with:

  • summary - CrawlDiffSummary with base, target, changed, new, deleted, unchanged, total
  • urls - List of CrawlURLChange objects (url, status, changed_fields)

Example:

versions = client.crawls.versions().versions
diff = client.crawls.diff(base=versions[1].version_id, target=versions[0].version_id)
print(f"{diff.summary.changed} changed, {diff.summary.new} new, {diff.summary.deleted} deleted")
for change in diff.urls:
    print(change.url, change.status, change.changed_fields)

client.crawls.diff_url()

Get the before/after text + similarity for one URL across two crawl versions.

Signature:

detail = client.crawls.diff_url(
    base="<older version_id>",
    target="<newer version_id>",
    url="https://example.com/pricing"
)

Returns CrawlURLDiffResponse with:

  • url, base, target - Echo of the request
  • fields - List of CrawlFieldDiff objects (field, before, after, similarity 0-1, before_missing, after_missing)
  • overall_similarity - Similarity of the page across the two versions (0-1)

Raises a not-found error if the URL is in neither version.

client.crawls.list_issues()

List the latest crawl's SEO/structural issue report (summaries only, no URL lists).

Issues cover the project's own domain and are a snapshot, not a history: every crawl replaces the previous report, and an empty report after a clean crawl is meaningful. crawled_at/crawl_run_id identify the producing crawl.

Signature:

report = client.crawls.list_issues(
    type="Issue",       # Optional: "Issue" | "Warning" | "Opportunity" (case-insensitive)
    priority="High",    # Optional: "High" | "Medium" | "Low" (case-insensitive)
    limit=50,           # Optional: max issues
    offset=0            # Optional: pagination offset
)

Returns CrawlIssuesListResponse with:

  • issues - List of CrawlIssueSummary objects
  • total - Total matching issues
  • crawled_at / crawl_run_id - Provenance of the snapshot (absent before the first crawl)

Each CrawlIssueSummary has:

  • name - Exact issue name (pass to get_issue() for the affected URLs)
  • type - "Issue" | "Warning" | "Opportunity"
  • priority - "High" | "Medium" | "Low"
  • description - What the issue means
  • how_to_fix - Remediation guidance
  • help_url - Documentation link (optional)
  • url_count - Total number of affected URLs

client.crawls.get_issue()

Get one issue by name with a paginated window of its affected URLs.

Signature:

detail = client.crawls.get_issue(
    name="Canonicals: Missing",  # Exact name from list_issues()
    limit=200,                   # Optional: max URLs per page
    offset=0                     # Optional: offset into the URL list
)

Returns CrawlIssueDetailResponse with all CrawlIssueSummary fields plus:

  • urls - The requested window of affected URLs
  • urls_total - Total affected URLs
  • crawled_at / crawl_run_id - Provenance of the snapshot

Raises a not-found error for an unknown issue name.

Example:

report = client.crawls.list_issues(priority="High")
for issue in report.issues:
    detail = client.crawls.get_issue(name=issue.name, limit=200)
    print(issue.name, detail.urls[:5])

google_search

Search the web using Google - includes web results, news, images, shopping, and AI features.

web() surfaces classic SERP features. In addition to organic results (with sitelinks), the web response exposes — when Google actually returns them for the query — the featured snippet (answer_box), People Also Ask (related_questions), knowledge panel (knowledge_graph), local pack (local_results), inline images / videos, top stories, inline AI Overview (ai_overview), and related_searches. Absent surfaces are omitted.

Three AI surfaces, three different things. (1) web().features.ai_overview is the AI Overview block embedded in a normal SERP, set only when Google attaches one to the query — useful for "would an organic searcher see an AI block?". (2) ai_overview() always asks Google for the AI Overview block specifically; returns the same shape as the embedded one without the surrounding SERP. (3) ai_mode() is Google's distinct AI Mode product (the udm=50 conversational tab); returns a richer payload with full markdown and structured blocks/citations.

Vertical methods (news(), images(), shopping()) surface only the result list for their own surface — they do not carry SERP features.

client.google_search.web()

Search the web using Google. Returns organic results plus classic SERP surfaces when Google includes them for the query.

Signature:

results = client.google_search.web(
    query="best seo tools",
    max_results=10,      # Max results (1-100, default: 10)
    country="us",        # Optional: ISO 3166-1 alpha-2 country code
    language="en",       # Optional: ISO 639-1 language code
    location="Austin,Texas,United States"  # Optional: canonical name for city-level targeting
)

Returns GoogleSearchWebResponse with:

  • query - The search query
  • results - List of GoogleSearchResult objects (organic results)
  • total - Organic results count
  • answer_box - GoogleSearchAnswerBox or None (featured snippet)
  • related_questions - List of GoogleSearchRelatedQuestion (People Also Ask)
  • knowledge_graph - GoogleSearchKnowledgeGraph or None (knowledge panel)
  • local_results - List of GoogleSearchLocalResult (local pack)
  • inline_images - List of GoogleSearchInlineImage (inline carousel)
  • inline_videos - List of GoogleSearchInlineVideo (inline carousel)
  • top_stories - List of GoogleSearchTopStory (top stories carousel)
  • ai_overview - GoogleSearchAIOverview or None (inline AI Overview)
  • related_searches - List of GoogleSearchRelatedSearch
  • search_metadata - Optional metadata (id, status, total_time_taken)

Each organic result has: title, url, snippet, displayed_link, date, thumbnail, sitelinks (list of {title, link, snippet}).

All SERP-feature fields default to empty / None when Google did not include that surface for the query. Detect AI Overview with if results.ai_overview:, PAA with if results.related_questions:, etc.

client.google_search.news()

Search Google News.

Signature:

results = client.google_search.news(
    query="AI technology",
    max_results=10,      # Max results (1-100, default: 10)
    country="us",        # Optional: ISO 3166-1 alpha-2 country code
    language="en"        # Optional: ISO 639-1 language code
)

Returns GoogleSearchNewsResponse with:

  • query - The search query
  • results - List of GoogleSearchNewsResult objects
  • total - Total results count Each result has: title, url, snippet, source, date, thumbnail

client.google_search.images()

Search Google Images.

Signature:

results = client.google_search.images(
    query="modern website design",
    max_results=10,      # Max results (1-100, default: 10)
    country="us"         # Optional: ISO 3166-1 alpha-2 country code
)

Returns GoogleSearchImagesResponse with:

  • query - The search query
  • results - List of GoogleSearchImageResult objects
  • total - Total results count Each result has: title, url, original, thumbnail, source, width, height

client.google_search.shopping()

Search Google Shopping.

Signature:

results = client.google_search.shopping(
    query="wireless headphones",
    max_results=10,      # Max results (1-100, default: 10)
    country="us"         # Optional: ISO 3166-1 alpha-2 country code
)

Returns GoogleSearchShoppingResponse with:

  • query - The search query
  • results - List of GoogleSearchShoppingResult objects
  • total - Total results count Each result has: title, url, price, source, thumbnail, rating, reviews

client.google_search.ai_overview()

Fetch Google's inline AI Overview block — the AI summary that sometimes appears above the organic results on a regular SERP. For Google's standalone AI Mode product (different surface, richer response), see ai_mode().

Latency: typically 2–6.5 s (measured via SerpAPI, the routed provider). AI Overview is best-effort upstream — it does not appear for every query — so budget for occasional empty results.

Signature:

results = client.google_search.ai_overview(
    query="What is SEO?",
    country="us",        # Optional: ISO 3166-1 alpha-2 country code
    language="en",       # Optional: ISO 639-1 language code
    location="Austin,Texas,United States"  # Optional: canonical name for city-level targeting
)

Returns GoogleSearchAIOverviewResponse with:

  • query - The search query
  • answer - AI-generated answer (joined paragraph text)
  • sources - List of GoogleSearchResult objects (source citations)
  • search_metadata - Optional metadata

client.google_search.ai_mode()

Query Google's dedicated AI Mode product (the udm=50 conversational AI tab). Distinct from ai_overview() — AI Mode is a separate product with a richer payload.

Latency: typically ~6 s (measured via SerpAPI's google_ai_mode engine, the routed provider), returning full markdown plus references.

Signature:

results = client.google_search.ai_mode(
    query="compare next.js and remix for a small saas",
    country="us",        # Optional: ISO 3166-1 alpha-2 country code
    language="en",       # Optional: ISO 639-1 language code
    location="Austin,Texas,United States"  # Optional: canonical name for city-level targeting
)

Returns GoogleSearchAIModeResponse with:

  • query - The search query
  • markdown - Full answer rendered as markdown (the primary signal)
  • text_blocks - List of GoogleSearchAIModeTextBlock for structured rendering Each block has: snippet, type (heading/paragraph/list/expandable/comparison), reference_indexes (point into references[])
  • references - List of GoogleSearchAIModeReference with index, title, link, source, snippet
  • search_metadata - Optional metadata

bing_search

Search the web using Microsoft Bing (web search only).

web() surfaces classic SERP features. In addition to organic results (with sitelinks), the web response exposes — when Bing actually returns them for the query — the answer box (answer_box), People Also Ask (related_questions), and related_searches. Absent surfaces are omitted.

client.bing_search.web()

Search the web using Bing. Returns organic results plus classic SERP surfaces when Bing includes them for the query.

Signature:

results = client.bing_search.web(
    query="best seo tools",
    max_results=10,      # Max results (1-100, default: 10)
    country="us"         # Optional: ISO 3166-1 alpha-2 country code
)

Returns BingSearchWebResponse with:

  • query - The search query
  • results - List of BingSearchResult objects (organic results)
  • total - Organic results count
  • answer_box - BingSearchAnswerBox or None
  • related_questions - List of BingSearchRelatedQuestion (People Also Ask)
  • related_searches - List of BingSearchRelatedSearch
  • search_metadata - Optional metadata

Each organic result has: title, url, snippet, displayed_link, date, sitelinks (list of {title, link, snippet}).

All SERP-feature fields default to empty / None when Bing did not include that surface for the query.

google_maps

Search for places and businesses on Google Maps, and retrieve reviews.

client.google_maps.search()

Search for places and businesses on Google Maps.

Signature:

results = client.google_maps.search(
    query="coffee shops",
    location="New York, NY",   # Optional: text location (mutually exclusive with ll)
    ll="@40.7128,-74.0060,15z", # Optional: GPS coords (mutually exclusive with location)
    type="cafe",               # Optional: place type filter
    language="en",             # Optional: ISO 639-1 language code
    country="us"               # Optional: ISO 3166-1 alpha-2 country code
)

Returns GoogleMapsSearchResponse with:

  • query - The search query
  • results - List of GoogleMapsPlace objects
  • total - Total results count
  • search_metadata - Optional metadata

Each GoogleMapsPlace has:

  • title - Place name
  • place_id - Google place ID
  • data_id - Google data ID (use this for reviews)
  • address - Full address
  • phone - Phone number
  • website - Website URL
  • rating - Average rating (1-5)
  • reviews - Number of reviews
  • type - Place type
  • thumbnail - Thumbnail image URL
  • gps_coordinates - GPS coordinates dict
  • hours - Operating hours
  • price_level - Price level indicator

client.google_maps.reviews()

Get reviews for a Google Maps place.

Signature:

reviews = client.google_maps.reviews(
    data_id="0x89c259a...",    # Preferred: data_id from search result
    place_id="ChIJ...",        # Alternative: Google place_id
    sort_by="most_relevant",   # Optional: most_relevant, newest, highest_rating, lowest_rating
    num=10,                    # Optional: number of reviews (default: 10)
    language="en"              # Optional: ISO 639-1 language code
)

Note: You must provide either data_id or place_id.

Returns GoogleMapsReviewsResponse with:

  • place_id - Google place ID
  • data_id - Google data ID
  • title - Place name
  • address - Full address
  • rating - Average rating
  • total_reviews - Total review count
  • reviews - List of GoogleMapsReview objects
  • search_metadata - Optional metadata

Each GoogleMapsReview has:

  • rating - Review rating (1-5)
  • text - Review text
  • author - Reviewer name
  • date - Review date
  • likes - Number of likes
  • response - Owner response (if any)

google_trends

Explore search interest over time and discover related queries using Google Trends.

client.google_trends.interest_over_time()

Get search interest over time for a query.

Latency: typically 5–20 s. Trends is fetched via SerpAPI's google_trends engine; usually fast but can spike when Google rate limits widget rendering.

Signature:

interest = client.google_trends.interest_over_time(
    query="artificial intelligence",
    geo="US",                  # Optional: region code (empty for worldwide)
    timeframe="today 12-m"     # Optional: time range
)

Timeframe options:

  • 'now 1-H': Past hour
  • 'now 4-H': Past 4 hours
  • 'now 1-d': Past day
  • 'now 7-d': Past 7 days
  • 'today 1-m': Past month
  • 'today 3-m': Past 3 months
  • 'today 12-m': Past year
  • 'today 5-y': Past 5 years
  • 'all': Since 2004
  • '2020-01-01 2020-12-31': Custom date range

Returns GoogleTrendsInterestOverTimeResponse with:

  • query - The search query
  • geo - Geographic region
  • timeframe - Time range used
  • timeline - List of GoogleTrendsTimelinePoint objects
  • average_interest - Average interest score
  • search_metadata - Optional metadata

Each GoogleTrendsTimelinePoint has:

  • date - Date string
  • value - Interest value (0-100, relative)
  • formatted_value - Formatted display value

client.google_trends.related_queries()

Get related queries for a search term.

Latency: typically 5–20 s, same notes as interest_over_time().

Signature:

related = client.google_trends.related_queries(
    query="machine learning",
    geo="US"                   # Optional: region code (empty for worldwide)
)

Returns GoogleTrendsRelatedQueriesResponse with:

  • query - The search query
  • geo - Geographic region
  • top - List of top related queries (by volume)
  • rising - List of rising queries (fastest growing)
  • search_metadata - Optional metadata

Each GoogleTrendsRelatedQuery has:

  • query - The related query text
  • value - Interest value or percentage
  • link - Link to explore the query
  • extracted_value - Extracted numeric value

ai_search

Grounded AI-powered web search using Perplexity, ChatGPT, or Gemini. Ask an answer engine a question and get back a citation-backed answer grounded in a live web search. This is an answer engine, not a SERP reader — it returns no rank/position data (use google_search for that).

IMPORTANT - Usage Restriction: Only use this method when the user has explicitly requested AI-powered search. Do NOT use it proactively. Almost always, what you're trying to achieve can be done using sub-agents instead, which is the preferred approach.

client.ai_search.query()

Perform an AI-powered web search with citations.

IMPORTANT: Only use this method when the user explicitly requests AI-powered search. For most tasks, prefer using sub-agents instead of calling this API directly.

Signature:

result = client.ai_search.query(
    model="sonar",                    # Required: sonar, sonar-pro, gpt-5.4-mini, gemini-3.5-flash, etc.
    user_query="What is SEO?",        # Required: search query
    system_prompt="Be concise.",      # Optional: system prompt
    max_tokens=1000,                  # Optional: max response length
    user_location={"city": "Austin", "country": "US"}  # Optional: localize results (OpenAI & Perplexity only; Gemini ignores)
)

Supported models:

  • Perplexity: sonar, sonar-pro, sonar-reasoning-pro, sonar-deep-research
  • OpenAI: gpt-5.4-mini, gpt-5.4-nano
  • Gemini: gemini-3.5-flash, gemini-3.1-pro-preview

user_location accepts any of: city, region, country, timezone.

Returns AISearchResponse with:

  • id - Response identifier
  • model - Model used
  • content - Generated response text
  • citations - List of AISearchCitation(url, title, text)
  • search_results - List of AISearchResult(title, url, snippet) - Perplexity only
  • usage.input_tokens, usage.output_tokens, usage.total_tokens

gsc

Access your Google Search Console data including search analytics, sitemaps, and URL inspection.

client.gsc.query()

Query search analytics data.

Signature:

result = client.gsc.query(
    start_date="2024-01-01",
    end_date="2024-01-31",
    dimensions=["query", "page"],  # Optional: "query", "page", "country", "device", "searchAppearance", "date"
    search_type="web",             # Optional: "web", "image", "video", "news", "discover", "googleNews"
    row_limit=100,                 # Optional: max rows (default 1000, max 25000)
    start_row=0,                   # Optional: pagination offset
    aggregation_type="auto",       # Optional: "auto", "byPage", "byProperty"
    data_state="final"             # Optional: "final", "all"
)

Returns SearchAnalyticsResponse with:

  • rows - List of SearchAnalyticsRow objects
  • response_aggregation_type - How data was aggregated Each row has: keys (list), clicks, impressions, ctr, position

Pagination Example

The Search Console API returns max 25,000 rows per request. For large datasets:

def get_all_search_analytics(start_date: str, end_date: str, dimensions: list):
    """Fetch all search analytics data with automatic pagination."""
    all_rows = []
    start_row = 0
    row_limit = 25000  # Maximum allowed by the API

    while True:
        result = client.gsc.query(
            start_date=start_date,
            end_date=end_date,
            dimensions=dimensions,
            row_limit=row_limit,
            start_row=start_row,
        )

        all_rows.extend(result.rows)

        # If we got fewer rows than requested, we've reached the end
        if len(result.rows) < row_limit:
            break

        start_row += row_limit

    return all_rows

Note: The API has daily quota limits. For large datasets, consider reducing the date range or using fewer dimensions.

client.gsc.get_site()

Get site information.

Signature:

site = client.gsc.get_site()

Returns SiteInfo with:

  • site_url - The site URL
  • permission_level - Permission level (e.g., "siteOwner")

client.gsc.list_sitemaps()

List all sitemaps.

Signature:

sitemaps = client.gsc.list_sitemaps()

Returns SitemapListResponse with:

  • sitemap - List of Sitemap objects Each sitemap has: path, last_submitted, is_pending, is_sitemaps_index, last_downloaded, warnings, errors, contents

client.gsc.get_sitemap()

Get specific sitemap details.

Signature:

sitemap = client.gsc.get_sitemap("https://example.com/sitemap.xml")

Returns Sitemap with full details.

client.gsc.inspect_url()

Inspect a URL's index status.

Signature:

inspection = client.gsc.inspect_url(
    url="https://example.com/page",
    language_code="en-US"  # Optional: for localized results
)

Returns UrlInspectionResponse. All nested fields are typed attributes (snake_case) — no dictionary access needed. Top-level shape:

  • inspection_result.inspection_result_link — link to the GSC report.
  • inspection_result.index_status_resultIndexStatusResult or None. Fields: verdict, coverage_state, robots_txt_state, indexing_state, last_crawl_time, page_fetch_state, google_canonical, user_canonical, crawled_as, referring_urls (list), sitemap (list).
  • inspection_result.mobile_usability_resultMobileUsabilityResult or None. Fields: verdict, issues (list of MobileUsabilityIssue with issue_type, message, severity).
  • inspection_result.rich_results_resultRichResultsResult or None. Fields: verdict, detected_items (list of DetectedItem with rich_result_type and items, each with name and issues).

reddit

Read Reddit posts, comments, keyword search results, and subreddit feeds. Always available — no integration or Reddit account required.

IMPORTANT — these calls are SLOW. Every method triggers a fresh data-collection job on the provider side and blocks until it finishes: typically 30–120 seconds per call (worst case a few minutes), not milliseconds. Design scripts accordingly:

  • Batch inputs: every method accepts up to 20 URLs / keywords / subreddits in ONE call. One call with 10 URLs is ~10x faster and cheaper than 10 calls with 1 URL. Never call these methods in a loop over items you could batch.
  • Fetch Reddit data once, early in the script, then work on the results.
  • Responses include duration_ms (how long collection took) and error_count (inputs that failed, e.g. deleted posts) — a partial result does not raise.

Field names follow the provider's schema: post body is description, score is num_upvotes, author is user_posted, and each post embeds its subreddit's metadata as community_* fields (name, url, description, members count). Unmodeled fields are still present on the objects.

client.reddit.search()

Search Reddit posts by keyword. SLOW: typically 1-2 minutes.

Signature:

results = client.reddit.search(
    keywords=["ai seo tools", "answer engine optimization"],  # Required: str or list of str (max 20)
    date="Past month",           # Optional: "Past week", "Past month", "Past year", "All time"
    num_of_posts=10,             # Optional: max posts per keyword (1-100)
    wait_seconds=300,            # Optional: server-side job wait (default 300, max 600)
)

Returns RedditPostsResponse with:

  • posts - List of RedditPost objects
  • count - Records collected
  • error_count - Inputs that failed upstream
  • duration_ms - Collection job runtime

Each RedditPost has:

  • post_id, url, title
  • description - Post body text (markdown variant in description_markdown)
  • user_posted - Author username
  • date_posted - ISO-8601 timestamp string
  • num_upvotes - Net score
  • num_comments - Comment count
  • tag - Post flair (may be None)
  • photos / videos / embedded_links - Media attachments
  • comments - Truncated embedded comment sample (full tree via get_post_comments)
  • community_name / community_url / community_description / community_members_num - Subreddit metadata

client.reddit.get_posts()

Fetch Reddit posts (content + engagement stats) by permalink URL. SLOW: typically 30-60 seconds. Batch up to 20 URLs per call.

Signature:

response = client.reddit.get_posts([
    "https://www.reddit.com/r/golang/comments/abc123/some_post/",
    "https://www.reddit.com/r/webdev/comments/def456/other_post/",
])

Returns RedditPostsResponse (same shape as search). Use this to check engagement (num_upvotes, num_comments) on posts you are tracking — a deleted/unreachable post shows up in error_count instead of raising.

client.reddit.get_subreddit_posts()

Fetch recent posts from subreddits. SLOW: typically 1-2 minutes.

Signature:

feed = client.reddit.get_subreddit_posts(
    subreddits=["golang", "r/webdev"],  # Required: names or full URLs (max 20)
    sort_by="New",               # Optional: "Hot", "New", "Top", "Rising"
    num_of_posts=25,             # Optional: max posts per subreddit (1-100)
)

Returns RedditPostsResponse. Subreddit metadata (members count, description) is embedded on each post as community_* fields.

client.reddit.get_post_comments()

Fetch the comments of Reddit posts. SLOW: typically 1-2 minutes.

Signature:

response = client.reddit.get_post_comments(
    urls="https://www.reddit.com/r/golang/comments/abc123/some_post/",  # str or list (max 20)
    days_back=30,                # Optional: only comments from the last N days
    load_all_replies=True,       # Optional: expand nested reply trees (slower)
    comment_limit=100,           # Optional: max comments per post
)

Returns RedditCommentsResponse with:

  • comments - List of RedditComment objects (one per comment)
  • count / error_count / duration_ms

Each RedditComment has:

  • comment_id, url, post_id, post_url
  • comment - Comment body text
  • user_posted - Author username
  • date_posted - ISO-8601 timestamp string
  • num_upvotes, num_replies
  • replies - Nested replies (when load_all_replies=True)
  • parent_comment_id / root_comment_id - Thread position
  • community_name - Subreddit

linkedin

Publish posts to the connected user's personal LinkedIn profile. The user connects their LinkedIn account once from the workspace Integrations page; post() then publishes AS THAT PERSON through LinkedIn's official API.

IMPORTANT — the 60-day token wall. LinkedIn grants this integration a ~60-day access token with NO refresh token. There is no silent renewal: when the token lapses, post() fails until the user clicks through LinkedIn's consent screen again (workspace → Integrations → LinkedIn → Reconnect; one click if they're logged in to LinkedIn). Every response carries token_days_remaining plus a warnings list once expiry is ≤14 days away — ALWAYS relay those warnings to the user so they renew before posting breaks. Call status() at the start of any posting workflow.

Write-only. LinkedIn's API cannot read a member's posts back — engagement (likes, comments, reshares) is invisible to it. list_posts() returns Berlin's own record of what was published (post URNs + URLs).

Posts publicly as a real person. Only publish content the user has asked for or approved; there is no unpublish through this API.

client.linkedin.status()

Check the LinkedIn connection and token-expiry countdown. Call this before posting workflows.

Signature:

status = client.linkedin.status()

Returns LinkedInStatus with:

  • connected - Whether a LinkedIn account is connected for this project
  • member_name - Display name of the connected person
  • person_urn - Their LinkedIn identity (urn:li:person:...)
  • token_issued_at / token_expires_at - Current token lifetime
  • token_days_remaining - Whole days until the token lapses
  • token_expired - True once posting is disabled pending reconnect
  • warnings - Actionable messages to relay to the user (e.g. "token expires in 9 days — reconnect from the Integrations page")

connected=False means no account is connected — ask the user to connect LinkedIn from the workspace Integrations page. This method never raises for a missing integration.

client.linkedin.post()

Publish a post to the connected user's LinkedIn profile.

Signature:

result = client.linkedin.post(
    text="Excited to share...",        # Required: post text (max 3000 chars)
    visibility="PUBLIC",               # Optional: "PUBLIC" (default) or "CONNECTIONS"
    article_url="https://example.com/blog/launch",  # Optional: attach a link share
    article_title="Launch post",       # Optional: link preview title (needs article_url)
    article_description="...",         # Optional: link preview description (needs article_url)
)

Returns LinkedInPostResult with:

  • post_urn - LinkedIn's id for the post; keep it (engagement join key)
  • post_url - Public URL (https://www.linkedin.com/feed/update/{urn}/)
  • posted_at - Publish timestamp
  • author_name - Who the post was published as
  • token_expires_at / token_days_remaining - Token countdown
  • warnings - Non-fatal advisories; when present, RELAY THEM TO THE USER (they are usually the token-expiry countdown)

Raises AgentBerlinNotFoundError when no LinkedIn account is connected, and AgentBerlinAPIError with a reconnect instruction when the token has expired or been revoked — pass that instruction on to the user, only they can re-authorize.

client.linkedin.list_posts()

List posts published through this integration, newest first. This is Berlin's own publish record — LinkedIn offers no engagement read-back.

Signature:

history = client.linkedin.list_posts(limit=20)  # limit optional, default 50

Returns LinkedInPostsList with:

  • posts - List of LinkedInPostRecord (post_urn, post_url, text, visibility, media_category, article_url, posted_at)
  • count - Number of records returned

bing_webmaster

Access Bing Webmaster Tools data including search analytics, page performance, traffic stats, and crawl information.

client.bing_webmaster.query_stats()

Get search query statistics.

Returns search query analytics including impressions, clicks, average position, and CTR for queries that triggered your site.

Signature:

result = client.bing_webmaster.query_stats()

Returns QueryStatsResponse with:

  • rows - List of QueryStatsRow objects

Each QueryStatsRow has:

  • query - The search query
  • impressions - Number of impressions
  • clicks - Number of clicks
  • avg_position - Average position in search results
  • avg_ctr - Average click-through rate
  • date - Date of the data (optional)

client.bing_webmaster.page_stats()

Get page-level statistics.

Returns page-level analytics including impressions, clicks, average position, and CTR for your pages.

Signature:

result = client.bing_webmaster.page_stats()

Returns PageStatsResponse with:

  • rows - List of PageStatsRow objects

Each PageStatsRow has:

  • url - The page URL
  • impressions - Number of impressions
  • clicks - Number of clicks
  • avg_position - Average position in search results
  • avg_ctr - Average click-through rate

client.bing_webmaster.traffic_stats()

Get overall traffic statistics.

Returns aggregate traffic metrics for your site.

Signature:

traffic = client.bing_webmaster.traffic_stats()

Returns TrafficStats with:

  • date - Date of the data (optional)
  • impressions - Total impressions
  • clicks - Total clicks
  • avg_ctr - Average click-through rate
  • avg_imp_rank - Average impression rank
  • avg_click_position - Average click position

client.bing_webmaster.crawl_stats()

Get crawl statistics.

Returns crawl metrics including pages crawled, errors, indexing status, and various crawl issues.

Signature:

crawl = client.bing_webmaster.crawl_stats()

Returns CrawlStats with:

  • date - Date of the data (optional)
  • crawled_pages - Number of pages crawled
  • crawl_errors - Number of crawl errors
  • in_index - Number of pages in the index
  • in_links - Number of inbound links
  • blocked_by_robots_txt - Pages blocked by robots.txt
  • contains_malware - Pages flagged for malware
  • http_code_error - Pages with HTTP errors

client.bing_webmaster.get_site()

Get site information.

Returns the site URL and verification status for the site associated with the current project.

Signature:

site = client.bing_webmaster.get_site()

Returns SiteInfo with:

  • site_url - The site URL
  • is_verified - Whether the site is verified
  • is_in_index - Whether the site is in the Bing index

amplitude

Product analytics from Amplitude including sessions, users, events, funnels, and retention.

client.amplitude.query()

Get core analytics data (sessions and users).

Signature:

analytics = client.amplitude.query(
    start_date="20240101",   # Optional: YYYYMMDD format (default: 30 days ago)
    end_date="20240131"      # Optional: YYYYMMDD format (default: today)
)

Returns AmplitudeQueryResponse with:

  • sessions.total - Total sessions
  • sessions.average_length - Average session length in seconds
  • sessions.per_user - Sessions per user
  • users.active - Active users count
  • users.new - New users count

Example:

analytics = client.amplitude.query()
print(f"Active users: {analytics.users.active}")
print(f"New users: {analytics.users.new}")
print(f"Avg session length: {analytics.sessions.average_length}s")

client.amplitude.list_events()

List all trackable events.

Signature:

events = client.amplitude.list_events()

Returns EventsListResponse with:

  • events - List of AmplitudeEvent objects

Each AmplitudeEvent has:

  • name - Event name
  • totals - Weekly total count

Example:

events = client.amplitude.list_events()
for event in events.events:
    print(f"{event.name}: {event.totals} weekly")

client.amplitude.list_cohorts()

List all behavioral cohorts.

Signature:

cohorts = client.amplitude.list_cohorts()

Returns CohortsListResponse with:

  • cohorts - List of AmplitudeCohort objects

Each AmplitudeCohort has:

  • id - Cohort ID
  • name - Cohort name
  • size - Number of users in cohort
  • description - Cohort description (optional)

Example:

cohorts = client.amplitude.list_cohorts()
for cohort in cohorts.cohorts:
    print(f"{cohort.name}: {cohort.size} users")

client.amplitude.funnel()

Analyze funnel conversion for a sequence of events.

Signature:

funnel = client.amplitude.funnel(
    events=["Sign Up", "Complete Profile", "Make Purchase"],  # Required: min 2 events
    start_date="20240101",   # Optional: YYYYMMDD format
    end_date="20240131"      # Optional: YYYYMMDD format
)

Returns FunnelResponse with:

  • steps - List of FunnelStep objects
  • overall_conversion - Overall funnel conversion rate (percentage)

Each FunnelStep has:

  • event - Event name
  • users_entered - Users who reached this step
  • users_completed - Users who completed this step
  • conversion_rate - Conversion rate (percentage)
  • drop_off_rate - Drop-off rate (percentage)

Example:

funnel = client.amplitude.funnel(
    events=["Sign Up", "Complete Profile", "Make Purchase"]
)
print(f"Overall conversion: {funnel.overall_conversion}%")
for step in funnel.steps:
    print(f"{step.event}: {step.conversion_rate}% conversion")

client.amplitude.retention()

Get retention analysis.

Signature:

retention = client.amplitude.retention(
    start_date="20240101",   # Optional: YYYYMMDD format
    end_date="20240131"      # Optional: YYYYMMDD format
)

Returns RetentionResponse with:

  • cohort_size - Size of the cohort
  • retention - List of RetentionDay objects

Each RetentionDay has:

  • day - Day number (0, 1, 7, 14, 30)
  • retained - Number of users retained
  • retention_rate - Retention rate (percentage)

Example:

retention = client.amplitude.retention()
print(f"Cohort size: {retention.cohort_size}")
for day in retention.retention:
    print(f"Day {day.day}: {day.retention_rate}% retained")

posthog

Run queries against a connected PostHog project. Exposes a single generic query() method that forwards the request body to PostHog's /api/projects/:id/query/ endpoint — any query kind PostHog supports works without backend changes.

client.posthog.query()

Run a query against the connected PostHog project.

Signature:

result = client.posthog.query({
    "kind": "HogQLQuery",                          # Required: query kind
    "query": "SELECT ... FROM events ...",        # Body shape depends on kind
})

Args:

  • query (dict, required): The PostHog query body — the inner object, not wrapped in {"query": ...}. Must include a "kind" field. Common kinds:
    • HogQLQuery — raw SQL-like queries over events (set "query" to the SQL string)
    • TrendsQuery — time-series of events with series, dateRange, interval
    • FunnelsQuery — step-by-step conversion analysis
    • RetentionQuery — cohort retention tables
    • EventsQuery, PathsQuery, ... — see PostHog's docs for the full set

Raises:

  • ValueError: If query is empty or missing a "kind" field.
  • AgentBerlinNotFoundError: If no PostHog integration is connected for the project.
  • AgentBerlinAPIError: If PostHog rejects the query.

Returns PostHogQueryResponse with:

  • result - Raw PostHog response body as a dict. Shape depends on the query kind:
    • HogQLQueryresult typically contains results (rows), columns, types, hogql, is_cached, ...
    • TrendsQueryresult.results is a list of series with data, labels, ...
    • FunnelsQueryresult.results describes step-by-step conversion
    • RetentionQueryresult.results is the retention cohort table

Examples:

# Top pages by pageviews (HogQL)
result = client.posthog.query({
    "kind": "HogQLQuery",
    "query": (
        "SELECT properties.$pathname AS path, count() AS pageviews "
        "FROM events WHERE event = '$pageview' "
        "AND timestamp >= now() - INTERVAL 30 DAY "
        "GROUP BY path ORDER BY pageviews DESC LIMIT 20"
    ),
})
for row in result.result.get("results", []):
    path, pageviews = row[0], row[1]
    print(f"{path}: {pageviews}")

# Daily active users trend (TrendsQuery)
result = client.posthog.query({
    "kind": "TrendsQuery",
    "series": [{"event": "$pageview", "math": "dau"}],
    "dateRange": {"date_from": "-30d"},
})

pagespeed

Google PageSpeed Insights Lighthouse analysis — returns performance score, Core Web Vitals (lab + real-user CrUX data), actionable opportunities with estimated savings, diagnostics, and third-party cost breakdown. System-wide; no user integration required.

client.pagespeed.analyze()

Run a Lighthouse audit on a URL via Google PageSpeed Insights and get back a workflow-friendly summary: performance score, lab + real-user (CrUX) Core Web Vitals, ranked opportunities with estimated LCP/FCP savings, diagnostics, and a third-party resource rollup.

Signature:

result = client.pagespeed.analyze(
    url="https://example.com",
    strategy="mobile",   # Optional: "mobile" (default) or "desktop"
    locale="en-US"       # Optional: BCP 47 locale (default "en-US")
)

Returns PageSpeedAnalyzeResponse with:

  • url - The analyzed URL (echoed from the request)
  • strategy - Form factor used ("mobile" or "desktop")
  • analysis_utc_timestamp - When Google ran the analysis
  • lighthouse_version - Lighthouse version that produced the report
  • performance_score - Overall Lighthouse performance score (0.0–1.0, lab-based). None if not computed.
  • core_web_vitals - PageSpeedCoreWebVitals (lab metrics) with:
    • largest_contentful_paint_ms (LCP)
    • first_contentful_paint_ms (FCP)
    • cumulative_layout_shift (CLS, unitless)
    • total_blocking_time_ms (TBT)
    • speed_index_ms
    • time_to_interactive_ms (TTI)
    • server_response_time_ms (TTFB proxy)
  • field_data - PageSpeedFieldData (real-user CrUX data, None when site lacks traffic) with:
    • url_metrics - CrUX for this exact URL (PageSpeedCruxMetrics)
    • origin_metrics - CrUX aggregated across the origin (PageSpeedCruxMetrics) Each PageSpeedCruxMetrics has overall_category ("FAST" | "AVERAGE" | "SLOW") plus per-metric PageSpeedCruxMetric (percentile + category) for LCP, FCP, CLS, INP, TTFB. Google ranks sites on CrUX data, not lab data — this is the most important signal for SEO.
  • opportunities - List of PageSpeedOpportunity, sorted by estimated LCP savings desc. Each has:
    • id, title, description
    • score (0.0–1.0, lower = worse)
    • display_value (human-readable savings summary)
    • estimated_savings_ms.{lcp, fcp} - predicted improvement if fixed
    • offenders - up to 10 contributing URLs with wasted_bytes / wasted_ms
  • diagnostics - List of PageSpeedDiagnostic for issues without a precise savings number (e.g. "DOM too large: 811 elements"). Each has id, title, numeric_value, numeric_unit.
  • third_parties - List of PageSpeedThirdParty (entity, transfer_size_bytes, main_thread_time_ms), sorted by transfer size desc. Useful for identifying heavy third-party scripts.

Important notes:

  • PSI is synchronous — each call blocks 10–30s typically, longer for heavy sites. The backend enforces a 90s timeout.
  • Quota is 50 analyses/day per org by default (shared Google API key cap of 25,000/day divided across brands). Exceeding returns HTTP 429.
  • Mobile and desktop are separate calls — PSI returns CrUX and lab data only for the form factor you request. Call analyze twice (once per strategy) if you want both (each call consumes one quota unit).
  • If analysis times out, the response is a 504 with a message that the target site is too slow. Retrying won't help — it runs Lighthouse from scratch with the same outcome likely.
  • If Lighthouse itself fails (unreachable target, bad response), the response is a 502 with a Lighthouse error code (e.g. ERRORED_DOCUMENT).
  • Response payload is typically 5–15 KB after trimming (screenshots + internal Lighthouse UI fields are dropped).

Example — top-line performance + CrUX check:

result = client.pagespeed.analyze(url="https://example.com", strategy="mobile")

if result.performance_score is not None:
    print(f"Lab score: {result.performance_score * 100:.0f}/100")

if result.field_data and result.field_data.url_metrics:
    lcp = result.field_data.url_metrics.largest_contentful_paint_ms
    if lcp:
        print(f"Real-user LCP p75: {lcp.percentile:.0f}ms — {lcp.category}")

Example — surface top opportunities for an SEO writeup:

for op in result.opportunities[:5]:
    savings = op.estimated_savings_ms
    print(f"- {op.title}: save {savings.lcp:.0f}ms LCP")
    for offender in op.offenders[:3]:
        print(f"    * {offender.url} (wasted {offender.wasted_bytes:.0f} bytes)")

Example — check third-party weight:

for tp in result.third_parties[:5]:
    kb = tp.transfer_size_bytes / 1024
    print(f"- {tp.entity}: {kb:.0f} KB, {tp.main_thread_time_ms:.0f}ms main-thread")

market

Competitive organic-search intelligence about any target domain (yours or a competitor's): the keywords a domain ranks for, related keyword ideas, a ranking/traffic overview, monthly rank history, bulk traffic estimates, top pages, and tech stack. The outside-world counterpart to pages and keywords, which serve your project's own data from Berlin storage. Paid third-party data, metered per request. System-wide; no user integration required.

client.market.ranked_keywords()

Get the keywords a domain ranks for in Google organic search, each with search volume, CPC, competition, SERP position, the ranking page, and estimated traffic value.

Signature:

result = client.market.ranked_keywords(
    target="stripe.com",          # Required: domain to analyze
    location_code=2840,            # Optional: location code (2840 = USA)
    language_code="en",            # Optional: language code
    limit=100,                     # Optional: max results (default 100, max 1000)
    offset=0,                      # Optional: pagination offset
    order_by=["keyword_data.keyword_info.search_volume,desc"],  # Optional: sort directives
    filters=None,                  # Optional: filter expression (passed through)
)

Returns MarketRankedKeywordsResponse with a keywords list of MarketRankedKeyword (keyword, search_volume, cpc, competition, competition_level, rank, rank_group, url, domain, title, intent, etv).

Example:

result = client.market.ranked_keywords(target="stripe.com", limit=50)
for kw in result.keywords:
    print(f"{kw.keyword}: rank={kw.rank}, volume={kw.search_volume}")

client.market.keyword_ideas()

Expand 1–200 seed keywords into semantically related keyword ideas, each with search volume, CPC, competition, and keyword difficulty.

Signature:

result = client.market.keyword_ideas(
    keywords=["payment processing", "online payments"],  # Required: seed keywords (1-200)
    location_code=2840,            # Optional
    language_code="en",            # Optional
    limit=100,                     # Optional: max results (default 100, max 1000)
    offset=0,                      # Optional
    order_by=["keyword_info.search_volume,desc"],  # Optional
    filters=None,                  # Optional
)

Returns MarketKeywordIdeasResponse with a keywords list of MarketKeywordIdea (keyword, search_volume, cpc, competition, competition_level, difficulty).

client.market.domain_overview()

Get a domain's organic + paid ranking and traffic snapshot.

Signature:

overview = client.market.domain_overview(
    target="stripe.com",           # Required
    location_code=2840,            # Optional
    language_code="en",            # Optional
)

Returns MarketDomainOverview with target, organic, and paid, where organic/paid are MarketTrafficMetrics (count, etv, paid_traffic_cost, is_new/is_up/is_down/is_lost; pos_* buckets are absent on this surface).

client.market.historical_rank_overview()

Get a domain's ranking/traffic metrics as a monthly time series — the momentum signal.

Signature:

result = client.market.historical_rank_overview(
    target="stripe.com",           # Required
    location_code=2840,            # Optional
    language_code="en",            # Optional
    date_from="2025-01-01",        # Optional: YYYY-MM-DD
    date_to="2025-12-31",          # Optional: YYYY-MM-DD
)

Returns MarketHistoricalRankOverviewResponse with a history list of MarketRankHistoryPoint (year, month, organic, paid). On this surface the organic/paid MarketTrafficMetrics include the pos_1 / pos_2_3 / pos_4_10 / pos_11_plus rank buckets.

client.market.bulk_traffic_estimation()

Estimate organic/paid traffic for up to 1,000 domains in a single call — for scoring a whole prospect or competitor list at once.

Signature:

result = client.market.bulk_traffic_estimation(
    targets=["stripe.com", "adyen.com", "checkout.com"],  # Required (1-1000)
    location_code=2840,            # Optional
    language_code="en",            # Optional
    item_types=["organic", "paid"],  # Optional: SERP element types
)

Returns MarketBulkTrafficEstimationResponse with an estimates list of MarketTrafficEstimate (target, organic, paid).

client.market.top_pages()

Get a domain's top pages ranked by organic traffic — what content actually performs.

Signature:

result = client.market.top_pages(
    target="stripe.com",           # Required
    location_code=2840,            # Optional
    language_code="en",            # Optional
    limit=100,                     # Optional: max results (default 100, max 1000)
    offset=0,                      # Optional
    order_by=["metrics.organic.etv,desc"],  # Optional
    filters=None,                  # Optional
)

Returns MarketTopPagesResponse with a pages list of MarketTopPage (page_address, organic, paid).

client.market.technologies()

Detect the technology stack a domain runs on (CMS, analytics, marketing/SEO tooling, frameworks) plus contact/identity signals.

Signature:

stack = client.market.technologies(target="stripe.com")  # Required

Returns MarketTechStack with target, domain_rank, last_visited, country_iso_code, emails, phone_numbers, social_graph_urls, and technologies (category → subcategory → list of detected technologies).

backlinks

Off-page / authority profile of any target domain (yours or a competitor's): the aggregate backlink summary, individual backlinks, referring domains, and link competitors. Paid third-party data, metered per request. System-wide; no user integration required.

client.backlinks.summary()

Get a target's aggregate backlink/authority profile — referring-domain counts, total backlinks, backlink rank, spam score, and link-mix distributions.

Signature:

summary = client.backlinks.summary(
    target="stripe.com",            # Required: domain, subdomain, or page
    include_subdomains=True,        # Optional (default True)
    backlinks_status_type="live",   # Optional: "live" (default) | "lost" | "all"
)

Returns BacklinksSummary (target, rank, backlinks, backlinks_spam_score, referring_domains, referring_main_domains, referring_pages, referring_ips, broken_backlinks, and the open-ended referring_links_types / _attributes / _platform_types / _countries maps).

client.backlinks.list()

List the individual backlinks pointing at a target, each with its source page, anchor, dofollow flag, and rank.

Signature:

result = client.backlinks.list(
    target="stripe.com",            # Required
    mode="as_is",                   # Optional: "as_is" | "one_per_domain" | "one_per_anchor"
    limit=100,                      # Optional: max results (default 100, max 1000)
    offset=0,                       # Optional
    order_by=["rank,desc"],         # Optional: sort directives
    filters=None,                   # Optional: filter expression
    include_subdomains=True,        # Optional (default True)
    backlinks_status_type="live",   # Optional: "live" (default) | "lost" | "all"
)

Returns BacklinksListResponse with a backlinks list of Backlink (domain_from, url_from, domain_to, url_to, anchor, item_type, dofollow, is_broken, rank, page_from_rank, domain_from_rank, tld_from, domain_from_country, first_seen, last_seen).

client.backlinks.referring_domains()

Get the domains linking to a target, one aggregate row per linking domain — the "who links to me" breakdown.

Signature:

result = client.backlinks.referring_domains(
    target="stripe.com",                # Required
    limit=100,                          # Optional (default 100, max 1000)
    offset=0,                           # Optional
    order_by=["rank,desc"],             # Optional
    filters=None,                       # Optional
    include_subdomains=True,            # Optional (default True)
    exclude_internal_backlinks=True,    # Optional (default True)
    backlinks_status_type="live",       # Optional: "live" (default) | "lost" | "all"
)

Returns ReferringDomainsResponse with a referring_domains list of ReferringDomain (domain, rank, backlinks, referring_domains, referring_main_domains, referring_pages, backlinks_spam_score, broken_backlinks, first_seen, lost_date).

client.backlinks.competitors()

Find the domains that share part of a target's backlink profile, ranked by the number of intersecting referring domains — the off-page competitive set.

Signature:

result = client.backlinks.competitors(
    target="stripe.com",                # Required
    limit=100,                          # Optional (default 100, max 1000)
    offset=0,                           # Optional
    order_by=["intersections,desc"],    # Optional
    filters=None,                       # Optional
    main_domain=True,                   # Optional (default True)
    exclude_large_domains=True,         # Optional: omit facebook.com etc. (default True)
    exclude_internal_backlinks=True,    # Optional (default True)
)

Returns BacklinkCompetitorsResponse with a competitors list of BacklinkCompetitor (domain, rank, intersections, backlinks, referring_domains).

ai_mentions

AI-answer-engine visibility (AEO/GEO): how a brand, domain, or keyword is mentioned inside AI answers (ChatGPT & Google AI) — raw mention records, aggregate breakdowns, and the most-mentioned domains/pages. Paid third-party data, metered per request. System-wide; no user integration required. Distinct from ai_search, which asks an answer engine a question; ai_mentions measures who gets mentioned in AI answers.

client.ai_mentions.search()

Track how a brand, domain, or keyword surfaces inside AI answers (ChatGPT & Google AI). Returns the raw mention records: the question asked, the answer, the cited sources, and the AI search volume. Provide at least one keyword or domain (up to 10 targets total).

NOTE: the upstream data supplier gates this API behind a monthly account commitment; calls fail until it is active on the account.

Signature:

result = client.ai_mentions.search(
    keywords=["best payment processor"],  # Provide keywords and/or domains
    domains=["stripe.com"],               # (at least one required)
    platform="google",                    # Optional: "google" (default) | "chat_gpt"
    location_code=2840,                   # Optional
    language_code="en",                   # Optional
    search_filter="include",              # Optional: "include" (default) | "exclude"
    search_scope=["question", "answer"],  # Optional: where to look
    match_type="word_match",              # Optional: "word_match" (default) | "partial_match"
    include_subdomains=False,             # Optional (domain targets)
    filters=None,                         # Optional
    order_by=None,                        # Optional
    limit=100,                            # Optional (default 100, max 1000)
    offset=0,                             # Optional
)

Returns AIMentionsResponse with a mentions list of AIMention (platform, model_name, question, answer, ai_search_volume, first_response_at, last_response_at, sources, brand_entities, fan_out_queries).

client.ai_mentions.summary()

Get the aggregate AI-visibility profile for your keywords/domains — the total mention count plus breakdowns by platform, cited source domain, brand entity, location, and language. Provide at least one keyword or domain (up to 10 targets total).

Signature:

summary = client.ai_mentions.summary(
    domains=["stripe.com"],       # Provide keywords and/or domains
    platform="google",            # Optional: "google" (default) | "chat_gpt"
    location_code=2840,           # Optional
    language_code="en",           # Optional
    internal_list_limit=10,       # Optional: elements per breakdown, 1-20 (default 10)
)

Returns AIMentionsSummary (total_mentions, and the by_platform, by_sources_domain, by_search_results_domain, by_brand_entity, by_location, by_language breakdown lists of AIMentionBreakdown: key, mentions, ai_search_volume).

client.ai_mentions.top_domains()

Get the domains most frequently mentioned in AI answers for your keywords/domains — who gets surfaced instead of (or alongside) you. Provide at least one keyword or domain (up to 10 targets total).

Signature:

result = client.ai_mentions.top_domains(
    keywords=["best crm for startups"],  # Provide keywords and/or domains
    platform="google",                   # Optional: "google" (default) | "chat_gpt"
    location_code=2840,                   # Optional
    language_code="en",                   # Optional
    links_scope="sources",               # Optional: "sources" (default) | "search_results"
    items_list_limit=5,                  # Optional: ranked domains, 1-10 (default 5)
    internal_list_limit=5,               # Optional
)

Returns AIMentionTopDomainsResponse with a domains list of AIMentionTopItem (key, mentions, ai_search_volume).

client.ai_mentions.top_pages()

Get the pages most frequently mentioned in AI answers for your keywords/domains. Arguments mirror top_domains.

Returns AIMentionTopPagesResponse with a pages list of AIMentionTopItem (key, mentions, ai_search_volume).

email_marketing

The project's connected email service provider (ESP), e.g. Klaviyo — audiences, contacts, campaigns, templates, events, and suppressions. Requires an ESP integration on the project. Providers differ widely: call capabilities() first and branch on its flags. The intended campaign workflow is DRAFT-FIRST — create_campaign() creates a draft and returns vendor_edit_url so a human reviews and approves it in the ESP's own UI; prefer that over send_campaign(). Every method accepts an optional provider keyword, only needed when the project has more than one ESP integration connected.

client.email_marketing.capabilities()

Get the feature matrix of the connected ESP. Call this first and branch on the flags instead of assuming a capability exists.

Signature:

caps = client.email_marketing.capabilities(
    provider="klaviyo",  # Optional: only when multiple ESPs are connected
)

Returns EmailCapabilitiesResponse with:

  • provider - The connected ESP, e.g. "klaviyo"
  • capabilities - ESPCapabilities flags, including:
    • supports_lists / supports_segments / supports_tags - Available audience kinds
    • membership_write_kinds - Audience kinds that accept add/remove contacts
    • supports_segment_create / segment_create_manual_only - Segment creation support
    • supports_campaign_create_draft / supports_campaign_update - Campaign writes
    • supports_schedule / schedule_granularity_minutes / supports_send_now / supports_cancel_scheduled - Sending
    • supports_raw_html_content / requires_template / strips_style_tags - Content constraints
    • supports_campaign_metrics / metrics_include_bounces / metrics_include_revenue - Reporting
    • supports_suppression_read / supports_event_ingestion / supports_webhooks - Misc

Raises AgentBerlinNotFoundError if no ESP integration is connected.

Example:

caps = client.email_marketing.capabilities()
if caps.capabilities.supports_raw_html_content:
    print(f"{caps.provider} accepts raw HTML campaigns")

client.email_marketing.list_audiences()

List audiences (lists, segments, and tags).

Signature:

result = client.email_marketing.list_audiences(
    kinds=["list", "segment"],  # Optional: filter by kind ("list" | "segment" | "tag")
    cursor=None,                # Optional: pagination cursor
    page_size=50,               # Optional
    provider="klaviyo",         # Optional: only when multiple ESPs are connected
)

Returns EmailAudiencesListResponse with:

  • provider - The connected ESP
  • audiences - List of EmailAudience objects
  • next_cursor - Cursor for the next page (None when exhausted)

Each EmailAudience has:

  • external_id - Provider-side audience ID
  • name - Audience name
  • kind - "list", "segment", or "tag"
  • member_count - Number of members (-1 when unknown)

Example:

result = client.email_marketing.list_audiences(kinds=["list"])
for a in result.audiences:
    print(f"{a.name} ({a.external_id}): {a.member_count} members")

client.email_marketing.add_to_audience()

Add contacts to an audience. Check capabilities().capabilities.membership_write_kinds — not every audience kind accepts membership writes on every provider.

Signature:

result = client.email_marketing.add_to_audience(
    "list_123",                     # Required: audience external_id
    ["a@example.com"],              # Required: emails to add
    mark_as_subscribed=True,        # Optional: also mark as subscribed (consent)
    provider="klaviyo",             # Optional
)

Returns EmailMarketingActionResponse with:

  • provider - The connected ESP
  • success - Whether the operation succeeded

client.email_marketing.remove_from_audience()

Remove contacts from an audience.

Signature:

result = client.email_marketing.remove_from_audience(
    "list_123",             # Required: audience external_id
    ["a@example.com"],      # Required: emails to remove
    provider="klaviyo",     # Optional
)

Returns EmailMarketingActionResponse with:

  • provider - The connected ESP
  • success - Whether the operation succeeded

client.email_marketing.get_contact()

Look up a contact by email or provider-side ID. At least one identifier is required (raises ValueError otherwise).

Signature:

contact = client.email_marketing.get_contact(
    email="a@example.com",   # Provide email and/or external_id
    external_id=None,        # (at least one required)
    provider="klaviyo",      # Optional
)

Returns EmailContact with:

  • provider - The connected ESP
  • external_id - Provider-side contact ID
  • email - Email address
  • first_name / last_name - Names (optional)
  • status - "subscribed" | "unsubscribed" | "non_subscribed" | "suppressed" | "unknown"
  • custom_fields - Provider-side custom properties (optional)

Raises AgentBerlinNotFoundError if the contact does not exist.

client.email_marketing.upsert_contact()

Create or update a contact by email (the upsert key).

Signature:

contact = client.email_marketing.upsert_contact(
    "a@example.com",                  # Required: email
    first_name="Ada",                 # Optional
    last_name="Lovelace",             # Optional
    custom_fields={"plan": "pro"},    # Optional
    provider="klaviyo",               # Optional
)

Returns EmailContact (same shape as get_contact).

client.email_marketing.create_campaign()

Create a campaign as a DRAFT in the ESP. Nothing is sent — a human reviews and approves the draft in the ESP's own UI via the returned campaign.vendor_edit_url. Prefer this draft + human-approval flow over send_campaign().

Signature:

result = client.email_marketing.create_campaign(
    "July product update",            # Required: subject
    "Acme",                           # Required: from_name
    "news@acme.com",                  # Required: from_email
    ["list_123"],                     # Required: include_audience_ids
    name="2026-07 newsletter",        # Optional: internal campaign name
    preview_text="What's new",        # Optional
    reply_to="support@acme.com",      # Optional
    html="<html>...</html>",          # Optional: raw HTML body
    plain_text=None,                  # Optional
    template_external_id=None,        # Optional: use a template instead of raw HTML
    exclude_audience_ids=["seg_9"],   # Optional
    provider_options=None,            # Optional: provider-native extras
    provider="klaviyo",               # Optional
)

Returns EmailCampaignResponse with:

  • provider - The connected ESP
  • campaign - EmailCampaign object

Each EmailCampaign has:

  • external_id - Provider-side campaign ID
  • name - Campaign name
  • status - "draft" | "scheduled" | "sending" | "sent" | "canceled"
  • content - EmailCampaignContent (subject, preview_text, from_name, from_email, reply_to, html, plain_text, template_external_id)
  • include_audience_ids / exclude_audience_ids - Audience targeting
  • scheduled_at / sent_at - RFC3339 timestamps (optional)
  • vendor_edit_url - Deep link to review/edit the draft in the ESP UI

Raises ValueError if subject, from_name, from_email, or include_audience_ids is empty. Invalid drafts fail with a 422 carrying the provider's validation_errors.

Example:

result = client.email_marketing.create_campaign(
    "July product update", "Acme", "news@acme.com", ["list_123"],
    html="<html>...</html>",
)
print(f"Draft ready for review: {result.campaign.vendor_edit_url}")

client.email_marketing.update_campaign()

Update an existing campaign; only provided fields change. Check capabilities: supports_campaign_update and supports_audience_update_after_create.

Signature:

result = client.email_marketing.update_campaign(
    "cmp_456",                        # Required: campaign external_id
    subject="New subject",            # Optional content fields
    html="<html>...</html>",          # Optional
    include_audience_ids=["list_1"],  # Optional: replacement audiences
    provider="klaviyo",               # Optional
)

Returns EmailCampaignResponse (same shape as create_campaign).

client.email_marketing.get_campaign()

Get a single campaign.

Signature:

result = client.email_marketing.get_campaign(
    "cmp_456",           # Required: campaign external_id
    provider="klaviyo",  # Optional
)

Returns EmailCampaignResponse (same shape as create_campaign).

client.email_marketing.list_campaigns()

List campaigns.

Signature:

result = client.email_marketing.list_campaigns(
    status="draft",      # Optional: "draft" | "scheduled" | "sending" | "sent" | "canceled"
    cursor=None,         # Optional: pagination cursor
    page_size=50,        # Optional
    provider="klaviyo",  # Optional
)

Returns EmailCampaignsListResponse with:

  • provider - The connected ESP
  • campaigns - List of EmailCampaign objects
  • next_cursor - Cursor for the next page (None when exhausted)

client.email_marketing.campaign_metrics()

Get performance metrics for a campaign. Open counts are inflated by Apple Mail privacy protection — prefer clicks as the engagement signal. Klaviyo caps this endpoint at 225 requests/day; cache results instead of polling.

Signature:

metrics = client.email_marketing.campaign_metrics(
    "cmp_456",           # Required: campaign external_id
    provider="klaviyo",  # Optional
)

Returns EmailCampaignMetrics with:

  • provider - The connected ESP
  • recipients / delivered - Delivery counts
  • opens / unique_opens - Opens (inflated by Apple Mail privacy)
  • clicks / unique_clicks - Clicks (the reliable engagement signal)
  • bounces / unsubscribes / spam_complaints - Negative signals
  • revenue - Attributed revenue where supported

Example:

metrics = client.email_marketing.campaign_metrics("cmp_456")
ctr = metrics.unique_clicks / metrics.delivered if metrics.delivered else 0
print(f"CTR: {ctr:.2%}")

client.email_marketing.schedule_campaign()

Schedule a campaign for a future send time. Check capabilities: schedule_granularity_minutes is the smallest increment the provider accepts.

Signature:

result = client.email_marketing.schedule_campaign(
    "cmp_456",                  # Required: campaign external_id
    "2026-08-01T09:00:00Z",     # Required: send_at (RFC3339)
    provider="klaviyo",         # Optional
)

Returns EmailMarketingActionResponse with:

  • provider - The connected ESP
  • success - Whether the operation succeeded

client.email_marketing.send_campaign()

Send a campaign immediately to its audience. WARNING: this sends real email right away — prefer the draft + human-approval flow (create_campaign + review via vendor_edit_url) or schedule_campaign so a human can still intervene.

Signature:

result = client.email_marketing.send_campaign(
    "cmp_456",           # Required: campaign external_id
    provider="klaviyo",  # Optional
)

Returns EmailMarketingActionResponse with:

  • provider - The connected ESP
  • success - Whether the operation succeeded

client.email_marketing.cancel_scheduled_campaign()

Cancel a scheduled campaign, reverting it to a draft.

Signature:

result = client.email_marketing.cancel_scheduled_campaign(
    "cmp_456",           # Required: campaign external_id
    provider="klaviyo",  # Optional
)

Returns EmailMarketingActionResponse with:

  • provider - The connected ESP
  • success - Whether the operation succeeded

client.email_marketing.create_segment()

Create a segment from an explicit email list OR a provider-native definition — exactly one of the two (raises ValueError otherwise). Check capabilities: segment_create_manual_only means only the emails form is accepted.

Signature:

audience = client.email_marketing.create_segment(
    "VIP customers",                       # Required: name
    emails=["a@example.com"],              # Either explicit emails...
    definition=None,                       # ...or a provider-native definition
    provider="klaviyo",                    # Optional
)

Returns EmailAudience for the newly created segment:

  • external_id - Provider-side audience ID
  • name - Segment name
  • kind - "segment"
  • member_count - Number of members (-1 when unknown)

Example:

# Klaviyo-native definition (condition_groups JSON)
audience = client.email_marketing.create_segment(
    "Recent buyers",
    definition={"condition_groups": [...]},
)

client.email_marketing.list_templates()

List email templates.

Signature:

result = client.email_marketing.list_templates(
    cursor=None,         # Optional: pagination cursor
    provider="klaviyo",  # Optional
)

Returns EmailTemplatesListResponse with:

  • provider - The connected ESP
  • templates - List of EmailTemplate objects (external_id, name)
  • next_cursor - Cursor for the next page (None when exhausted)

client.email_marketing.create_template()

Create an email template. Check capabilities: supports_template_create.

Signature:

template = client.email_marketing.create_template(
    "Newsletter base",     # Required: name
    "<html>...</html>",    # Required: html
    provider="klaviyo",    # Optional
)

Returns EmailTemplate with:

  • external_id - Provider-side template ID
  • name - Template name

client.email_marketing.update_template()

Update an email template's HTML (and optionally its name).

Signature:

template = client.email_marketing.update_template(
    "tpl_789",             # Required: template external_id
    "<html>...</html>",    # Required: new html
    name="Newsletter v2",  # Optional
    provider="klaviyo",    # Optional
)

Returns EmailTemplate (same shape as create_template).

client.email_marketing.track_event()

Track a custom event for a contact into the ESP. Check capabilities: supports_event_ingestion.

Signature:

result = client.email_marketing.track_event(
    "a@example.com",                   # Required: contact email
    "Viewed pricing",                  # Required: event name
    properties={"plan": "pro"},        # Optional
    timestamp="2026-07-17T10:00:00Z",  # Optional: RFC3339 (default: now)
    provider="klaviyo",                # Optional
)

Returns EmailMarketingActionResponse with:

  • provider - The connected ESP
  • success - Whether the operation succeeded

client.email_marketing.list_suppressed()

List suppressed contacts (excluded from all sends). Check capabilities: supports_suppression_read.

Signature:

result = client.email_marketing.list_suppressed(
    cursor=None,         # Optional: pagination cursor
    page_size=100,       # Optional
    provider="klaviyo",  # Optional
)

Returns SuppressedListResponse with:

  • provider - The connected ESP
  • contacts - List of SuppressedContact objects
  • next_cursor - Cursor for the next page (None when exhausted)

Each SuppressedContact has:

  • email - Email address
  • reason - "unsubscribed" | "hard_bounce" | "soft_bounce" | "spam_complaint" | "manual" | "unknown"
  • suppressed_at - RFC3339 timestamp (optional)

google_ads

Daily Google Ads performance for the project's connected account: search terms, keywords (with quality score), campaigns, ad groups, and landing pages, plus the search-term coverage honesty metric. Read-only — the SDK never mutates campaigns or any other account state. Data is served from Berlin's warehouse, synced nightly from Google Ads (check meta.data_as_of on every response for freshness; it can be 24h+ stale). All cost values are micros: millionths of one unit of the account currency in meta.currency_code (cost_micros=1500000 with currency_code="USD" is $1.50). Google redacts below-threshold search terms, so pair search_terms() with coverage() for the honest spend denominator.

client.google_ads.search_terms()

Get daily search-term performance — the actual queries ads matched, with the keyword and match type each query triggered.

Google redacts below-threshold search terms (a large share of spend can be hidden), so always pair this with coverage() for the same period to know how much spend the visible terms actually represent.

Signature:

result = client.google_ads.search_terms(
    start_date="2026-06-01",  # Required: YYYY-MM-DD format
    end_date="2026-06-30",    # Required: YYYY-MM-DD format
    limit=500,                # Optional: max rows (default 10000, max 100000)
)

Args:

  • start_date: Start date in YYYY-MM-DD format (required)
  • end_date: End date in YYYY-MM-DD format (required)
  • limit: Maximum rows to return (default 10000, max 100000). The response sets truncated=True when the cap cut rows off.

Returns GoogleAdsSearchTermsResponse with:

  • rows - List of GoogleAdsSearchTermRow objects
  • row_count - Number of rows returned
  • truncated - True when the row limit cut rows off
  • meta - GoogleAdsMeta (source, customer_id, currency_code, data_as_of, backfill_complete, integration_error)

Each GoogleAdsSearchTermRow has:

  • date - Day (YYYY-MM-DD)
  • campaign_id / campaign_name / ad_group_id / ad_group_name
  • search_term - The actual user query
  • status / match_type / keyword_text - The keyword the query matched
  • impressions / clicks / cost_micros / conversions / conversions_value

Example:

result = client.google_ads.search_terms(
    start_date="2026-06-01",
    end_date="2026-06-30",
)
cov = client.google_ads.coverage(
    start_date="2026-06-01",
    end_date="2026-06-30",
)
print(f"Visible terms cover {cov.coverage.coverage_ratio:.0%} of spend")
for row in result.rows:
    cost = row.cost_micros / 1_000_000
    print(f"{row.search_term}: {row.clicks} clicks, {cost:.2f} {result.meta.currency_code}")

client.google_ads.keywords()

Get daily bid-keyword performance with quality score and impression share.

Quality-score fields (quality_score, creative_quality_score, post_click_quality_score, search_predicted_ctr) are None for dates before the integration was connected: Google exposes no quality-score history, so it only accrues from the connect date via the nightly sync. Metrics columns are genuinely historical everywhere.

Signature:

result = client.google_ads.keywords(
    start_date="2026-06-01",  # Required: YYYY-MM-DD format
    end_date="2026-06-30",    # Required: YYYY-MM-DD format
    limit=500,                # Optional: max rows (default 10000, max 100000)
)

Args:

  • start_date: Start date in YYYY-MM-DD format (required)
  • end_date: End date in YYYY-MM-DD format (required)
  • limit: Maximum rows to return (default 10000, max 100000). The response sets truncated=True when the cap cut rows off.

Returns GoogleAdsKeywordsResponse with:

  • rows - List of GoogleAdsKeywordRow objects
  • row_count - Number of rows returned
  • truncated - True when the row limit cut rows off
  • meta - GoogleAdsMeta (source, customer_id, currency_code, data_as_of, backfill_complete, integration_error)

Each GoogleAdsKeywordRow has:

  • date - Day (YYYY-MM-DD)
  • campaign_id / ad_group_id / criterion_id
  • keyword_text / match_type / status
  • quality_score / creative_quality_score / post_click_quality_score / search_predicted_ctr - None for dates before the integration connected
  • impressions / clicks / cost_micros / conversions / conversions_value
  • search_impression_share / search_rank_lost_impression_share - None when Google reports no value

Example:

result = client.google_ads.keywords(
    start_date="2026-06-01",
    end_date="2026-06-30",
)
for row in result.rows:
    if row.quality_score is not None and row.quality_score < 5:
        print(f"Low QS {row.quality_score}: {row.keyword_text}")

client.google_ads.campaigns()

Get daily campaign rollups including budget, channel type, bidding strategy, and impression-share metrics.

Signature:

result = client.google_ads.campaigns(
    start_date="2026-06-01",  # Required: YYYY-MM-DD format
    end_date="2026-06-30",    # Required: YYYY-MM-DD format
    limit=500,                # Optional: max rows (default 10000, max 100000)
)

Args:

  • start_date: Start date in YYYY-MM-DD format (required)
  • end_date: End date in YYYY-MM-DD format (required)
  • limit: Maximum rows to return (default 10000, max 100000). The response sets truncated=True when the cap cut rows off.

Returns GoogleAdsCampaignsResponse with:

  • rows - List of GoogleAdsCampaignRow objects
  • row_count - Number of rows returned
  • truncated - True when the row limit cut rows off
  • meta - GoogleAdsMeta (source, customer_id, currency_code, data_as_of, backfill_complete, integration_error)

Each GoogleAdsCampaignRow has:

  • date - Day (YYYY-MM-DD)
  • campaign_id / campaign_name / status
  • advertising_channel_type / bidding_strategy_type
  • budget_amount_micros - Daily budget in micros
  • impressions / clicks / cost_micros / conversions / conversions_value
  • search_impression_share / search_budget_lost_impression_share / search_rank_lost_impression_share - None when Google reports no value

Example:

result = client.google_ads.campaigns(
    start_date="2026-06-01",
    end_date="2026-06-30",
)
for row in result.rows:
    spend = row.cost_micros / 1_000_000
    print(f"{row.date} {row.campaign_name}: {spend:.2f} {result.meta.currency_code}")

client.google_ads.ad_groups()

Get daily ad-group rollups. Unlike search terms, these totals are NOT redacted — they are the denominator behind the coverage() metric.

Signature:

result = client.google_ads.ad_groups(
    start_date="2026-06-01",  # Required: YYYY-MM-DD format
    end_date="2026-06-30",    # Required: YYYY-MM-DD format
    limit=500,                # Optional: max rows (default 10000, max 100000)
)

Args:

  • start_date: Start date in YYYY-MM-DD format (required)
  • end_date: End date in YYYY-MM-DD format (required)
  • limit: Maximum rows to return (default 10000, max 100000). The response sets truncated=True when the cap cut rows off.

Returns GoogleAdsAdGroupsResponse with:

  • rows - List of GoogleAdsAdGroupRow objects
  • row_count - Number of rows returned
  • truncated - True when the row limit cut rows off
  • meta - GoogleAdsMeta (source, customer_id, currency_code, data_as_of, backfill_complete, integration_error)

Each GoogleAdsAdGroupRow has:

  • date - Day (YYYY-MM-DD)
  • campaign_id / ad_group_id / ad_group_name / status
  • impressions / clicks / cost_micros / conversions / conversions_value
  • search_impression_share / search_rank_lost_impression_share - None when Google reports no value

Example:

result = client.google_ads.ad_groups(
    start_date="2026-06-01",
    end_date="2026-06-30",
)
for row in result.rows:
    print(f"{row.ad_group_name}: {row.impressions} impressions")

client.google_ads.landing_pages()

Get daily landing-page performance keyed by real (expanded) final URLs, joinable against the crawled page index (client.pages).

Signature:

result = client.google_ads.landing_pages(
    start_date="2026-06-01",  # Required: YYYY-MM-DD format
    end_date="2026-06-30",    # Required: YYYY-MM-DD format
    limit=500,                # Optional: max rows (default 10000, max 100000)
)

Args:

  • start_date: Start date in YYYY-MM-DD format (required)
  • end_date: End date in YYYY-MM-DD format (required)
  • limit: Maximum rows to return (default 10000, max 100000). The response sets truncated=True when the cap cut rows off.

Returns GoogleAdsLandingPagesResponse with:

  • rows - List of GoogleAdsLandingPageRow objects
  • row_count - Number of rows returned
  • truncated - True when the row limit cut rows off
  • meta - GoogleAdsMeta (source, customer_id, currency_code, data_as_of, backfill_complete, integration_error)

Each GoogleAdsLandingPageRow has:

  • date - Day (YYYY-MM-DD)
  • expanded_final_url - The real final URL ads sent traffic to
  • impressions / clicks / cost_micros / conversions

Example:

result = client.google_ads.landing_pages(
    start_date="2026-06-01",
    end_date="2026-06-30",
)
for row in result.rows:
    print(f"{row.expanded_final_url}: {row.clicks} clicks")

client.google_ads.coverage()

Get the search-term visibility ratio for a period: how much of the account's total spend the (redacted) search-term report actually covers. Google hides below-threshold terms, so visible search terms can represent anywhere from a small fraction to nearly all of spend — surface this next to any search_terms() analysis so conclusions are honest about their denominator.

Signature:

cov = client.google_ads.coverage(
    start_date="2026-06-01",  # Required: YYYY-MM-DD format
    end_date="2026-06-30",    # Required: YYYY-MM-DD format
)

Args:

  • start_date: Start date in YYYY-MM-DD format (required)
  • end_date: End date in YYYY-MM-DD format (required)

Returns GoogleAdsCoverageResponse with:

  • coverage - GoogleAdsCoverage object
  • meta - GoogleAdsMeta (source, customer_id, currency_code, data_as_of, backfill_complete, integration_error)

The GoogleAdsCoverage object has:

  • visible_cost_micros - Spend attributed to visible search terms
  • total_cost_micros - Total spend (from unredacted ad-group rollups)
  • coverage_ratio - visible/total in [0, 1]; 0 when there is no spend

Example:

cov = client.google_ads.coverage(
    start_date="2026-06-01",
    end_date="2026-06-30",
)
print(f"Search terms cover {cov.coverage.coverage_ratio:.0%} of spend")

openai_ads

Performance for the project's connected OpenAI (ChatGPT) Ads account — ads shown inside ChatGPT. Read-only — the SDK never mutates campaigns or any account state. Data is read LIVE from the customer's Advertiser API key on each call (there is no warehouse; the OpenAI insights API is synchronous REST with 5-year retention), so there is no data_as_of staleness. OpenAI Ads has NO keywords and NO search terms — reporting exposes only six metrics: impressions, clicks, spend, ctr, cpc, cpm. Insights money (spend/cpc/cpm) is in the account currency in meta.currency_code, NOT micros (campaign budget limits ARE micros). The insight methods mirror the platform's aggregation levels — account(), campaigns(), ad_groups(), ads() — and list_campaigns() returns campaign metadata (names, statuses, budgets, bidding type).

client.openai_ads.account()

Get account-level performance totals (one row per time bucket), read live from the connected Advertiser API key.

Signature:

totals = client.openai_ads.account(
    start_date="2026-07-01",  # Optional: YYYY-MM-DD (default: 30 days ago)
    end_date="2026-07-31",    # Optional: YYYY-MM-DD (default: today)
    granularity="daily",      # Optional: daily | hourly | monthly | none
    segment="country",        # Optional: product | country | device
)

Args:

  • start_date: Start date in YYYY-MM-DD format (optional; defaults to 30 days ago)
  • end_date: End date in YYYY-MM-DD format (optional; defaults to today)
  • granularity: Time bucket — daily (default), hourly, monthly, or none
  • segment: Optional single breakdown — product, country, or device

Returns OpenAIAdsInsightsResponse with:

  • rows - List of OpenAIAdsInsightRow objects
  • row_count - Number of rows returned
  • meta - OpenAIAdsMeta (source, account_id, currency_code, timezone)

Each OpenAIAdsInsightRow has:

  • entity_id / entity_name - The entity at the report level (empty at account level)
  • status / review_status - Entity states where applicable
  • start_time / end_time / readable_time - The time bucket
  • segment / segment_value - Present when a segment was requested
  • impressions / clicks / spend / ctr / cpc / cpm - Money in meta.currency_code (not micros)

Example:

totals = client.openai_ads.account(start_date="2026-07-01", end_date="2026-07-31")
for row in totals.rows:
    print(f"{row.readable_time}: {row.spend:.2f} {totals.meta.currency_code}")

client.openai_ads.campaigns()

Get per-campaign performance (one row per campaign per time bucket), read live. Use list_campaigns() for budgets and bidding type.

Signature:

report = client.openai_ads.campaigns(
    start_date="2026-07-01",  # Optional: YYYY-MM-DD (default: 30 days ago)
    end_date="2026-07-31",    # Optional: YYYY-MM-DD (default: today)
    granularity="daily",      # Optional: daily | hourly | monthly | none
    segment="device",         # Optional: product | country | device
)

Args:

  • start_date / end_date: YYYY-MM-DD (optional; default last 30 days)
  • granularity: daily (default), hourly, monthly, or none
  • segment: Optional single breakdown — product, country, or device

Returns OpenAIAdsInsightsResponse (rows, row_count, meta). Each row's entity_id/entity_name identify the campaign; money is in meta.currency_code, not micros.

Example:

report = client.openai_ads.campaigns(start_date="2026-07-01", end_date="2026-07-31")
for row in report.rows:
    print(f"{row.entity_name}: {row.clicks} clicks, {row.spend:.2f} {report.meta.currency_code}")

client.openai_ads.ad_groups()

Get per-ad-group performance (one row per ad group per time bucket), read live.

Signature:

report = client.openai_ads.ad_groups(
    start_date="2026-07-01",  # Optional: YYYY-MM-DD (default: 30 days ago)
    end_date="2026-07-31",    # Optional: YYYY-MM-DD (default: today)
    granularity="daily",      # Optional: daily | hourly | monthly | none
    segment=None,             # Optional: product | country | device
)

Args:

  • start_date / end_date: YYYY-MM-DD (optional; default last 30 days)
  • granularity: daily (default), hourly, monthly, or none
  • segment: Optional single breakdown — product, country, or device

Returns OpenAIAdsInsightsResponse (rows, row_count, meta).

Example:

for row in client.openai_ads.ad_groups().rows:
    print(f"{row.entity_name}: {row.impressions} impressions")

client.openai_ads.ads()

Get per-ad performance (one row per ad per time bucket), read live. Each row includes the ad's review_status — where an OAI-AdsBot crawl rejection surfaces per ad.

Signature:

report = client.openai_ads.ads(
    start_date="2026-07-01",  # Optional: YYYY-MM-DD (default: 30 days ago)
    end_date="2026-07-31",    # Optional: YYYY-MM-DD (default: today)
    granularity="daily",      # Optional: daily | hourly | monthly | none
    segment=None,             # Optional: product | country | device
)

Args:

  • start_date / end_date: YYYY-MM-DD (optional; default last 30 days)
  • granularity: daily (default), hourly, monthly, or none
  • segment: Optional single breakdown — product, country, or device

Returns OpenAIAdsInsightsResponse (rows, row_count, meta).

Example:

for row in client.openai_ads.ads().rows:
    print(f"{row.entity_name} [{row.review_status}]: {row.clicks} clicks")

client.openai_ads.list_campaigns()

List the connected account's campaigns with their metadata (names, statuses, bidding type, budgets) — the context for interpreting the insight rows. Read live.

Signature:

campaigns = client.openai_ads.list_campaigns()

Args: none (uses the project's connected OpenAI Ads account).

Returns OpenAIAdsCampaignsResponse with:

  • rows - List of OpenAIAdsCampaignRow objects
  • row_count - Number of campaigns returned
  • meta - OpenAIAdsMeta (source, account_id, currency_code, timezone)

Each OpenAIAdsCampaignRow has:

  • id / name / description / status
  • bidding_type - "impressions" (CPM) or "clicks" (CPC); immutable
  • mode - "product_feed" for feed campaigns
  • budget - OpenAIAdsBudget (lifetime_spend_limit_micros or daily_spend_limit_micros; these ARE micros)
  • start_time / end_time / created_at / updated_at - unix seconds

Example:

for c in client.openai_ads.list_campaigns().rows:
    print(f"{c.name} [{c.status}] bidding={c.bidding_type}")

client.openai_ads.record_hint_set()

Record a generated set of ad-group context_hints (and the topics they came from) for history. OpenAI keeps NO history of past context_hints and its insights API never reports them, so this is the only record of which hints an ad group carried — persist each set you generate so you can later attribute ad-group performance to it. This writes to Berlin only; it does NOT apply the hints to the ad group (applying is a product-mediated action outside the SDK).

Signature:

hs = client.openai_ads.record_hint_set(
    hints=["trail running shoes", "gore-tex boots"],  # Required, non-empty
    source_topics=["trail-running", "waterproof-footwear"],  # Optional: provenance
    ad_group_id="adgrp_301",  # Optional: set once applied
    account_id="adacct_123",  # Optional: defaults to the connected account
    label="Trail Q3",         # Optional
)

Args:

  • hints: The context_hints (free-text topic phrases). Required, non-empty.
  • source_topics: Tracked topics the hints were derived from (provenance).
  • ad_group_id: The ad group the hints were applied to, if any.
  • account_id: Connected ad account id (defaults to the project's account).
  • label: Optional human label.

Returns the stored OpenAIAdsHintSet (id, project_domain, account_id, ad_group_id, label, hints, source_topics, created_at).

Example:

hs = client.openai_ads.record_hint_set(
    hints=["trail running shoes"],
    source_topics=["trail-running"],
    ad_group_id="adgrp_301",
)
print(hs.id, hs.created_at)

client.openai_ads.list_hint_sets()

List recorded hint sets for the project, newest first. Join these against ad_groups() insights to see which hint sets performed.

Signature:

result = client.openai_ads.list_hint_sets(
    ad_group_id="adgrp_301",  # Optional: filter to one ad group
    limit=20,                 # Optional
)

Args:

  • ad_group_id: Restrict to hint sets applied to one ad group.
  • limit: Maximum records to return (default: all).

Returns OpenAIAdsHintSetsResponse with hint_sets (List[OpenAIAdsHintSet]) and count.

Example:

for hs in client.openai_ads.list_hint_sets().hint_sets:
    print(hs.label, hs.hints)

microsoft_ads

Daily Microsoft Advertising performance for the project's connected account: search terms, keywords (with quality score), campaigns, ad groups, landing pages, and keyword share of voice, plus the search-term coverage honesty metric. Read-only — the SDK never mutates campaigns or any other account state. Data is served from Berlin's warehouse, synced nightly from Microsoft Ads (check meta.data_as_of on every response for freshness; it can be 24h+ stale). Unlike Google Ads, all money values are DECIMAL in the account currency in meta.currency_code (spend=1.50 with currency_code="USD" is $1.50) — not micros, so no division is needed; all IDs are strings. Microsoft redacts below-threshold search terms, so pair search_terms() with coverage() for the honest spend denominator. Share of voice keeps only ~6 months of history.

client.microsoft_ads.search_terms()

Get daily search-term performance — the actual queries ads matched, with the keyword and match type each query triggered.

Microsoft redacts below-threshold search terms (a large share of spend can be hidden), so always pair this with coverage() for the same period to know how much spend the visible terms actually represent.

Signature:

result = client.microsoft_ads.search_terms(
    start_date="2026-06-01",  # Required: YYYY-MM-DD format
    end_date="2026-06-30",    # Required: YYYY-MM-DD format
    limit=500,                # Optional: max rows (default 10000, max 100000)
)

Args:

  • start_date: Start date in YYYY-MM-DD format (required)
  • end_date: End date in YYYY-MM-DD format (required)
  • limit: Maximum rows to return (default 10000, max 100000). The response sets truncated=True when the cap cut rows off.

Returns MicrosoftAdsSearchTermsResponse with:

  • rows - List of MicrosoftAdsSearchTermRow objects
  • row_count - Number of rows returned
  • truncated - True when the row limit cut rows off
  • meta - MicrosoftAdsMeta (source, account_id, currency_code, data_as_of, backfill_complete, integration_error)

Each MicrosoftAdsSearchTermRow has:

  • date - Day (YYYY-MM-DD)
  • campaign_id / campaign_name / ad_group_id / ad_group_name
  • search_term - The actual user query
  • keyword_id / keyword_text / match_type - The keyword the query matched
  • currency_code
  • impressions / clicks / spend / conversions / revenue - spend and revenue are decimal amounts in the account currency (not micros)

Example:

result = client.microsoft_ads.search_terms(
    start_date="2026-06-01",
    end_date="2026-06-30",
)
cov = client.microsoft_ads.coverage(
    start_date="2026-06-01",
    end_date="2026-06-30",
)
print(f"Visible terms cover {cov.coverage.coverage_ratio:.0%} of spend")
for row in result.rows:
    print(f"{row.search_term}: {row.clicks} clicks, {row.spend:.2f} {result.meta.currency_code}")

client.microsoft_ads.keywords()

Get daily bid-keyword performance with quality score and impression share.

Unlike Google Ads, the quality columns (quality_score, expected_ctr, ad_relevance, landing_page_experience) are genuine per-day history: Microsoft populates them for backfilled months too, not just from the connect date. Metrics columns are historical everywhere.

Signature:

result = client.microsoft_ads.keywords(
    start_date="2026-06-01",  # Required: YYYY-MM-DD format
    end_date="2026-06-30",    # Required: YYYY-MM-DD format
    limit=500,                # Optional: max rows (default 10000, max 100000)
)

Args:

  • start_date: Start date in YYYY-MM-DD format (required)
  • end_date: End date in YYYY-MM-DD format (required)
  • limit: Maximum rows to return (default 10000, max 100000). The response sets truncated=True when the cap cut rows off.

Returns MicrosoftAdsKeywordsResponse with:

  • rows - List of MicrosoftAdsKeywordRow objects
  • row_count - Number of rows returned
  • truncated - True when the row limit cut rows off
  • meta - MicrosoftAdsMeta (source, account_id, currency_code, data_as_of, backfill_complete, integration_error)

Each MicrosoftAdsKeywordRow has:

  • date - Day (YYYY-MM-DD)
  • campaign_id / ad_group_id / keyword_id
  • keyword_text / status / currency_code
  • quality_score / expected_ctr / ad_relevance / landing_page_experience
    • genuine per-day history (populated for backfilled months too)
  • impressions / clicks / spend / conversions / revenue - decimal, not micros
  • impression_share / impression_lost_to_rank_share / impression_lost_to_budget_share - None when Microsoft reports no value

Example:

result = client.microsoft_ads.keywords(
    start_date="2026-06-01",
    end_date="2026-06-30",
)
for row in result.rows:
    if row.quality_score is not None and row.quality_score < 5:
        print(f"Low QS {row.quality_score}: {row.keyword_text}")

client.microsoft_ads.campaigns()

Get daily campaign rollups including campaign type and impression-share metrics.

Signature:

result = client.microsoft_ads.campaigns(
    start_date="2026-06-01",  # Required: YYYY-MM-DD format
    end_date="2026-06-30",    # Required: YYYY-MM-DD format
    limit=500,                # Optional: max rows (default 10000, max 100000)
)

Args:

  • start_date: Start date in YYYY-MM-DD format (required)
  • end_date: End date in YYYY-MM-DD format (required)
  • limit: Maximum rows to return (default 10000, max 100000). The response sets truncated=True when the cap cut rows off.

Returns MicrosoftAdsCampaignsResponse with:

  • rows - List of MicrosoftAdsCampaignRow objects
  • row_count - Number of rows returned
  • truncated - True when the row limit cut rows off
  • meta - MicrosoftAdsMeta (source, account_id, currency_code, data_as_of, backfill_complete, integration_error)

Each MicrosoftAdsCampaignRow has:

  • date - Day (YYYY-MM-DD)
  • campaign_id / campaign_name / status / campaign_type / currency_code
  • impressions / clicks / spend / conversions / revenue - decimal, not micros
  • impression_share / impression_lost_to_budget_share / impression_lost_to_rank_share - None when Microsoft reports no value

Example:

result = client.microsoft_ads.campaigns(
    start_date="2026-06-01",
    end_date="2026-06-30",
)
for row in result.rows:
    print(f"{row.date} {row.campaign_name}: {row.spend:.2f} {result.meta.currency_code}")

client.microsoft_ads.ad_groups()

Get daily ad-group rollups. Unlike search terms, these totals are NOT redacted — they are the denominator behind the coverage() metric.

Signature:

result = client.microsoft_ads.ad_groups(
    start_date="2026-06-01",  # Required: YYYY-MM-DD format
    end_date="2026-06-30",    # Required: YYYY-MM-DD format
    limit=500,                # Optional: max rows (default 10000, max 100000)
)

Args:

  • start_date: Start date in YYYY-MM-DD format (required)
  • end_date: End date in YYYY-MM-DD format (required)
  • limit: Maximum rows to return (default 10000, max 100000). The response sets truncated=True when the cap cut rows off.

Returns MicrosoftAdsAdGroupsResponse with:

  • rows - List of MicrosoftAdsAdGroupRow objects
  • row_count - Number of rows returned
  • truncated - True when the row limit cut rows off
  • meta - MicrosoftAdsMeta (source, account_id, currency_code, data_as_of, backfill_complete, integration_error)

Each MicrosoftAdsAdGroupRow has:

  • date - Day (YYYY-MM-DD)
  • campaign_id / ad_group_id / ad_group_name / status / currency_code
  • impressions / clicks / spend / conversions / revenue - decimal, not micros
  • impression_share / impression_lost_to_rank_share / impression_lost_to_budget_share - None when Microsoft reports no value

Example:

result = client.microsoft_ads.ad_groups(
    start_date="2026-06-01",
    end_date="2026-06-30",
)
for row in result.rows:
    print(f"{row.ad_group_name}: {row.impressions} impressions")

client.microsoft_ads.landing_pages()

Get daily landing-page performance keyed by final URLs, joinable against the crawled page index (client.pages).

Signature:

result = client.microsoft_ads.landing_pages(
    start_date="2026-06-01",  # Required: YYYY-MM-DD format
    end_date="2026-06-30",    # Required: YYYY-MM-DD format
    limit=500,                # Optional: max rows (default 10000, max 100000)
)

Args:

  • start_date: Start date in YYYY-MM-DD format (required)
  • end_date: End date in YYYY-MM-DD format (required)
  • limit: Maximum rows to return (default 10000, max 100000). The response sets truncated=True when the cap cut rows off.

Returns MicrosoftAdsLandingPagesResponse with:

  • rows - List of MicrosoftAdsLandingPageRow objects
  • row_count - Number of rows returned
  • truncated - True when the row limit cut rows off
  • meta - MicrosoftAdsMeta (source, account_id, currency_code, data_as_of, backfill_complete, integration_error)

Each MicrosoftAdsLandingPageRow has:

  • date - Day (YYYY-MM-DD)
  • final_url - The final URL ads sent traffic to
  • currency_code
  • impressions / clicks / spend / conversions - spend is decimal, not micros

Example:

result = client.microsoft_ads.landing_pages(
    start_date="2026-06-01",
    end_date="2026-06-30",
)
for row in result.rows:
    print(f"{row.final_url}: {row.clicks} clicks")

client.microsoft_ads.share_of_voice()

Get daily keyword share-of-voice (auction competitiveness): impression and click share, plus how much share was lost to rank or budget.

Microsoft retains only ~6 months of share-of-voice history, so a start_date further back than that returns no rows for the older dates even when the rest of the account has deeper history.

Signature:

result = client.microsoft_ads.share_of_voice(
    start_date="2026-06-01",  # Required: YYYY-MM-DD format
    end_date="2026-06-30",    # Required: YYYY-MM-DD format
    limit=500,                # Optional: max rows (default 10000, max 100000)
)

Args:

  • start_date: Start date in YYYY-MM-DD format (required)
  • end_date: End date in YYYY-MM-DD format (required)
  • limit: Maximum rows to return (default 10000, max 100000). The response sets truncated=True when the cap cut rows off.

Returns MicrosoftAdsShareOfVoiceResponse with:

  • rows - List of MicrosoftAdsShareOfVoiceRow objects
  • row_count - Number of rows returned
  • truncated - True when the row limit cut rows off
  • meta - MicrosoftAdsMeta (source, account_id, currency_code, data_as_of, backfill_complete, integration_error)

Each MicrosoftAdsShareOfVoiceRow has:

  • date - Day (YYYY-MM-DD)
  • campaign_id / ad_group_id / keyword_id / keyword_text / match_type
  • impression_share / click_share / exact_match_impression_share / impression_lost_to_rank_share / impression_lost_to_budget_share / absolute_top_impression_share / top_impression_share - None when Microsoft reports no value

Example:

result = client.microsoft_ads.share_of_voice(
    start_date="2026-06-01",
    end_date="2026-06-30",
)
for row in result.rows:
    if row.impression_share is not None and row.impression_share < 0.5:
        print(f"Losing impressions: {row.keyword_text}")

client.microsoft_ads.coverage()

Get the search-term visibility ratio for a period: how much of the account's total spend the (redacted) search-term report actually covers. Microsoft hides below-threshold terms, so visible search terms can represent anywhere from a small fraction to nearly all of spend — surface this next to any search_terms() analysis so conclusions are honest about their denominator.

Signature:

cov = client.microsoft_ads.coverage(
    start_date="2026-06-01",  # Required: YYYY-MM-DD format
    end_date="2026-06-30",    # Required: YYYY-MM-DD format
)

Args:

  • start_date: Start date in YYYY-MM-DD format (required)
  • end_date: End date in YYYY-MM-DD format (required)

Returns MicrosoftAdsCoverageResponse with:

  • coverage - MicrosoftAdsCoverage object
  • meta - MicrosoftAdsMeta (source, account_id, currency_code, data_as_of, backfill_complete, integration_error)

The MicrosoftAdsCoverage object has:

  • visible_spend - Spend attributed to visible search terms (decimal)
  • total_spend - Total spend (from unredacted ad-group rollups, decimal)
  • coverage_ratio - visible/total in [0, 1]; 0 when there is no spend

Example:

cov = client.microsoft_ads.coverage(
    start_date="2026-06-01",
    end_date="2026-06-30",
)
print(f"Search terms cover {cov.coverage.coverage_ratio:.0%} of spend")

linkedin_ads

LinkedIn Ads (paid social) performance for the project's connected ad account: account, campaign-group, campaign, and creative insights, plus a campaign listing. Read-only — the SDK never mutates campaigns or any other account state. Data is read LIVE via LinkedIn's /adAnalytics finder on every call (no warehouse, so no data_as_of staleness). This is paid SOCIAL, not paid search: there are NO keywords and NO search terms. Money (cost_in_local_currency) is a DECIMAL in the account currency in meta.currency_code — not micros, so no division is needed; all IDs are URNs kept as strings.

client.linkedin_ads.account()

Get account-level performance totals (one row per time bucket).

Signature:

result = client.linkedin_ads.account(
    start_date="2026-06-01",  # Optional: YYYY-MM-DD (default 30 days ago)
    end_date="2026-06-30",    # Optional: YYYY-MM-DD (default today)
    granularity="daily",      # Optional: daily (default) | monthly | yearly | all
)

Returns LinkedInAdsInsightsResponse with:

  • rows - List of LinkedInAdsInsightRow objects
  • row_count - Number of rows returned
  • meta - LinkedInAdsMeta (source, account_id, currency_code)

Each LinkedInAdsInsightRow has:

  • pivot_value - URN of the pivoted entity (empty at account level)
  • start / end - Time-bucket bounds (YYYY-MM-DD)
  • impressions / clicks
  • cost_in_local_currency / cost_in_usd - decimal, not micros
  • conversions / one_click_leads / likes / comments / shares / follows

client.linkedin_ads.campaign_groups()

Per-campaign-group performance (one row per group per time bucket). Same args and shape as account(); each row's pivot_value is the campaign group URN.

client.linkedin_ads.campaigns()

Per-campaign performance (one row per campaign per time bucket). Same args and shape as account(); each row's pivot_value is the campaign URN. Pair with list_campaigns() for names, budgets, and type.

Example:

report = client.linkedin_ads.campaigns(
    start_date="2026-06-01",
    end_date="2026-06-30",
)
for row in report.rows:
    print(f"{row.pivot_value}: {row.clicks} clicks, "
          f"{row.cost_in_local_currency:.2f} {report.meta.currency_code}")

client.linkedin_ads.creatives()

Per-creative performance (one row per creative per time bucket). Same args and shape as account(); each row's pivot_value is the creative URN.

client.linkedin_ads.list_campaigns()

List the connected account's campaigns with metadata for interpreting the insight rows.

Signature:

result = client.linkedin_ads.list_campaigns()

Returns LinkedInAdsCampaignsResponse with:

  • rows - List of LinkedInAdsCampaignRow objects
  • row_count - Number of campaigns returned
  • meta - LinkedInAdsMeta (source, account_id, currency_code)

Each LinkedInAdsCampaignRow has:

  • id / name / status
  • type - SPONSORED_UPDATES | TEXT_AD | SPONSORED_INMAILS | DYNAMIC
  • cost_type - CPC | CPM | CPV
  • daily_budget / total_budget / unit_cost - decimal-string money objects in the account currency (amount + currency_code)
  • campaign_group - the parent campaign group URN
  • audience_expansion_enabled / run_schedule / targeting_criteria

Example:

for c in client.linkedin_ads.list_campaigns().rows:
    print(f"{c.name} [{c.status}] type={c.type}")

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

agentberlin-0.167.0.tar.gz (241.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

agentberlin-0.167.0-py3-none-any.whl (201.3 kB view details)

Uploaded Python 3

File details

Details for the file agentberlin-0.167.0.tar.gz.

File metadata

  • Download URL: agentberlin-0.167.0.tar.gz
  • Upload date:
  • Size: 241.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for agentberlin-0.167.0.tar.gz
Algorithm Hash digest
SHA256 748a336f68c2d5e9ad3bc59571969df5ad1bc7e8bd1bfd68dcb7a3aeea4b326a
MD5 4c3a2b3e8dd1bce24a992cb4d2f7cf97
BLAKE2b-256 b1e413870729f6e616340253c656e91109196972cb4a2dcf1e38a11b2726fd71

See more details on using hashes here.

File details

Details for the file agentberlin-0.167.0-py3-none-any.whl.

File metadata

  • Download URL: agentberlin-0.167.0-py3-none-any.whl
  • Upload date:
  • Size: 201.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for agentberlin-0.167.0-py3-none-any.whl
Algorithm Hash digest
SHA256 24ac7041220d05186b579790481d839de2c5e7df9fd44e5f57e2ae11c3d5c927
MD5 4120ace89842943b38a9e68c4f4ec3aa
BLAKE2b-256 20f91e64fe77b0dcd98920527e449fbc058fed4c5f83c3ba303f7e7ed96e6d46

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page