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.
marketing
Section titled “marketing”| Endpoint | Permission |
|---|---|
GET /marketing/accounts | marketing.link.manage |
GET /marketing/clients | marketing.metrics.read |
GET /marketing/companies/{company_id}/drilldown | marketing.metrics.read |
GET /marketing/companies/{company_id}/metrics | marketing.metrics.read |
GET /marketing/companies/{company_id}/settings | marketing.metrics.read |
PUT /marketing/companies/{company_id}/settings | marketing.link.manage |
GET /marketing/links | marketing.metrics.read |
POST /marketing/links | marketing.link.manage |
DELETE /marketing/links/{link_id} | marketing.link.manage |
GET /marketing/overview | marketing.overview.read |
GET /marketing/settings | marketing.link.manage |
PUT /marketing/settings | marketing.link.manage |
GET /marketing/summary | marketing.metrics.read |
Parameters and schemas per endpoint
GET /api/v1/marketing/accounts
Section titled “GET /api/v1/marketing/accounts”Available Accounts — The accounts/properties/sites the caller’s connection can reach for source.
Permission: marketing.link.manage
Query parameters:
| Name | Type | Meaning |
|---|---|---|
source | MarketingSource · required | |
website_id | string | Required for a source whose credential is per website (rankmath); ignored by every other source. |
Returns: 200 · AccountsResponse
GET /api/v1/marketing/clients
Section titled “GET /api/v1/marketing/clients”Linked Clients — The clients that actually have a source linked, with which sources and how each is doing.
Permission: marketing.metrics.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
limit | integer | (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:
| Name | Type | Meaning |
|---|---|---|
link_id | string · required | |
kind | string · required | |
range_days | integer | (default 30) |
period | string | The 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:
| Name | Type | Meaning |
|---|---|---|
range_days | integer | (default 30) |
period | string | The 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
GET /api/v1/marketing/links
Section titled “GET /api/v1/marketing/links”List Links
Permission: marketing.metrics.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
company_id | string · required |
Returns: 200 · app__modules__marketing__schemas__LinkRead[]
POST /api/v1/marketing/links
Section titled “POST /api/v1/marketing/links”Create Link
Permission: marketing.link.manage
Request body: app__modules__marketing__schemas__LinkCreate
Returns: 201 · app__modules__marketing__schemas__LinkRead
DELETE /api/v1/marketing/links/{link_id}
Section titled “DELETE /api/v1/marketing/links/{link_id}”Unlink
Permission: marketing.link.manage
Path parameters: link_id
Returns: 204
GET /api/v1/marketing/overview
Section titled “GET /api/v1/marketing/overview”Overview — The morning-coffee grid: one row per linked client, from stored data, server-sorted.
Permission: marketing.overview.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
range_days | integer | (default 30) |
sort | string | company_name | sessions | clicks | position | cost | conversions (- = desc) |
period | string | The 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 /api/v1/marketing/settings
Section titled “GET /api/v1/marketing/settings”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
PUT /api/v1/marketing/settings
Section titled “PUT /api/v1/marketing/settings”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
GET /api/v1/marketing/summary
Section titled “GET /api/v1/marketing/summary”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:
| Name | Type | Meaning |
|---|---|---|
range_days | integer | (default 30) |
limit | integer | (default 5) |
period | string | The 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
google_ads
Section titled “google_ads”| Endpoint | Permission |
|---|---|
GET /google-ads/accounts | google_ads.account.read |
POST /google-ads/accounts | google_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-groups | google_ads.account.read |
POST /google-ads/accounts/{account_id}/ad-groups | google_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}/ads | google_ads.account.read |
POST /google-ads/accounts/{account_id}/ads | google_ads.campaign.write |
PATCH /google-ads/accounts/{account_id}/ads | google_ads.campaign.write |
POST /google-ads/accounts/{account_id}/budgets | google_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}/campaigns | google_ads.account.read |
POST /google-ads/accounts/{account_id}/campaigns | google_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}/changes | google_ads.account.read |
GET /google-ads/accounts/{account_id}/conversions | google_ads.account.read |
GET /google-ads/accounts/{account_id}/decisions | google_ads.account.read |
POST /google-ads/accounts/{account_id}/decisions | google_ads.policy.manage |
DELETE /google-ads/accounts/{account_id}/decisions/{decision_id} | google_ads.policy.manage |
GET /google-ads/accounts/{account_id}/devices | google_ads.account.read |
GET /google-ads/accounts/{account_id}/geo | google_ads.account.read |
POST /google-ads/accounts/{account_id}/keyword-ideas | google_ads.account.read |
GET /google-ads/accounts/{account_id}/keywords | google_ads.account.read |
POST /google-ads/accounts/{account_id}/keywords | google_ads.keyword.write |
PATCH /google-ads/accounts/{account_id}/keywords | google_ads.keyword.write |
POST /google-ads/accounts/{account_id}/keywords/remove | google_ads.keyword.write |
POST /google-ads/accounts/{account_id}/negative-lists | google_ads.negative.write |
DELETE /google-ads/accounts/{account_id}/negative-lists/{shared_set_id} | google_ads.negative.write |
GET /google-ads/accounts/{account_id}/negatives | google_ads.account.read |
POST /google-ads/accounts/{account_id}/negatives | google_ads.negative.write |
POST /google-ads/accounts/{account_id}/negatives/remove | google_ads.negative.write |
GET /google-ads/accounts/{account_id}/policy | google_ads.policy.manage |
PUT /google-ads/accounts/{account_id}/policy | google_ads.policy.manage |
DELETE /google-ads/accounts/{account_id}/policy | google_ads.policy.manage |
POST /google-ads/accounts/{account_id}/query | google_ads.query.run |
GET /google-ads/accounts/{account_id}/recommendations | google_ads.account.read |
GET /google-ads/accounts/{account_id}/search-terms | google_ads.account.read |
GET /google-ads/accounts/{account_id}/snapshot | google_ads.account.read |
GET /google-ads/accounts/{account_id}/trend | google_ads.account.read |
POST /google-ads/accounts/{account_id}/verify | google_ads.settings.manage |
GET /google-ads/accounts/available | google_ads.settings.manage |
GET /google-ads/policy | google_ads.policy.manage |
PUT /google-ads/policy | google_ads.policy.manage |
DELETE /google-ads/policy | google_ads.policy.manage |
GET /google-ads/settings | google_ads.settings.manage |
PUT /google-ads/settings | google_ads.settings.manage |
Parameters and schemas per endpoint
GET /api/v1/google-ads/accounts
Section titled “GET /api/v1/google-ads/accounts”List Google Ads Accounts — Every linked Google Ads account this caller may see — start here.
Permission: google_ads.account.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
company_id | string | |
active_only | boolean | (default false) |
Returns: 200 · GoogleAdsAccountRead[]
POST /api/v1/google-ads/accounts
Section titled “POST /api/v1/google-ads/accounts”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:
| Name | Type | Meaning |
|---|---|---|
campaigns | string[] | |
include_removed | boolean | (default false) |
period | string | A 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_from | string | YYYY-MM-DD, inclusive. |
date_to | string | YYYY-MM-DD, inclusive. |
status | string | Only rows with this Google status: ENABLED, PAUSED or REMOVED. REMOVED implies include_removed, because a filter that always answers nothing is not a filter. |
q | string | Free 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. |
limit | integer | How 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. |
offset | integer | Where 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:
| Name | Type | Meaning |
|---|---|---|
validate_only | boolean | Check 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:
| Name | Type | Meaning |
|---|---|---|
validate_only | boolean | Check 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:
| Name | Type | Meaning |
|---|---|---|
campaigns | string[] | |
period | string | A 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_from | string | YYYY-MM-DD, inclusive. |
date_to | string | YYYY-MM-DD, inclusive. |
status | string | Only rows with this Google status: ENABLED, PAUSED or REMOVED. REMOVED implies include_removed, because a filter that always answers nothing is not a filter. |
q | string | Free 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. |
limit | integer | How 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. |
offset | integer | Where 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:
| Name | Type | Meaning |
|---|---|---|
validate_only | boolean | Check 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:
| Name | Type | Meaning |
|---|---|---|
campaigns | string[] | Filter by campaign name, case-insensitive substring match. |
include_removed | boolean | Include 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) |
period | string | A 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_from | string | YYYY-MM-DD, inclusive. |
date_to | string | YYYY-MM-DD, inclusive. |
status | string | Only rows with this Google status: ENABLED, PAUSED or REMOVED. REMOVED implies include_removed, because a filter that always answers nothing is not a filter. |
q | string | Free 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. |
limit | integer | How 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. |
offset | integer | Where 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:
| Name | Type | Meaning |
|---|---|---|
validate_only | boolean | Check 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:
| Name | Type | Meaning |
|---|---|---|
period | string | A 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_from | string | YYYY-MM-DD, inclusive. |
date_to | string | YYYY-MM-DD, inclusive. |
q | string | Free 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. |
limit | integer | How 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. |
offset | integer | Where 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:
| Name | Type | Meaning |
|---|---|---|
period | string | A 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_from | string | YYYY-MM-DD, inclusive. |
date_to | string | YYYY-MM-DD, inclusive. |
q | string | Free 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. |
limit | integer | How 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. |
offset | integer | Where 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:
| Name | Type | Meaning |
|---|---|---|
subject_type | string | |
decision | string | |
include_withdrawn | boolean | (default false) |
limit | integer | (default 50) |
offset | integer | (default 0) |
count | boolean | Set 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:
| Name | Type | Meaning |
|---|---|---|
campaigns | string[] | |
period | string | A 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_from | string | YYYY-MM-DD, inclusive. |
date_to | string | YYYY-MM-DD, inclusive. |
q | string | Free 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. |
limit | integer | How 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. |
offset | integer | Where 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:
| Name | Type | Meaning |
|---|---|---|
campaigns | string[] | |
period | string | A 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_from | string | YYYY-MM-DD, inclusive. |
date_to | string | YYYY-MM-DD, inclusive. |
q | string | Free 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. |
limit | integer | How 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. |
offset | integer | Where 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:
| Name | Type | Meaning |
|---|---|---|
campaigns | string[] | |
include_removed | boolean | (default false) |
period | string | A 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_from | string | YYYY-MM-DD, inclusive. |
date_to | string | YYYY-MM-DD, inclusive. |
status | string | Only rows with this Google status: ENABLED, PAUSED or REMOVED. REMOVED implies include_removed, because a filter that always answers nothing is not a filter. |
q | string | Free 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. |
limit | integer | How 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. |
offset | integer | Where 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:
| Name | Type | Meaning |
|---|---|---|
validate_only | boolean | Check 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:
| Name | Type | Meaning |
|---|---|---|
q | string | Free 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. |
limit | integer | How 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. |
offset | integer | Where 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:
| Name | Type | Meaning |
|---|---|---|
q | string | Free 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. |
limit | integer | How 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. |
offset | integer | Where 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:
| Name | Type | Meaning |
|---|---|---|
campaigns | string[] | |
min_cost | number | Only terms that cost at least this, in account currency. |
min_clicks | integer | |
period | string | A 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_from | string | YYYY-MM-DD, inclusive. |
date_to | string | YYYY-MM-DD, inclusive. |
q | string | Free 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. |
limit | integer | How 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. |
offset | integer | Where 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:
| Name | Type | Meaning |
|---|---|---|
period | string | A 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_from | string | YYYY-MM-DD, inclusive. |
date_to | string | YYYY-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:
| Name | Type | Meaning |
|---|---|---|
compare | string | What to compare against: ‘year’ (the same period a year earlier, the default) or ‘previous’ (the period immediately before). |
period | string | A 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_from | string | YYYY-MM-DD, inclusive. |
date_to | string | YYYY-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
GET /api/v1/google-ads/accounts/available
Section titled “GET /api/v1/google-ads/accounts/available”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 /api/v1/google-ads/policy
Section titled “GET /api/v1/google-ads/policy”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
PUT /api/v1/google-ads/policy
Section titled “PUT /api/v1/google-ads/policy”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
DELETE /api/v1/google-ads/policy
Section titled “DELETE /api/v1/google-ads/policy”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 /api/v1/google-ads/settings
Section titled “GET /api/v1/google-ads/settings”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
PUT /api/v1/google-ads/settings
Section titled “PUT /api/v1/google-ads/settings”Save Google Ads Settings
Permission: google_ads.settings.manage
Request body: GoogleAdsSettingsWrite
Returns: 200 · GoogleAdsSettingsRead
google_tag_manager
Section titled “google_tag_manager”| Endpoint | Permission |
|---|---|
GET /gtm/containers | google_tag_manager.container.read |
POST /gtm/containers | google_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}/conversions | google_tag_manager.container.read |
POST /gtm/containers/{container_id}/conversions | google_tag_manager.tag.write |
GET /gtm/containers/{container_id}/snippet | google_tag_manager.container.read |
GET /gtm/containers/{container_id}/status | google_tag_manager.container.read |
GET /gtm/containers/{container_id}/tags | google_tag_manager.container.read |
POST /gtm/containers/{container_id}/tags | google_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}/triggers | google_tag_manager.container.read |
POST /gtm/containers/{container_id}/triggers | google_tag_manager.tag.write |
DELETE /gtm/containers/{container_id}/triggers/{trigger_id} | google_tag_manager.tag.write |
GET /gtm/containers/{container_id}/variables | google_tag_manager.container.read |
POST /gtm/containers/{container_id}/variables | google_tag_manager.tag.write |
DELETE /gtm/containers/{container_id}/variables/{variable_id} | google_tag_manager.tag.write |
POST /gtm/containers/{container_id}/verify | google_tag_manager.settings.manage |
GET /gtm/containers/{container_id}/versions | google_tag_manager.container.read |
POST /gtm/containers/{container_id}/versions | google_tag_manager.tag.write |
POST /gtm/containers/{container_id}/versions/{version_id}/publish | google_tag_manager.version.publish |
GET /gtm/containers/{container_id}/workspaces | google_tag_manager.container.read |
GET /gtm/containers/available | google_tag_manager.settings.manage |
GET /gtm/settings | google_tag_manager.settings.manage |
PUT /gtm/settings | google_tag_manager.settings.manage |
Parameters and schemas per endpoint
GET /api/v1/gtm/containers
Section titled “GET /api/v1/gtm/containers”List Gtm Containers — Every linked Tag Manager container this caller may see — start here.
Permission: google_tag_manager.container.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
company_id | string | |
active_only | boolean | (default false) |
Returns: 200 · GtmContainerRead[]
POST /api/v1/gtm/containers
Section titled “POST /api/v1/gtm/containers”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 /api/v1/gtm/containers/{container_id}
Section titled “GET /api/v1/gtm/containers/{container_id}”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:
| Name | Type | Meaning |
|---|---|---|
workspace_id | string |
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:
| Name | Type | Meaning |
|---|---|---|
workspace_id | string |
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:
| Name | Type | Meaning |
|---|---|---|
workspace_id | string |
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:
| Name | Type | Meaning |
|---|---|---|
workspace_id | string |
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:
| Name | Type | Meaning |
|---|---|---|
workspace_id | string |
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:
| Name | Type | Meaning |
|---|---|---|
workspace_id | string |
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:
| Name | Type | Meaning |
|---|---|---|
workspace_id | string |
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:
| Name | Type | Meaning |
|---|---|---|
workspace_id | string |
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:
| Name | Type | Meaning |
|---|---|---|
workspace_id | string |
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:
| Name | Type | Meaning |
|---|---|---|
workspace_id | string |
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:
| Name | Type | Meaning |
|---|---|---|
workspace_id | string |
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[]
GET /api/v1/gtm/containers/available
Section titled “GET /api/v1/gtm/containers/available”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 /api/v1/gtm/settings
Section titled “GET /api/v1/gtm/settings”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
PUT /api/v1/gtm/settings
Section titled “PUT /api/v1/gtm/settings”Save Gtm Settings
Permission: google_tag_manager.settings.manage
Request body: GtmSettingsWrite
Returns: 200 · GtmSettingsRead
reporting
Section titled “reporting”| Endpoint | Permission |
|---|---|
GET /reporting/companies/{company_id}/profile | reporting.profile.manage |
PUT /reporting/companies/{company_id}/profile | reporting.profile.manage |
GET /reporting/reports | reporting.report.read |
GET /reporting/reports/{report_id} | reporting.report.read |
DELETE /reporting/reports/{report_id} | reporting.report.write |
PUT /reporting/reports/{report_id}/narrative | reporting.report.write |
GET /reporting/reports/{report_id}/pdf | reporting.report.read |
GET /reporting/reports/{report_id}/preview | reporting.report.read |
POST /reporting/reports/{report_id}/publish | reporting.report.send |
POST /reporting/reports/{report_id}/rewrite | reporting.report.write |
POST /reporting/reports/{report_id}/send | reporting.report.send |
POST /reporting/reports/generate | reporting.report.write |
POST /reporting/reports/generate-batch | reporting.report.write |
GET /reporting/settings | reporting.profile.manage |
PUT /reporting/settings | reporting.settings.manage |
GET /reporting/templates | reporting.profile.manage |
POST /reporting/templates | reporting.settings.manage |
PUT /reporting/templates/{template_id} | reporting.settings.manage |
DELETE /reporting/templates/{template_id} | reporting.settings.manage |
GET /reporting/templates/designs/{design}/source | reporting.settings.manage |
POST /reporting/templates/preview | reporting.settings.manage |
GET /reporting/templates/sections | reporting.settings.manage |
GET /reporting/tones | reporting.profile.manage |
POST /reporting/tones | reporting.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
GET /api/v1/reporting/reports
Section titled “GET /api/v1/reporting/reports”List Reports
Permission: reporting.report.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
company_id | string | |
audience | ReportAudience | |
limit | integer | (default 50) |
offset | integer | (default 0) |
count | boolean | (default true) |
Returns: 200 · ReportList
GET /api/v1/reporting/reports/{report_id}
Section titled “GET /api/v1/reporting/reports/{report_id}”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:
| Name | Type | Meaning |
|---|---|---|
published | boolean | (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
POST /api/v1/reporting/reports/generate
Section titled “POST /api/v1/reporting/reports/generate”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 /api/v1/reporting/settings
Section titled “GET /api/v1/reporting/settings”Get Settings
Permission: reporting.profile.manage
Returns: 200 · ReportingSettingsRead
PUT /api/v1/reporting/settings
Section titled “PUT /api/v1/reporting/settings”Save Settings
Permission: reporting.settings.manage
Request body: ReportingSettingsWrite
Returns: 200 · ReportingSettingsRead
GET /api/v1/reporting/templates
Section titled “GET /api/v1/reporting/templates”List Templates
Permission: reporting.profile.manage
Query parameters:
| Name | Type | Meaning |
|---|---|---|
audience | ReportAudience |
Returns: 200 · ReportTemplateRead[]
POST /api/v1/reporting/templates
Section titled “POST /api/v1/reporting/templates”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
POST /api/v1/reporting/templates/preview
Section titled “POST /api/v1/reporting/templates/preview”Preview Template — Render an unsaved template — the editor’s live preview.
Permission: reporting.settings.manage
Request body: ReportTemplatePreviewRequest
Returns: 200
GET /api/v1/reporting/templates/sections
Section titled “GET /api/v1/reporting/templates/sections”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[]
GET /api/v1/reporting/tones
Section titled “GET /api/v1/reporting/tones”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[]
POST /api/v1/reporting/tones
Section titled “POST /api/v1/reporting/tones”Create Tone
Permission: reporting.settings.manage
Request body: ReportToneWrite
Returns: 201 · ReportToneRead
PUT /api/v1/reporting/tones/{tone_id}
Section titled “PUT /api/v1/reporting/tones/{tone_id}”Update Tone
Permission: reporting.settings.manage
Path parameters: tone_id
Request body: ReportToneWrite
Returns: 200 · ReportToneRead
DELETE /api/v1/reporting/tones/{tone_id}
Section titled “DELETE /api/v1/reporting/tones/{tone_id}”Delete Tone
Permission: reporting.settings.manage
Path parameters: tone_id
Returns: 204