Skip to content

Marketing and reporting

The client’s numbers, the advertising that moves them, the container that measures them, and the monthly document that explains them.

118 endpoints across 4 groups. A permission written module.resource.action:own is scoped: :own covers rows that are yours, :any covers everyone’s, and :any satisfies a check for :own.

EndpointPermission
GET /marketing/accountsmarketing.link.manage
GET /marketing/clientsmarketing.metrics.read
GET /marketing/companies/{company_id}/drilldownmarketing.metrics.read
GET /marketing/companies/{company_id}/metricsmarketing.metrics.read
GET /marketing/companies/{company_id}/settingsmarketing.metrics.read
PUT /marketing/companies/{company_id}/settingsmarketing.link.manage
GET /marketing/linksmarketing.metrics.read
POST /marketing/linksmarketing.link.manage
DELETE /marketing/links/{link_id}marketing.link.manage
GET /marketing/overviewmarketing.overview.read
GET /marketing/settingsmarketing.link.manage
PUT /marketing/settingsmarketing.link.manage
GET /marketing/summarymarketing.metrics.read
Parameters and schemas per endpoint

Available Accounts — The accounts/properties/sites the caller’s connection can reach for source.

Permission: marketing.link.manage

Query parameters:

NameTypeMeaning
sourceMarketingSource · required
website_idstringRequired for a source whose credential is per website (rankmath); ignored by every other source.

Returns: 200 · AccountsResponse

Linked Clients — The clients that actually have a source linked, with which sources and how each is doing.

Permission: marketing.metrics.read

Query parameters:

NameTypeMeaning
limitinteger(default 200)

Returns: 200 · MarketingClientList

GET /api/v1/marketing/companies/{company_id}/drilldown

Section titled “GET /api/v1/marketing/companies/{company_id}/drilldown”

Drilldown — A live tier-2 drill-down (top pages/queries/campaigns), Redis-cached ~1h.

Permission: marketing.metrics.read

Path parameters: company_id

Query parameters:

NameTypeMeaning
link_idstring · required
kindstring · required
range_daysinteger(default 30)
periodstringThe span to report on: a trailing window (30d, 90d, 365d), a preset (month, last_month, quarter, last_quarter) or a named calendar period (2026-07, 2026-Q3). Wins over range_days; an unknown value falls back to 30d.

Returns: 200 · DrilldownResponse

GET /api/v1/marketing/companies/{company_id}/metrics

Section titled “GET /api/v1/marketing/companies/{company_id}/metrics”

Company Metrics

Permission: marketing.metrics.read

Path parameters: company_id

Query parameters:

NameTypeMeaning
range_daysinteger(default 30)
periodstringThe span to report on: a trailing window (30d, 90d, 365d), a preset (month, last_month, quarter, last_quarter) or a named calendar period (2026-07, 2026-Q3). Wins over range_days; an unknown value falls back to 30d.

Returns: 200 · CompanyMarketing

GET /api/v1/marketing/companies/{company_id}/settings

Section titled “GET /api/v1/marketing/companies/{company_id}/settings”

Get Company Settings — This client’s marketing preferences, and what they resolve to.

Permission: marketing.metrics.read

Path parameters: company_id

Returns: 200 · CompanySettingsRead

PUT /api/v1/marketing/companies/{company_id}/settings

Section titled “PUT /api/v1/marketing/companies/{company_id}/settings”

Set Company Settings — Per-client marketing preferences: the curated tab layout (#192), the comparison this client’s dashboard measures against (#312) and the legacy key-events toggle (#134).

Permission: marketing.link.manage

Path parameters: company_id

Request body: CompanySettingsUpdate

Returns: 200 · CompanySettingsRead

List Links

Permission: marketing.metrics.read

Query parameters:

NameTypeMeaning
company_idstring · required

Returns: 200 · app__modules__marketing__schemas__LinkRead[]

Create Link

Permission: marketing.link.manage

Request body: app__modules__marketing__schemas__LinkCreate

Returns: 201 · app__modules__marketing__schemas__LinkRead

Unlink

Permission: marketing.link.manage

Path parameters: link_id

Returns: 204

Overview — The morning-coffee grid: one row per linked client, from stored data, server-sorted.

Permission: marketing.overview.read

Query parameters:

NameTypeMeaning
range_daysinteger(default 30)
sortstringcompany_name | sessions | clicks | position | cost | conversions (- = desc)
periodstringThe span to report on: a trailing window (30d, 90d, 365d), a preset (month, last_month, quarter, last_quarter) or a named calendar period (2026-07, 2026-Q3). Wins over range_days; an unknown value falls back to 30d.

Returns: 200 · OverviewResponse

Get Settings — The org’s marketing settings — reports whether an Ads developer token is configured; the token itself is write-only and never returned (the Google client-secret pattern).

Permission: marketing.link.manage

Returns: 200 · MarketingSettingsRead

Save Settings — Store the encrypted Google Ads developer token (an empty value keeps the stored one).

Permission: marketing.link.manage

Request body: MarketingSettingsWrite

Returns: 200 · MarketingSettingsRead

Summary — The dashboard widget’s compact digest: top linked clients by their headline KPI, from stored data. Horizon-scoped like the per-company metrics read it summarizes — never wider than what the caller could fetch client-by-client.

Permission: marketing.metrics.read

Query parameters:

NameTypeMeaning
range_daysinteger(default 30)
limitinteger(default 5)
periodstringThe span to report on: a trailing window (30d, 90d, 365d), a preset (month, last_month, quarter, last_quarter) or a named calendar period (2026-07, 2026-Q3). Wins over range_days; an unknown value falls back to 30d.

Returns: 200 · MarketingSummary

EndpointPermission
GET /google-ads/accountsgoogle_ads.account.read
POST /google-ads/accountsgoogle_ads.settings.manage
GET /google-ads/accounts/{account_id}google_ads.account.read
PATCH /google-ads/accounts/{account_id}google_ads.settings.manage
DELETE /google-ads/accounts/{account_id}google_ads.settings.manage
GET /google-ads/accounts/{account_id}/ad-groupsgoogle_ads.account.read
POST /google-ads/accounts/{account_id}/ad-groupsgoogle_ads.campaign.write
PATCH /google-ads/accounts/{account_id}/ad-groups/{ad_group_id}google_ads.campaign.write
DELETE /google-ads/accounts/{account_id}/ad-groups/{ad_group_id}google_ads.campaign.write
DELETE /google-ads/accounts/{account_id}/ad-groups/{ad_group_id}/ads/{ad_id}google_ads.campaign.write
GET /google-ads/accounts/{account_id}/adsgoogle_ads.account.read
POST /google-ads/accounts/{account_id}/adsgoogle_ads.campaign.write
PATCH /google-ads/accounts/{account_id}/adsgoogle_ads.campaign.write
POST /google-ads/accounts/{account_id}/budgetsgoogle_ads.budget.write
PATCH /google-ads/accounts/{account_id}/budgets/{budget_id}google_ads.budget.write
DELETE /google-ads/accounts/{account_id}/budgets/{budget_id}google_ads.budget.write
GET /google-ads/accounts/{account_id}/campaignsgoogle_ads.account.read
POST /google-ads/accounts/{account_id}/campaignsgoogle_ads.campaign.write
PATCH /google-ads/accounts/{account_id}/campaigns/{campaign_id}google_ads.campaign.write
DELETE /google-ads/accounts/{account_id}/campaigns/{campaign_id}google_ads.campaign.write
GET /google-ads/accounts/{account_id}/changesgoogle_ads.account.read
GET /google-ads/accounts/{account_id}/conversionsgoogle_ads.account.read
GET /google-ads/accounts/{account_id}/decisionsgoogle_ads.account.read
POST /google-ads/accounts/{account_id}/decisionsgoogle_ads.policy.manage
DELETE /google-ads/accounts/{account_id}/decisions/{decision_id}google_ads.policy.manage
GET /google-ads/accounts/{account_id}/devicesgoogle_ads.account.read
GET /google-ads/accounts/{account_id}/geogoogle_ads.account.read
POST /google-ads/accounts/{account_id}/keyword-ideasgoogle_ads.account.read
GET /google-ads/accounts/{account_id}/keywordsgoogle_ads.account.read
POST /google-ads/accounts/{account_id}/keywordsgoogle_ads.keyword.write
PATCH /google-ads/accounts/{account_id}/keywordsgoogle_ads.keyword.write
POST /google-ads/accounts/{account_id}/keywords/removegoogle_ads.keyword.write
POST /google-ads/accounts/{account_id}/negative-listsgoogle_ads.negative.write
DELETE /google-ads/accounts/{account_id}/negative-lists/{shared_set_id}google_ads.negative.write
GET /google-ads/accounts/{account_id}/negativesgoogle_ads.account.read
POST /google-ads/accounts/{account_id}/negativesgoogle_ads.negative.write
POST /google-ads/accounts/{account_id}/negatives/removegoogle_ads.negative.write
GET /google-ads/accounts/{account_id}/policygoogle_ads.policy.manage
PUT /google-ads/accounts/{account_id}/policygoogle_ads.policy.manage
DELETE /google-ads/accounts/{account_id}/policygoogle_ads.policy.manage
POST /google-ads/accounts/{account_id}/querygoogle_ads.query.run
GET /google-ads/accounts/{account_id}/recommendationsgoogle_ads.account.read
GET /google-ads/accounts/{account_id}/search-termsgoogle_ads.account.read
GET /google-ads/accounts/{account_id}/snapshotgoogle_ads.account.read
GET /google-ads/accounts/{account_id}/trendgoogle_ads.account.read
POST /google-ads/accounts/{account_id}/verifygoogle_ads.settings.manage
GET /google-ads/accounts/availablegoogle_ads.settings.manage
GET /google-ads/policygoogle_ads.policy.manage
PUT /google-ads/policygoogle_ads.policy.manage
DELETE /google-ads/policygoogle_ads.policy.manage
GET /google-ads/settingsgoogle_ads.settings.manage
PUT /google-ads/settingsgoogle_ads.settings.manage
Parameters and schemas per endpoint

List Google Ads Accounts — Every linked Google Ads account this caller may see — start here.

Permission: google_ads.account.read

Query parameters:

NameTypeMeaning
company_idstring
active_onlyboolean(default false)

Returns: 200 · GoogleAdsAccountRead[]

Link Google Ads Account — Link an Ads account to a client.

Permission: google_ads.settings.manage

Request body: GoogleAdsAccountCreate

Returns: 201 · GoogleAdsAccountRead

GET /api/v1/google-ads/accounts/{account_id}

Section titled “GET /api/v1/google-ads/accounts/{account_id}”

Get Google Ads Account

Permission: google_ads.account.read

Path parameters: account_id

Returns: 200 · GoogleAdsAccountRead

PATCH /api/v1/google-ads/accounts/{account_id}

Section titled “PATCH /api/v1/google-ads/accounts/{account_id}”

Update Google Ads Account

Permission: google_ads.settings.manage

Path parameters: account_id

Request body: GoogleAdsAccountUpdate

Returns: 200 · GoogleAdsAccountRead

DELETE /api/v1/google-ads/accounts/{account_id}

Section titled “DELETE /api/v1/google-ads/accounts/{account_id}”

Unlink Google Ads Account — Deactivate the link. The row survives: history hangs off it, and a re-link must find the same account rather than collide with its own unique constraint.

Permission: google_ads.settings.manage

Path parameters: account_id

Returns: 204

GET /api/v1/google-ads/accounts/{account_id}/ad-groups

Section titled “GET /api/v1/google-ads/accounts/{account_id}/ad-groups”

Google Ads Ad Groups — Ad-group performance, most expensive first. One level below campaigns.

Permission: google_ads.account.read

Path parameters: account_id

Query parameters:

NameTypeMeaning
campaignsstring[]
include_removedboolean(default false)
periodstringA named span: 30d, 90d, month, last_month, quarter, last_quarter, 2026-07, 2026-Q3. Resolved in the account’s own timezone and always ending yesterday. Ignored when date_from and date_to are both given.
date_fromstringYYYY-MM-DD, inclusive.
date_tostringYYYY-MM-DD, inclusive.
statusstringOnly rows with this Google status: ENABLED, PAUSED or REMOVED. REMOVED implies include_removed, because a filter that always answers nothing is not a filter.
qstringFree text, matched case-insensitively against the row’s own readable fields — the campaign or ad-group name, the keyword, the search term, the place. Applied to the whole list before the page is taken, so page 2 of a search is page 2 of the search.
limitintegerHow many rows this page holds. Omit for the rest of the list, which is what a caller with no pager means. Never more than the read’s own ceiling.
offsetintegerWhere the page starts. total_rows is what it runs to. (default 0)

Returns: 200 · GoogleAdsReport

POST /api/v1/google-ads/accounts/{account_id}/ad-groups

Section titled “POST /api/v1/google-ads/accounts/{account_id}/ad-groups”

Create Google Ads Ad Group — Create an ad group inside an existing campaign. Paused, like everything created here.

Permission: google_ads.campaign.write

Path parameters: account_id

Request body: GoogleAdsAdGroupCreate

Returns: 201 · GoogleAdsMutationRead

PATCH /api/v1/google-ads/accounts/{account_id}/ad-groups/{ad_group_id}

Section titled “PATCH /api/v1/google-ads/accounts/{account_id}/ad-groups/{ad_group_id}”

Update Google Ads Ad Group — Pause, resume, remove, rename or re-bid an ad group.

Permission: google_ads.campaign.write

Path parameters: account_id, ad_group_id

Request body: GoogleAdsAdGroupUpdate

Returns: 200 · GoogleAdsMutationRead

DELETE /api/v1/google-ads/accounts/{account_id}/ad-groups/{ad_group_id}

Section titled “DELETE /api/v1/google-ads/accounts/{account_id}/ad-groups/{ad_group_id}”

Remove Google Ads Ad Group — Delete an ad group, with the keywords and ads inside it. Permanent at Google.

Permission: google_ads.campaign.write

Path parameters: account_id, ad_group_id

Query parameters:

NameTypeMeaning
validate_onlybooleanCheck and change nothing. (default false)

Returns: 200 · GoogleAdsMutationRead

DELETE /api/v1/google-ads/accounts/{account_id}/ad-groups/{ad_group_id}/ads/{ad_id}

Section titled “DELETE /api/v1/google-ads/accounts/{account_id}/ad-groups/{ad_group_id}/ads/{ad_id}”

Remove Google Ads Ad — Delete one ad. Permanent at Google — a replacement is a new ad with its own history.

Permission: google_ads.campaign.write

Path parameters: account_id, ad_group_id, ad_id

Query parameters:

NameTypeMeaning
validate_onlybooleanCheck and change nothing. (default false)

Returns: 200 · GoogleAdsMutationRead

GET /api/v1/google-ads/accounts/{account_id}/ads

Section titled “GET /api/v1/google-ads/accounts/{account_id}/ads”

Google Ads Ads — Ads with their ad strength and policy approval status, most expensive first.

Permission: google_ads.account.read

Path parameters: account_id

Query parameters:

NameTypeMeaning
campaignsstring[]
periodstringA named span: 30d, 90d, month, last_month, quarter, last_quarter, 2026-07, 2026-Q3. Resolved in the account’s own timezone and always ending yesterday. Ignored when date_from and date_to are both given.
date_fromstringYYYY-MM-DD, inclusive.
date_tostringYYYY-MM-DD, inclusive.
statusstringOnly rows with this Google status: ENABLED, PAUSED or REMOVED. REMOVED implies include_removed, because a filter that always answers nothing is not a filter.
qstringFree text, matched case-insensitively against the row’s own readable fields — the campaign or ad-group name, the keyword, the search term, the place. Applied to the whole list before the page is taken, so page 2 of a search is page 2 of the search.
limitintegerHow many rows this page holds. Omit for the rest of the list, which is what a caller with no pager means. Never more than the read’s own ceiling.
offsetintegerWhere the page starts. total_rows is what it runs to. (default 0)

Returns: 200 · GoogleAdsReport

POST /api/v1/google-ads/accounts/{account_id}/ads

Section titled “POST /api/v1/google-ads/accounts/{account_id}/ads”

Create Google Ads Ad — Create a responsive search ad. It is paused until somebody reads it and enables it.

Permission: google_ads.campaign.write

Path parameters: account_id

Request body: GoogleAdsAdCreate

Returns: 201 · GoogleAdsMutationRead

PATCH /api/v1/google-ads/accounts/{account_id}/ads

Section titled “PATCH /api/v1/google-ads/accounts/{account_id}/ads”

Update Google Ads Ad — Pause or resume one ad. status is ENABLED or PAUSED; to delete it, use DELETE.

Permission: google_ads.campaign.write

Path parameters: account_id

Request body: GoogleAdsAdUpdate

Returns: 200 · GoogleAdsMutationRead

POST /api/v1/google-ads/accounts/{account_id}/budgets

Section titled “POST /api/v1/google-ads/accounts/{account_id}/budgets”

Create Google Ads Budget — Create a daily budget, in the account’s own currency.

Permission: google_ads.budget.write

Path parameters: account_id

Request body: GoogleAdsBudgetCreate

Returns: 201 · GoogleAdsMutationRead

PATCH /api/v1/google-ads/accounts/{account_id}/budgets/{budget_id}

Section titled “PATCH /api/v1/google-ads/accounts/{account_id}/budgets/{budget_id}”

Update Google Ads Budget — Change a daily budget — the one write where being wrong has already cost money.

Permission: google_ads.budget.write

Path parameters: account_id, budget_id

Request body: GoogleAdsBudgetUpdate

Returns: 200 · GoogleAdsMutationRead

DELETE /api/v1/google-ads/accounts/{account_id}/budgets/{budget_id}

Section titled “DELETE /api/v1/google-ads/accounts/{account_id}/budgets/{budget_id}”

Remove Google Ads Budget — Delete a daily budget.

Permission: google_ads.budget.write

Path parameters: account_id, budget_id

Query parameters:

NameTypeMeaning
validate_onlybooleanCheck and change nothing. (default false)

Returns: 200 · GoogleAdsMutationRead

GET /api/v1/google-ads/accounts/{account_id}/campaigns

Section titled “GET /api/v1/google-ads/accounts/{account_id}/campaigns”

Google Ads Campaigns — Campaign performance and settings, most expensive first.

Permission: google_ads.account.read

Path parameters: account_id

Query parameters:

NameTypeMeaning
campaignsstring[]Filter by campaign name, case-insensitive substring match.
include_removedbooleanInclude removed campaigns. Off by default: a list where a third of the rows cannot be acted on is a worse answer to ‘what are we running’. Turn it on to ask what was spent on things since removed. (default false)
periodstringA named span: 30d, 90d, month, last_month, quarter, last_quarter, 2026-07, 2026-Q3. Resolved in the account’s own timezone and always ending yesterday. Ignored when date_from and date_to are both given.
date_fromstringYYYY-MM-DD, inclusive.
date_tostringYYYY-MM-DD, inclusive.
statusstringOnly rows with this Google status: ENABLED, PAUSED or REMOVED. REMOVED implies include_removed, because a filter that always answers nothing is not a filter.
qstringFree text, matched case-insensitively against the row’s own readable fields — the campaign or ad-group name, the keyword, the search term, the place. Applied to the whole list before the page is taken, so page 2 of a search is page 2 of the search.
limitintegerHow many rows this page holds. Omit for the rest of the list, which is what a caller with no pager means. Never more than the read’s own ceiling.
offsetintegerWhere the page starts. total_rows is what it runs to. (default 0)

Returns: 200 · GoogleAdsReport

POST /api/v1/google-ads/accounts/{account_id}/campaigns

Section titled “POST /api/v1/google-ads/accounts/{account_id}/campaigns”

Create Google Ads Campaign — Create a campaign. It is always paused and spends nothing until somebody enables it.

Permission: google_ads.campaign.write

Path parameters: account_id

Request body: GoogleAdsCampaignCreate

Returns: 201 · GoogleAdsMutationRead

PATCH /api/v1/google-ads/accounts/{account_id}/campaigns/{campaign_id}

Section titled “PATCH /api/v1/google-ads/accounts/{account_id}/campaigns/{campaign_id}”

Update Google Ads Campaign — Pause, resume, remove or rename a campaign. status is ENABLED, PAUSED or REMOVED.

Permission: google_ads.campaign.write

Path parameters: account_id, campaign_id

Request body: GoogleAdsCampaignUpdate

Returns: 200 · GoogleAdsMutationRead

DELETE /api/v1/google-ads/accounts/{account_id}/campaigns/{campaign_id}

Section titled “DELETE /api/v1/google-ads/accounts/{account_id}/campaigns/{campaign_id}”

Remove Google Ads Campaign — Delete a campaign. Permanent at Google — it can be recreated, never restored.

Permission: google_ads.campaign.write

Path parameters: account_id, campaign_id

Query parameters:

NameTypeMeaning
validate_onlybooleanCheck and change nothing. (default false)

Returns: 200 · GoogleAdsMutationRead

GET /api/v1/google-ads/accounts/{account_id}/changes

Section titled “GET /api/v1/google-ads/accounts/{account_id}/changes”

Google Ads Changes — What was changed in the account, with each field’s old and new value.

Permission: google_ads.account.read

Path parameters: account_id

Query parameters:

NameTypeMeaning
periodstringA named span: 30d, 90d, month, last_month, quarter, last_quarter, 2026-07, 2026-Q3. Resolved in the account’s own timezone and always ending yesterday. Ignored when date_from and date_to are both given.
date_fromstringYYYY-MM-DD, inclusive.
date_tostringYYYY-MM-DD, inclusive.
qstringFree text, matched case-insensitively against the row’s own readable fields — the campaign or ad-group name, the keyword, the search term, the place. Applied to the whole list before the page is taken, so page 2 of a search is page 2 of the search.
limitintegerHow many rows this page holds. Omit for the rest of the list, which is what a caller with no pager means. Never more than the read’s own ceiling.
offsetintegerWhere the page starts. total_rows is what it runs to. (default 0)

Returns: 200 · GoogleAdsReport

GET /api/v1/google-ads/accounts/{account_id}/conversions

Section titled “GET /api/v1/google-ads/accounts/{account_id}/conversions”

Google Ads Conversion Health — What this account optimises toward, and what each conversion action actually recorded.

Permission: google_ads.account.read

Path parameters: account_id

Query parameters:

NameTypeMeaning
periodstringA named span: 30d, 90d, month, last_month, quarter, last_quarter, 2026-07, 2026-Q3. Resolved in the account’s own timezone and always ending yesterday. Ignored when date_from and date_to are both given.
date_fromstringYYYY-MM-DD, inclusive.
date_tostringYYYY-MM-DD, inclusive.
qstringFree text, matched case-insensitively against the row’s own readable fields — the campaign or ad-group name, the keyword, the search term, the place. Applied to the whole list before the page is taken, so page 2 of a search is page 2 of the search.
limitintegerHow many rows this page holds. Omit for the rest of the list, which is what a caller with no pager means. Never more than the read’s own ceiling.
offsetintegerWhere the page starts. total_rows is what it runs to. (default 0)

Returns: 200 · GoogleAdsReport

GET /api/v1/google-ads/accounts/{account_id}/decisions

Section titled “GET /api/v1/google-ads/accounts/{account_id}/decisions”

List Google Ads Decisions — What has already been decided about this account — check this before proposing a change.

Permission: google_ads.account.read

Path parameters: account_id

Query parameters:

NameTypeMeaning
subject_typestring
decisionstring
include_withdrawnboolean(default false)
limitinteger(default 50)
offsetinteger(default 0)
countbooleanSet false to skip the total. (default true)

Returns: 200 · GoogleAdsDecisionPage

POST /api/v1/google-ads/accounts/{account_id}/decisions

Section titled “POST /api/v1/google-ads/accounts/{account_id}/decisions”

Record Google Ads Decision — Write down a judgement that changes nothing in Google.

Permission: google_ads.policy.manage

Path parameters: account_id

Request body: GoogleAdsDecisionCreate

Returns: 201 · GoogleAdsDecisionRead

DELETE /api/v1/google-ads/accounts/{account_id}/decisions/{decision_id}

Section titled “DELETE /api/v1/google-ads/accounts/{account_id}/decisions/{decision_id}”

Withdraw Google Ads Decision — Unsay a decision. The row survives, marked withdrawn and by whom.

Permission: google_ads.policy.manage

Path parameters: account_id, decision_id

Returns: 200 · GoogleAdsDecisionRead

GET /api/v1/google-ads/accounts/{account_id}/devices

Section titled “GET /api/v1/google-ads/accounts/{account_id}/devices”

Google Ads Devices — Performance per device, per campaign, plus an account-wide rollup in extra.device_totals.

Permission: google_ads.account.read

Path parameters: account_id

Query parameters:

NameTypeMeaning
campaignsstring[]
periodstringA named span: 30d, 90d, month, last_month, quarter, last_quarter, 2026-07, 2026-Q3. Resolved in the account’s own timezone and always ending yesterday. Ignored when date_from and date_to are both given.
date_fromstringYYYY-MM-DD, inclusive.
date_tostringYYYY-MM-DD, inclusive.
qstringFree text, matched case-insensitively against the row’s own readable fields — the campaign or ad-group name, the keyword, the search term, the place. Applied to the whole list before the page is taken, so page 2 of a search is page 2 of the search.
limitintegerHow many rows this page holds. Omit for the rest of the list, which is what a caller with no pager means. Never more than the read’s own ceiling.
offsetintegerWhere the page starts. total_rows is what it runs to. (default 0)

Returns: 200 · GoogleAdsReport

GET /api/v1/google-ads/accounts/{account_id}/geo

Section titled “GET /api/v1/google-ads/accounts/{account_id}/geo”

Google Ads Geo — Where the people who saw the ads physically were — not where the campaign targets.

Permission: google_ads.account.read

Path parameters: account_id

Query parameters:

NameTypeMeaning
campaignsstring[]
periodstringA named span: 30d, 90d, month, last_month, quarter, last_quarter, 2026-07, 2026-Q3. Resolved in the account’s own timezone and always ending yesterday. Ignored when date_from and date_to are both given.
date_fromstringYYYY-MM-DD, inclusive.
date_tostringYYYY-MM-DD, inclusive.
qstringFree text, matched case-insensitively against the row’s own readable fields — the campaign or ad-group name, the keyword, the search term, the place. Applied to the whole list before the page is taken, so page 2 of a search is page 2 of the search.
limitintegerHow many rows this page holds. Omit for the rest of the list, which is what a caller with no pager means. Never more than the read’s own ceiling.
offsetintegerWhere the page starts. total_rows is what it runs to. (default 0)

Returns: 200 · GoogleAdsReport

POST /api/v1/google-ads/accounts/{account_id}/keyword-ideas

Section titled “POST /api/v1/google-ads/accounts/{account_id}/keyword-ideas”

Google Ads Keyword Ideas — Keyword ideas with search volume and competition, from seed terms or a landing page.

Permission: google_ads.account.read

Path parameters: account_id

Request body: GoogleAdsKeywordIdeaRequest

Returns: 200 · GoogleAdsReport

GET /api/v1/google-ads/accounts/{account_id}/keywords

Section titled “GET /api/v1/google-ads/accounts/{account_id}/keywords”

Google Ads Keywords — Positive keywords with match type, bid and Quality Score, most expensive first.

Permission: google_ads.account.read

Path parameters: account_id

Query parameters:

NameTypeMeaning
campaignsstring[]
include_removedboolean(default false)
periodstringA named span: 30d, 90d, month, last_month, quarter, last_quarter, 2026-07, 2026-Q3. Resolved in the account’s own timezone and always ending yesterday. Ignored when date_from and date_to are both given.
date_fromstringYYYY-MM-DD, inclusive.
date_tostringYYYY-MM-DD, inclusive.
statusstringOnly rows with this Google status: ENABLED, PAUSED or REMOVED. REMOVED implies include_removed, because a filter that always answers nothing is not a filter.
qstringFree text, matched case-insensitively against the row’s own readable fields — the campaign or ad-group name, the keyword, the search term, the place. Applied to the whole list before the page is taken, so page 2 of a search is page 2 of the search.
limitintegerHow many rows this page holds. Omit for the rest of the list, which is what a caller with no pager means. Never more than the read’s own ceiling.
offsetintegerWhere the page starts. total_rows is what it runs to. (default 0)

Returns: 200 · GoogleAdsReport

POST /api/v1/google-ads/accounts/{account_id}/keywords

Section titled “POST /api/v1/google-ads/accounts/{account_id}/keywords”

Add Google Ads Keywords — Add positive keywords to one ad group. match_type is EXACT, PHRASE or BROAD.

Permission: google_ads.keyword.write

Path parameters: account_id

Request body: GoogleAdsKeywordsAdd

Returns: 201 · GoogleAdsMutationRead

PATCH /api/v1/google-ads/accounts/{account_id}/keywords

Section titled “PATCH /api/v1/google-ads/accounts/{account_id}/keywords”

Update Google Ads Keyword — Pause, resume, remove or re-bid one keyword.

Permission: google_ads.keyword.write

Path parameters: account_id

Request body: GoogleAdsKeywordUpdate

Returns: 200 · GoogleAdsMutationRead

POST /api/v1/google-ads/accounts/{account_id}/keywords/remove

Section titled “POST /api/v1/google-ads/accounts/{account_id}/keywords/remove”

Remove Google Ads Keywords — Remove keywords from an ad group, by criterion id.

Permission: google_ads.keyword.write

Path parameters: account_id

Request body: GoogleAdsKeywordsRemove

Returns: 200 · GoogleAdsMutationRead

POST /api/v1/google-ads/accounts/{account_id}/negative-lists

Section titled “POST /api/v1/google-ads/accounts/{account_id}/negative-lists”

Create Google Ads Negative List — Create a shared negative-keyword list and optionally attach it to campaigns.

Permission: google_ads.negative.write

Path parameters: account_id

Request body: GoogleAdsNegativeListCreate

Returns: 201 · GoogleAdsMutationRead

DELETE /api/v1/google-ads/accounts/{account_id}/negative-lists/{shared_set_id}

Section titled “DELETE /api/v1/google-ads/accounts/{account_id}/negative-lists/{shared_set_id}”

Remove Google Ads Negative List — Delete a shared negative-keyword list.

Permission: google_ads.negative.write

Path parameters: account_id, shared_set_id

Query parameters:

NameTypeMeaning
validate_onlybooleanCheck and change nothing. (default false)

Returns: 200 · GoogleAdsMutationRead

GET /api/v1/google-ads/accounts/{account_id}/negatives

Section titled “GET /api/v1/google-ads/accounts/{account_id}/negatives”

Google Ads Negatives — Every negative keyword: ad-group level, campaign level, and shared negative lists.

Permission: google_ads.account.read

Path parameters: account_id

Query parameters:

NameTypeMeaning
qstringFree text, matched case-insensitively against the row’s own readable fields — the campaign or ad-group name, the keyword, the search term, the place. Applied to the whole list before the page is taken, so page 2 of a search is page 2 of the search.
limitintegerHow many rows this page holds. Omit for the rest of the list, which is what a caller with no pager means. Never more than the read’s own ceiling.
offsetintegerWhere the page starts. total_rows is what it runs to. (default 0)

Returns: 200 · GoogleAdsReport

POST /api/v1/google-ads/accounts/{account_id}/negatives

Section titled “POST /api/v1/google-ads/accounts/{account_id}/negatives”

Add Google Ads Negatives — Exclude search terms — and record the ones you deliberately did not exclude.

Permission: google_ads.negative.write

Path parameters: account_id

Request body: GoogleAdsNegativesAdd

Returns: 201 · GoogleAdsMutationRead

POST /api/v1/google-ads/accounts/{account_id}/negatives/remove

Section titled “POST /api/v1/google-ads/accounts/{account_id}/negatives/remove”

Remove Google Ads Negatives — Take exclusions back off, by criterion id, at the level they were added.

Permission: google_ads.negative.write

Path parameters: account_id

Request body: GoogleAdsNegativesRemove

Returns: 200 · GoogleAdsMutationRead

GET /api/v1/google-ads/accounts/{account_id}/policy

Section titled “GET /api/v1/google-ads/accounts/{account_id}/policy”

Get Google Ads Policy — The rules that bind writes to this account — read this before proposing anything.

Permission: google_ads.policy.manage

Path parameters: account_id

Returns: 200 · GoogleAdsPolicyRead

PUT /api/v1/google-ads/accounts/{account_id}/policy

Section titled “PUT /api/v1/google-ads/accounts/{account_id}/policy”

Save Google Ads Policy — Set this account’s own rules.

Permission: google_ads.policy.manage

Path parameters: account_id

Request body: GoogleAdsPolicyWrite

Returns: 200 · GoogleAdsPolicyRead

DELETE /api/v1/google-ads/accounts/{account_id}/policy

Section titled “DELETE /api/v1/google-ads/accounts/{account_id}/policy”

Clear Google Ads Policy — Drop this account’s own rules, so it inherits the agency’s house policy again.

Permission: google_ads.policy.manage

Path parameters: account_id

Returns: 200 · GoogleAdsPolicyRead

POST /api/v1/google-ads/accounts/{account_id}/query

Section titled “POST /api/v1/google-ads/accounts/{account_id}/query”

Google Ads Query — Run a GAQL query against this account — the escape hatch for questions the other tools do not answer.

Permission: google_ads.query.run

Path parameters: account_id

Request body: GoogleAdsQueryRequest

Returns: 200 · GoogleAdsQueryRead

GET /api/v1/google-ads/accounts/{account_id}/recommendations

Section titled “GET /api/v1/google-ads/accounts/{account_id}/recommendations”

Google Ads Recommendations — Google’s own suggestions for this account, with the impact it projects for each.

Permission: google_ads.account.read

Path parameters: account_id

Query parameters:

NameTypeMeaning
qstringFree text, matched case-insensitively against the row’s own readable fields — the campaign or ad-group name, the keyword, the search term, the place. Applied to the whole list before the page is taken, so page 2 of a search is page 2 of the search.
limitintegerHow many rows this page holds. Omit for the rest of the list, which is what a caller with no pager means. Never more than the read’s own ceiling.
offsetintegerWhere the page starts. total_rows is what it runs to. (default 0)

Returns: 200 · GoogleAdsReport

GET /api/v1/google-ads/accounts/{account_id}/search-terms

Section titled “GET /api/v1/google-ads/accounts/{account_id}/search-terms”

Google Ads Search Terms — What people actually typed, most expensive first.

Permission: google_ads.account.read

Path parameters: account_id

Query parameters:

NameTypeMeaning
campaignsstring[]
min_costnumberOnly terms that cost at least this, in account currency.
min_clicksinteger
periodstringA named span: 30d, 90d, month, last_month, quarter, last_quarter, 2026-07, 2026-Q3. Resolved in the account’s own timezone and always ending yesterday. Ignored when date_from and date_to are both given.
date_fromstringYYYY-MM-DD, inclusive.
date_tostringYYYY-MM-DD, inclusive.
qstringFree text, matched case-insensitively against the row’s own readable fields — the campaign or ad-group name, the keyword, the search term, the place. Applied to the whole list before the page is taken, so page 2 of a search is page 2 of the search.
limitintegerHow many rows this page holds. Omit for the rest of the list, which is what a caller with no pager means. Never more than the read’s own ceiling.
offsetintegerWhere the page starts. total_rows is what it runs to. (default 0)

Returns: 200 · GoogleAdsReport

GET /api/v1/google-ads/accounts/{account_id}/snapshot

Section titled “GET /api/v1/google-ads/accounts/{account_id}/snapshot”

Google Ads Snapshot — Account totals plus every campaign for the period — start an analysis here.

Permission: google_ads.account.read

Path parameters: account_id

Query parameters:

NameTypeMeaning
periodstringA named span: 30d, 90d, month, last_month, quarter, last_quarter, 2026-07, 2026-Q3. Resolved in the account’s own timezone and always ending yesterday. Ignored when date_from and date_to are both given.
date_fromstringYYYY-MM-DD, inclusive.
date_tostringYYYY-MM-DD, inclusive.

Returns: 200 · GoogleAdsSnapshotRead

GET /api/v1/google-ads/accounts/{account_id}/trend

Section titled “GET /api/v1/google-ads/accounts/{account_id}/trend”

Google Ads Trend — A period against its comparison, with the change per metric already computed.

Permission: google_ads.account.read

Path parameters: account_id

Query parameters:

NameTypeMeaning
comparestringWhat to compare against: ‘year’ (the same period a year earlier, the default) or ‘previous’ (the period immediately before).
periodstringA named span: 30d, 90d, month, last_month, quarter, last_quarter, 2026-07, 2026-Q3. Resolved in the account’s own timezone and always ending yesterday. Ignored when date_from and date_to are both given.
date_fromstringYYYY-MM-DD, inclusive.
date_tostringYYYY-MM-DD, inclusive.

Returns: 200 · GoogleAdsTrendRead

POST /api/v1/google-ads/accounts/{account_id}/verify

Section titled “POST /api/v1/google-ads/accounts/{account_id}/verify”

Verify Google Ads Account — Ask Google what it says about this account, and record the answer either way.

Permission: google_ads.settings.manage

Path parameters: account_id

Returns: 200 · GoogleAdsAccountRead

List Available Google Ads Accounts — Accounts the caller’s own Google grant can reach, manager hierarchies expanded.

Permission: google_ads.settings.manage

Returns: 200 · GoogleAdsPickerRead

Get Google Ads House Policy — The agency’s own standing Ads rules, applied to every account that does not override them.

Permission: google_ads.policy.manage

Returns: 200 · GoogleAdsPolicyRead

Save Google Ads House Policy — Set the agency’s standing rules. Fields you leave out are not touched.

Permission: google_ads.policy.manage

Request body: GoogleAdsPolicyWrite

Returns: 200 · GoogleAdsPolicyRead

Clear Google Ads House Policy — Drop the agency’s house rules, leaving only the built-in ceiling every account gets.

Permission: google_ads.policy.manage

Returns: 200 · GoogleAdsPolicyRead

Get Google Ads Settings — The org’s Ads configuration. The developer token is never part of the response.

Permission: google_ads.settings.manage

Returns: 200 · GoogleAdsSettingsRead

Save Google Ads Settings

Permission: google_ads.settings.manage

Request body: GoogleAdsSettingsWrite

Returns: 200 · GoogleAdsSettingsRead

EndpointPermission
GET /gtm/containersgoogle_tag_manager.container.read
POST /gtm/containersgoogle_tag_manager.settings.manage
GET /gtm/containers/{container_id}google_tag_manager.container.read
PATCH /gtm/containers/{container_id}google_tag_manager.settings.manage
DELETE /gtm/containers/{container_id}google_tag_manager.settings.manage
GET /gtm/containers/{container_id}/conversionsgoogle_tag_manager.container.read
POST /gtm/containers/{container_id}/conversionsgoogle_tag_manager.tag.write
GET /gtm/containers/{container_id}/snippetgoogle_tag_manager.container.read
GET /gtm/containers/{container_id}/statusgoogle_tag_manager.container.read
GET /gtm/containers/{container_id}/tagsgoogle_tag_manager.container.read
POST /gtm/containers/{container_id}/tagsgoogle_tag_manager.tag.write
PATCH /gtm/containers/{container_id}/tags/{tag_id}google_tag_manager.tag.write
DELETE /gtm/containers/{container_id}/tags/{tag_id}google_tag_manager.tag.write
GET /gtm/containers/{container_id}/triggersgoogle_tag_manager.container.read
POST /gtm/containers/{container_id}/triggersgoogle_tag_manager.tag.write
DELETE /gtm/containers/{container_id}/triggers/{trigger_id}google_tag_manager.tag.write
GET /gtm/containers/{container_id}/variablesgoogle_tag_manager.container.read
POST /gtm/containers/{container_id}/variablesgoogle_tag_manager.tag.write
DELETE /gtm/containers/{container_id}/variables/{variable_id}google_tag_manager.tag.write
POST /gtm/containers/{container_id}/verifygoogle_tag_manager.settings.manage
GET /gtm/containers/{container_id}/versionsgoogle_tag_manager.container.read
POST /gtm/containers/{container_id}/versionsgoogle_tag_manager.tag.write
POST /gtm/containers/{container_id}/versions/{version_id}/publishgoogle_tag_manager.version.publish
GET /gtm/containers/{container_id}/workspacesgoogle_tag_manager.container.read
GET /gtm/containers/availablegoogle_tag_manager.settings.manage
GET /gtm/settingsgoogle_tag_manager.settings.manage
PUT /gtm/settingsgoogle_tag_manager.settings.manage
Parameters and schemas per endpoint

List Gtm Containers — Every linked Tag Manager container this caller may see — start here.

Permission: google_tag_manager.container.read

Query parameters:

NameTypeMeaning
company_idstring
active_onlyboolean(default false)

Returns: 200 · GtmContainerRead[]

Link Gtm Container — Attach a container to this workspace, and say whose it is.

Permission: google_tag_manager.settings.manage

Request body: GtmContainerCreate

Returns: 201 · GtmContainerRead

Get Gtm Container

Permission: google_tag_manager.container.read

Path parameters: container_id

Returns: 200 · GtmContainerRead

PATCH /api/v1/gtm/containers/{container_id}

Section titled “PATCH /api/v1/gtm/containers/{container_id}”

Update Gtm Container

Permission: google_tag_manager.settings.manage

Path parameters: container_id

Request body: GtmContainerUpdate

Returns: 200 · GtmContainerRead

DELETE /api/v1/gtm/containers/{container_id}

Section titled “DELETE /api/v1/gtm/containers/{container_id}”

Unlink Gtm Container — Forget the container here. Nothing is removed from Tag Manager — an agency that stops working for a client does not thereby delete the tracking off their website.

Permission: google_tag_manager.settings.manage

Path parameters: container_id

Returns: 204

GET /api/v1/gtm/containers/{container_id}/conversions

Section titled “GET /api/v1/gtm/containers/{container_id}/conversions”

List Gtm Conversions — The conversions schakl set up in this container, and whether each is live yet.

Permission: google_tag_manager.container.read

Path parameters: container_id

Returns: 200 · GtmConversionRead[]

POST /api/v1/gtm/containers/{container_id}/conversions

Section titled “POST /api/v1/gtm/containers/{container_id}/conversions”

Create Gtm Conversion — Set up one conversion: the trigger, the tag, and the record that they belong together.

Permission: google_tag_manager.tag.write

Path parameters: container_id

Request body: GtmConversionCreate

Returns: 201 · GtmConversionRead

GET /api/v1/gtm/containers/{container_id}/snippet

Section titled “GET /api/v1/gtm/containers/{container_id}/snippet”

Gtm Container Snippet — The install snippet, for the developer who has to put it on the site.

Permission: google_tag_manager.container.read

Path parameters: container_id

Returns: 200 · GtmSnippetRead

GET /api/v1/gtm/containers/{container_id}/status

Section titled “GET /api/v1/gtm/containers/{container_id}/status”

Gtm Workspace Status — What is staged in a workspace and not live — the question before every publish.

Permission: google_tag_manager.container.read

Path parameters: container_id

Query parameters:

NameTypeMeaning
workspace_idstring

Returns: 200 · GtmWorkspaceStatusRead

GET /api/v1/gtm/containers/{container_id}/tags

Section titled “GET /api/v1/gtm/containers/{container_id}/tags”

List Gtm Tags — Every tag in a workspace — what is (or is about to be) measuring this client’s site.

Permission: google_tag_manager.container.read

Path parameters: container_id

Query parameters:

NameTypeMeaning
workspace_idstring

Returns: 200 · GtmTagRead[]

POST /api/v1/gtm/containers/{container_id}/tags

Section titled “POST /api/v1/gtm/containers/{container_id}/tags”

Create Gtm Tag — Create a tag from its own type and parameter array.

Permission: google_tag_manager.tag.write

Path parameters: container_id

Query parameters:

NameTypeMeaning
workspace_idstring

Request body: GtmTagWrite

Returns: 201 · GtmTagRead

PATCH /api/v1/gtm/containers/{container_id}/tags/{tag_id}

Section titled “PATCH /api/v1/gtm/containers/{container_id}/tags/{tag_id}”

Update Gtm Tag — Change one tag. Reads it first and writes back under its fingerprint, so an edit somebody made in Tag Manager meanwhile is a 409 rather than a silent overwrite of their work.

Permission: google_tag_manager.tag.write

Path parameters: container_id, tag_id

Query parameters:

NameTypeMeaning
workspace_idstring

Request body: GtmTagUpdate

Returns: 200 · GtmTagRead

DELETE /api/v1/gtm/containers/{container_id}/tags/{tag_id}

Section titled “DELETE /api/v1/gtm/containers/{container_id}/tags/{tag_id}”

Delete Gtm Tag

Permission: google_tag_manager.tag.write

Path parameters: container_id, tag_id

Query parameters:

NameTypeMeaning
workspace_idstring

Returns: 204

GET /api/v1/gtm/containers/{container_id}/triggers

Section titled “GET /api/v1/gtm/containers/{container_id}/triggers”

List Gtm Triggers

Permission: google_tag_manager.container.read

Path parameters: container_id

Query parameters:

NameTypeMeaning
workspace_idstring

Returns: 200 · GtmTriggerRead[]

POST /api/v1/gtm/containers/{container_id}/triggers

Section titled “POST /api/v1/gtm/containers/{container_id}/triggers”

Create Gtm Trigger — Create a trigger from six named kinds rather than from GTM’s own vocabulary.

Permission: google_tag_manager.tag.write

Path parameters: container_id

Query parameters:

NameTypeMeaning
workspace_idstring

Request body: GtmTriggerWrite

Returns: 201 · GtmTriggerRead

DELETE /api/v1/gtm/containers/{container_id}/triggers/{trigger_id}

Section titled “DELETE /api/v1/gtm/containers/{container_id}/triggers/{trigger_id}”

Delete Gtm Trigger

Permission: google_tag_manager.tag.write

Path parameters: container_id, trigger_id

Query parameters:

NameTypeMeaning
workspace_idstring

Returns: 204

GET /api/v1/gtm/containers/{container_id}/variables

Section titled “GET /api/v1/gtm/containers/{container_id}/variables”

List Gtm Variables

Permission: google_tag_manager.container.read

Path parameters: container_id

Query parameters:

NameTypeMeaning
workspace_idstring

Returns: 200 · GtmVariableRead[]

POST /api/v1/gtm/containers/{container_id}/variables

Section titled “POST /api/v1/gtm/containers/{container_id}/variables”

Create Gtm Variable — Create a user-defined variable — a dataLayer read (v), a constant (c), a lookup.

Permission: google_tag_manager.tag.write

Path parameters: container_id

Query parameters:

NameTypeMeaning
workspace_idstring

Request body: GtmVariableWrite

Returns: 201 · GtmVariableRead

DELETE /api/v1/gtm/containers/{container_id}/variables/{variable_id}

Section titled “DELETE /api/v1/gtm/containers/{container_id}/variables/{variable_id}”

Delete Gtm Variable

Permission: google_tag_manager.tag.write

Path parameters: container_id, variable_id

Query parameters:

NameTypeMeaning
workspace_idstring

Returns: 204

POST /api/v1/gtm/containers/{container_id}/verify

Section titled “POST /api/v1/gtm/containers/{container_id}/verify”

Verify Gtm Container — Ask Google what it says about this container, and record the answer either way.

Permission: google_tag_manager.settings.manage

Path parameters: container_id

Returns: 200 · GtmContainerRead

GET /api/v1/gtm/containers/{container_id}/versions

Section titled “GET /api/v1/gtm/containers/{container_id}/versions”

List Gtm Versions — The container’s version history, newest first, with the live one marked.

Permission: google_tag_manager.container.read

Path parameters: container_id

Returns: 200 · GtmVersionRead[]

POST /api/v1/gtm/containers/{container_id}/versions

Section titled “POST /api/v1/gtm/containers/{container_id}/versions”

Create Gtm Version — Freeze what is staged into a version. Still live for nobody — publishing is separate.

Permission: google_tag_manager.tag.write

Path parameters: container_id

Request body: GtmVersionCreate

Returns: 201 · GtmVersionCreated

POST /api/v1/gtm/containers/{container_id}/versions/{version_id}/publish

Section titled “POST /api/v1/gtm/containers/{container_id}/versions/{version_id}/publish”

Publish Gtm Version — Make this version live on the client’s website, now, for every visitor.

Permission: google_tag_manager.version.publish

Path parameters: container_id, version_id

Returns: 200 · GtmPublishResult

GET /api/v1/gtm/containers/{container_id}/workspaces

Section titled “GET /api/v1/gtm/containers/{container_id}/workspaces”

List Gtm Workspaces — The container’s workspaces — its shared drafts. Usually one; sometimes one per person.

Permission: google_tag_manager.container.read

Path parameters: container_id

Returns: 200 · GtmWorkspaceRead[]

List Available Gtm Containers — Containers the caller’s own Google grant can reach, across every Tag Manager account.

Permission: google_tag_manager.settings.manage

Returns: 200 · GtmPickerRead

Get Gtm Settings — The org’s Tag Manager posture: the write kill switch and the workspace schakl writes in.

Permission: google_tag_manager.settings.manage

Returns: 200 · GtmSettingsRead

Save Gtm Settings

Permission: google_tag_manager.settings.manage

Request body: GtmSettingsWrite

Returns: 200 · GtmSettingsRead

EndpointPermission
GET /reporting/companies/{company_id}/profilereporting.profile.manage
PUT /reporting/companies/{company_id}/profilereporting.profile.manage
GET /reporting/reportsreporting.report.read
GET /reporting/reports/{report_id}reporting.report.read
DELETE /reporting/reports/{report_id}reporting.report.write
PUT /reporting/reports/{report_id}/narrativereporting.report.write
GET /reporting/reports/{report_id}/pdfreporting.report.read
GET /reporting/reports/{report_id}/previewreporting.report.read
POST /reporting/reports/{report_id}/publishreporting.report.send
POST /reporting/reports/{report_id}/rewritereporting.report.write
POST /reporting/reports/{report_id}/sendreporting.report.send
POST /reporting/reports/generatereporting.report.write
POST /reporting/reports/generate-batchreporting.report.write
GET /reporting/settingsreporting.profile.manage
PUT /reporting/settingsreporting.settings.manage
GET /reporting/templatesreporting.profile.manage
POST /reporting/templatesreporting.settings.manage
PUT /reporting/templates/{template_id}reporting.settings.manage
DELETE /reporting/templates/{template_id}reporting.settings.manage
GET /reporting/templates/designs/{design}/sourcereporting.settings.manage
POST /reporting/templates/previewreporting.settings.manage
GET /reporting/templates/sectionsreporting.settings.manage
GET /reporting/tonesreporting.profile.manage
POST /reporting/tonesreporting.settings.manage
PUT /reporting/tones/{tone_id}reporting.settings.manage
DELETE /reporting/tones/{tone_id}reporting.settings.manage
Parameters and schemas per endpoint

GET /api/v1/reporting/companies/{company_id}/profile

Section titled “GET /api/v1/reporting/companies/{company_id}/profile”

Get Profile — The client’s reporting setup. A company that has never had one answers with the inherited defaults rather than a 404 — the form is the same either way.

Permission: reporting.profile.manage

Path parameters: company_id

Returns: 200 · ReportProfileRead

PUT /api/v1/reporting/companies/{company_id}/profile

Section titled “PUT /api/v1/reporting/companies/{company_id}/profile”

Save Profile

Permission: reporting.profile.manage

Path parameters: company_id

Request body: ReportProfileWrite

Returns: 200 · ReportProfileRead

List Reports

Permission: reporting.report.read

Query parameters:

NameTypeMeaning
company_idstring
audienceReportAudience
limitinteger(default 50)
offsetinteger(default 0)
countboolean(default true)

Returns: 200 · ReportList

Get Report

Permission: reporting.report.read

Path parameters: report_id

Returns: 200 · ReportDetail

DELETE /api/v1/reporting/reports/{report_id}

Section titled “DELETE /api/v1/reporting/reports/{report_id}”

Delete Report

Permission: reporting.report.write

Path parameters: report_id

Returns: 204

PUT /api/v1/reporting/reports/{report_id}/narrative

Section titled “PUT /api/v1/reporting/reports/{report_id}/narrative”

Edit Narrative — Hand-edit the prose before it goes out — the point of review-before-send.

Permission: reporting.report.write

Path parameters: report_id

Request body: ReportNarrativeUpdate

Returns: 200 · ReportDetail

GET /api/v1/reporting/reports/{report_id}/pdf

Section titled “GET /api/v1/reporting/reports/{report_id}/pdf”

Report Pdf — The stored document. Loaded through the portal-aware repository, so a client can download exactly the reports they can open — and never the internal analysis.

Permission: reporting.report.read

Path parameters: report_id

Returns: 200

GET /api/v1/reporting/reports/{report_id}/preview

Section titled “GET /api/v1/reporting/reports/{report_id}/preview”

Preview Report — The document as HTML — the same artefact the PDF prints, so the two cannot drift.

Permission: reporting.report.read

Path parameters: report_id

Returns: 200

POST /api/v1/reporting/reports/{report_id}/publish

Section titled “POST /api/v1/reporting/reports/{report_id}/publish”

Publish Report

Permission: reporting.report.send

Path parameters: report_id

Query parameters:

NameTypeMeaning
publishedboolean(default true)

Returns: 200 · ReportDetail

POST /api/v1/reporting/reports/{report_id}/rewrite

Section titled “POST /api/v1/reporting/reports/{report_id}/rewrite”

Rewrite Section

Permission: reporting.report.write

Path parameters: report_id

Request body: ReportRewriteRequest

Returns: 200 · ReportDetail

POST /api/v1/reporting/reports/{report_id}/send

Section titled “POST /api/v1/reporting/reports/{report_id}/send”

Send Report

Permission: reporting.report.send

Path parameters: report_id

Request body: ReportSendRequest

Returns: 200 · ReportDetail

Generate Report — Queue a run. Never generates inline — it calls several APIs and a model.

Permission: reporting.report.write

Request body: ReportRunRequest

Returns: 200 · ReportActionResult

POST /api/v1/reporting/reports/generate-batch

Section titled “POST /api/v1/reporting/reports/generate-batch”

Generate Batch

Permission: reporting.report.write

Request body: ReportRunBatchRequest

Returns: 200 · ReportRunBatchResult

Get Settings

Permission: reporting.profile.manage

Returns: 200 · ReportingSettingsRead

Save Settings

Permission: reporting.settings.manage

Request body: ReportingSettingsWrite

Returns: 200 · ReportingSettingsRead

List Templates

Permission: reporting.profile.manage

Query parameters:

NameTypeMeaning
audienceReportAudience

Returns: 200 · ReportTemplateRead[]

Create Template

Permission: reporting.settings.manage

Request body: ReportTemplateWrite

Returns: 201 · ReportTemplateRead

PUT /api/v1/reporting/templates/{template_id}

Section titled “PUT /api/v1/reporting/templates/{template_id}”

Update Template

Permission: reporting.settings.manage

Path parameters: template_id

Request body: ReportTemplateWrite

Returns: 200 · ReportTemplateRead

DELETE /api/v1/reporting/templates/{template_id}

Section titled “DELETE /api/v1/reporting/templates/{template_id}”

Delete Template

Permission: reporting.settings.manage

Path parameters: template_id

Returns: 204

GET /api/v1/reporting/templates/designs/{design}/source

Section titled “GET /api/v1/reporting/templates/designs/{design}/source”

Template Source — A shipped design’s own HTML and CSS, to start a custom report template from.

Permission: reporting.settings.manage

Path parameters: design

Returns: 200 · ReportTemplateSource

Preview Template — Render an unsaved template — the editor’s live preview.

Permission: reporting.settings.manage

Request body: ReportTemplatePreviewRequest

Returns: 200

Section Catalog — What a template may order or switch off — the registry, made visible (§15’s “registry, not free text”, applied to design).

Permission: reporting.settings.manage

Returns: 200 · SectionCatalogEntry[]

List Tones — Readable by whoever assigns one to a client, editable only by an admin (the service re-checks) — a manager must be able to see what voice they are picking.

Permission: reporting.profile.manage

Returns: 200 · ReportToneRead[]

Create Tone

Permission: reporting.settings.manage

Request body: ReportToneWrite

Returns: 201 · ReportToneRead

Update Tone

Permission: reporting.settings.manage

Path parameters: tone_id

Request body: ReportToneWrite

Returns: 200 · ReportToneRead

Delete Tone

Permission: reporting.settings.manage

Path parameters: tone_id

Returns: 204