Glitch MCP
Everything the MCP server can do, plus listing reports and exporting the audit trail, over plain HTTPS. The base URL is https://api.glitchads.ai/v1, and the machine readable schema is at GET /v1/openapi.json.
| Authentication | Authorization: Bearer <token> on every request: an API key (glads_live_…) or an OAuth access token. Never in a query string. |
| Content type | JSON in, JSON out. Content-Type: application/json on any request carrying a body. |
| Organization context | Explicit in the path, /organizations/{org}/…, where {org} is the slug. API keys may use _ for their own organization; OAuth tokens cover all the user's organizations and must name one. Your membership is checked on every request, and a slug the credential cannot act in, or one that does not exist, answers org_access_denied. |
| Money and dates | Amounts are numbers in the organization currency with up to two decimal places, so 40.00 arrives as 40.0. A campaign budget is a daily amount in whole units: a budget you send is rounded to a whole number. Target CPA keeps cents. Dates as YYYY-MM-DD, timestamps ISO 8601 in UTC. |
| Pagination | ?limit= defaults to 20 and accepts 1 to 100; anything else is a validation_error. Lists carry total and next_cursor, null when done; pass it back as ?cursor=. The organization list, the competitors, an account's campaigns and conversion actions, and the audit export return everything in one response. |
| Platforms | Google Ads unless you say otherwise. platform=meta reads the organization's Meta ad account on the campaigns list, a campaign's detail (by its Meta slug), performance and the action log; every other endpoint is Google Ads only. Where an ad account's numbers are not in Glitch yet, the response says so in availability, with status available, partial or not_available, rather than answering zero. |
| Writes preview first | Endpoints that change a running campaign take dry_run, which defaults to true: the first call returns a preview and changes nothing. Execute with "dry_run": false. An executed write that finishes at once returns an action_id that undo reverses; work that continues in the background returns a task_tracking_id, and a state change returns both. Campaign creation, ad creation, report generation and strategy generation have no dry_run and execute immediately. Creation, reports and strategies only produce drafts and documents, but a new ad on a live campaign goes to Google Ads and starts serving, and it returns no action_id: remove an ad in the dashboard. |
| Asynchronous work | Campaign creation, ad creation, report generation and strategy generation answer 202 Accepted with a task_tracking_id; an executed state change answers 200 and carries one too. Poll GET /task-tracking/{id}. |
| Errors and limits | One structured envelope, X-RateLimit-* headers, 429 with Retry-After, and a daily write quota shared by the organization. The details are on the errors page. |
The version is in the path. Changes within v1 are additive: new fields, new optional parameters, new endpoints. Read responses so that an unfamiliar field is ignored rather than fatal.
Anything that would break a working client arrives as a new prefix alongside this one, not as a change inside v1. /v1/openapi.json is generated from the running service, so its endpoints, fields and types are always current. Most accepted values and limits are not in it as enums; this page lists them.
/organizations
The organizations this credential can act in, each with your role, plan and currency. Exactly one for an API key; every organization the user belongs to for an OAuth token. With an OAuth token, Glitch AdOps staff also see every client organization assigned to their team, read only: those rows carry access adops_team and no role. adops_team names the team responsible for an organization on any row that has one, so results can be grouped by team.
// GET /v1/organizations
{
"organizations": [
{ "slug": "acme", "name": "Acme Ltd", "role": "owner", "plan": "SMB", "currency": "EUR",
"access": "membership", "adops_team": null },
{ "slug": "bright-dental", "name": "Bright Dental", "role": null, "plan": "SMB", "currency": "GBP",
"access": "adops_team", "adops_team": { "name": "Realglitch" } }
],
"total": 2,
"next_cursor": null
}
/organizations/{org}
Plan, currency, spend limits and ad accounts. max_spend is the organization's monthly spend cap, null when none is set, and api_budget_change_limit_pct is the most one call may move a budget. ad_accounts lists the Google Ads accounts the organization's campaigns use or its members have selected, each linked, pending or not_linked. access says how this credential reaches the organization, membership or adops_team, and adops_team names the Glitch team responsible for it, null when there is none.
// GET /v1/organizations/acme
{
"slug": "acme", "name": "Acme Ltd", "plan": "SMB", "currency": "EUR",
"max_spend": 5000.00, "api_budget_change_limit_pct": 25,
"ad_accounts": [ { "id": "8932210042", "platform": "google", "status": "linked" } ],
"access": "membership", "adops_team": null
}
/organizations/{org}/business-profile
The business context Glitch has learned: its URL, brand name, description, products, ideal customer profiles, competitors and trust signals, the organization's currency, the locations it serves customers in, its monthly ad_budgets per platform, and the intel Glitch has collected about it from outside its own data, each kind marked stale once it is out of date. Answers not_found until the organization has a business profile.
/organizations/{org}/competitors
The business's saved competitors, closest first: each one's id, name, website, closeness from 0 to 1, and what Glitch's research found (industry, audience, location, size, headquarters, founding year, social profiles and a description). All of them come in one response. Needs organizations:read.
/credential
What the presented credential can do: scopes, the scopes it can use in at least one organization, and organization, the slug an API key is bound to, null for an OAuth connection that acts in every organization its user belongs to. Needs no scope.
/organizations/{org}/campaigns
List campaigns with their seven day numbers. Each row says its platform, and metrics_status says whether its numbers are in Glitch yet, available or not_available, with metrics_message saying why when they are not. A Google campaign's type is prospecting, brand, competitor, icp, hipo or location; a Meta campaign's is scaling, testing or retargeting, with its leads counted as conversions.
| Parameter | Type | Notes |
|---|---|---|
platform |
Typestring | google, the default, or meta. |
account_id |
Typestring | Filter to one ad account. |
state |
Typestring | live, paused, draft or error; on Meta live, paused, archived or deleted. |
limit, cursor |
Type | Pagination. |
curl "https://api.glitchads.ai/v1/organizations/acme/campaigns?state=live" \
-H "Authorization: Bearer $GLITCH_API_KEY"
// 200
{
"campaigns": [
{ "platform": "google", "slug": "acme-prospecting", "name": "Prospecting", "type": "prospecting",
"state": "live", "budget": 40.00, "spend_7d": 212.40, "conversions_7d": 10, "cpa_7d": 21.24,
"created_at": "2026-05-02T10:11:04Z", "metrics_status": "available", "metrics_message": null },
{ "platform": "google", "slug": "acme-brand", "name": "Brand", "type": "brand",
"state": "live", "budget": 20.00, "spend_7d": 120.20, "conversions_7d": 19, "cpa_7d": 6.33,
"created_at": "2026-04-14T09:02:51Z", "metrics_status": "available", "metrics_message": null }
],
"total": 2, "next_cursor": null
}
/organizations/{org}/campaigns/{slug}
Full campaign detail. keywords_count includes paused keywords. negative_keywords_count counts the active exclusions on the campaign and on its ads, including ones added through this API. last_synced_at is when the performance numbers were last refreshed, and is null for a campaign that has never been in Google Ads. A Meta campaign's slug answers with its objective, budget and ad sets instead.
// GET /v1/organizations/acme/campaigns/acme-prospecting
{
"slug": "acme-prospecting", "name": "Prospecting", "type": "prospecting", "state": "live",
"spend_7d": 212.40, "conversions_7d": 10, "cpa_7d": 21.24, "created_at": "2026-05-02T10:11:04Z",
"budget": 40.00, "target_cpa": 15.00, "goal": "prospecting", "website_url": "https://acme.co",
"ads_count": 3, "keywords_count": 84, "negative_keywords_count": 31,
"last_synced_at": "2026-07-08T06:00:12Z"
}
/organizations/{org}/campaigns
Generate a complete campaign from a website. Always a draft: going live is a separate state call. Asynchronous. The draft starts from the business profile's description and target locations, in the Google Ads account selected for the organization. It is refused while the business profile is still being analysed, or when no Google Ads account is selected for the organization.
The response names the new draft's campaign slug and its url, the campaign's page in Glitch. The link opens the campaign in its own organization, even for someone who last worked in another one.
| Parameter | Type | Notes |
|---|---|---|
website_url |
Typestring, required | The site to build from. |
type |
Typestring | prospecting, the default, or brand. A brand campaign's keywords are also added as negatives to the account's unpublished draft campaigns, so those never compete for the brand's searches; campaigns already live get the same protection from the brand-leak check once the brand campaign is live. The other campaign types Glitch builds are not creatable here: hipo, location and icp come from applying a recommendation, and competitor needs a competitor selection this body cannot express. |
name |
Typestring | Kept as sent. Left out, Glitch names the campaign from the business, its type and its locations once generation finishes. |
budget |
Typenumber | Daily budget for the draft, in whole units. Checked against the organization's monthly spend cap. |
curl -X POST https://api.glitchads.ai/v1/organizations/acme/campaigns \
-H "Authorization: Bearer $GLITCH_API_KEY" -H "Content-Type: application/json" \
-d '{"website_url":"https://acme.co","type":"brand","budget":25.00}'
// 202
{ "task_tracking_id": 5521, "status": "queued", "campaign": "Xk2pQ9LmT4",
"url": "https://app.glitchads.ai/campaigns/google/Xk2pQ9LmT4?org=acme" }
/organizations/{org}/campaigns/{slug}/ads
Add a new ad (a Google ad group) to an existing campaign, generated from a landing page. Asynchronous, with no dry_run. On a live campaign the finished ad is pushed to Google Ads and starts serving; on a draft it waits with the campaign. It returns the new ad_id but no action_id: remove an ad in the dashboard. Imported external campaigns and competitor campaigns are not accepted, and neither is a campaign with no linked Google Ads account or an incomplete Google connection.
| Parameter | Type | Notes |
|---|---|---|
url |
Typestring, required | The landing page the ad is generated from. Must be on the campaign website's domain (or a subdomain of it). |
name |
Typestring | Numbered after the existing ads (Ad 2, Ad 3) if you leave it out. |
curl -X POST https://api.glitchads.ai/v1/organizations/acme/campaigns/acme-prospecting/ads \
-H "Authorization: Bearer $GLITCH_API_KEY" -H "Content-Type: application/json" \
-d '{"url":"https://acme.co/pricing"}'
// 202
{ "task_tracking_id": 5533, "status": "queued", "ad_id": 912 }
/organizations/{org}/campaigns/{slug}
Update name, budget or target CPA. Only the fields you send change. One call may move a budget by at most api_budget_change_limit_pct, 25 by default; a breach answers budget_cap_exceeded with allowed_min and allowed_max. Where the organization has a max_spend, the daily budgets of all its campaigns, drafts and paused ones included, times 30 must stay within it; a breach answers budget_cap_exceeded with monthly_cap and allowed_max_daily. A dry run reports either breach as within_limits: false instead.
An executed change is all or nothing: Google Ads and Glitch both take it or neither does, and its action_id undoes it. A call that sends only values the campaign already has is refused with validation_error, because there would be nothing to undo. The executed call can also be refused before anything is sent, and then nothing changes. If another change to the campaign is still being sent to Google Ads, the answer is sync_conflict: retry when it has finished. If the campaign was changed by something else while the call was being made, the answer is also sync_conflict: read the campaign again, then resend. If one of the campaign's ads has a funnel stage with fewer than 3 headlines or 2 descriptions, the answer is validation_error naming the ad and the stage: add the copy in the dashboard, then resend. A call made while the campaign's Google connection cannot be used is a validation_error too: reconnect Google in the dashboard.
| Parameter | Type | Notes |
|---|---|---|
name |
Typestring | New name. |
budget |
Typenumber | New daily budget, in whole units. |
target_cpa |
Typenumber | New target CPA. |
dry_run |
Typeboolean | Defaults to true. |
# preview: dry_run defaults to true, so this changes nothing
curl -X PATCH https://api.glitchads.ai/v1/organizations/acme/campaigns/acme-prospecting \
-H "Authorization: Bearer $GLITCH_API_KEY" -H "Content-Type: application/json" \
-d '{"budget":48.00}'
// 200 (dry run)
{ "dry_run": true, "would_change": { "budget": { "from": 40.00, "to": 48.00 } },
"within_limits": true }
// 200 (executed, with "dry_run": false)
{ "applied": true, "action_id": "act_9077",
"changed": { "budget": { "from": 40.00, "to": 48.00 } } }
/organizations/{org}/campaigns/{slug}/state
Pause, resume or launch. An executed change syncs to Google Ads in the background through the same pipeline as the dashboard, and answers straight away with a task_tracking_id to poll and an action_id to undo. The campaign must be linked to a Google Ads account the credential's user can access. A campaign that has been published can't be set back to draft: pause it instead. Asking for it is a validation_error.
Launching needs a paid plan: an organization on the free plan is answered payment_required. It also refuses a campaign with no budget, and one whose budget would take the organization over its monthly spend cap. It also checks Google Ads for changes made there since the last sync and answers sync_conflict if it finds any; the check covers syncs in the last 30 days, and when Google Ads cannot be read the launch is refused with the same error. A campaign's first launch also runs the checks the dashboard runs before approval: a website URL, a location or radius, valid sitelinks and callouts, at least one ad, valid headlines and descriptions on every ad, and a keyword on every ad. The error names the first thing missing. A dry run runs the same checks and reports the first one that fails as blocked_by, which is null when the transition would go through; the sync conflict check runs only on the executed call.
| Parameter | Type | Notes |
|---|---|---|
state |
Typestring, required | live, paused or draft. |
dry_run |
Typeboolean | Defaults to true. |
// 200 (dry run)
{ "dry_run": true,
"would_change": { "state": { "from": "draft", "to": "live" } },
"blocked_by": "Set a daily budget greater than 0 before launching." }
// 200 (executed)
{ "accepted": true, "task_tracking_id": 6102, "action_id": "act_9120" }
/organizations/{org}/campaigns/{slug}/keywords
The keywords on a campaign, with thirty day performance.
| Parameter | Type | Notes |
|---|---|---|
state |
Typestring | enabled or paused. |
sort |
Typestring | cost by default, or clicks or conversions. |
limit, cursor |
Type | Pagination. |
// 200
{
"keywords": [
{ "text": "ppc management", "match_type": "phrase", "state": "enabled",
"clicks_30d": 140, "cost_30d": 168.20, "conversions_30d": 11, "avg_cpc": 1.20 }
],
"total": 84, "next_cursor": "djE6MjA="
}
/organizations/{org}/campaigns/{slug}/negative-keywords
Exclude terms across the campaign. On a live campaign the exclusions go to Google Ads first; on a draft they wait with the campaign.
An executed call is all or nothing: Google Ads and Glitch both take every term or neither does, and its action_id undoes it. Terms the campaign already excludes are left out; a call where every term is already excluded is refused with validation_error. If another change to the campaign is still being sent to Google Ads, or the campaign changed while the call was being made, the answer is sync_conflict: retry once it has finished, or resend. If one of the campaign's ads has a funnel stage short of headlines or descriptions, or the campaign's Google connection cannot be used, the answer is validation_error saying what to fix. Nothing changes when a call is refused.
| Parameter | Type | Notes |
|---|---|---|
keywords |
Typestring[], required | 1 to 100 terms, each up to 80 characters. |
match_type |
Typestring | phrase by default, or exact or broad. |
dry_run |
Typeboolean | Defaults to true. |
curl -X POST https://api.glitchads.ai/v1/organizations/acme/campaigns/acme-prospecting/negative-keywords \
-H "Authorization: Bearer $GLITCH_API_KEY" -H "Content-Type: application/json" \
-d '{"keywords":["free ads course","ppc jobs"]}'
// 200 (dry run, the default)
{ "dry_run": true, "match_type": "phrase",
"would_apply": [ { "op": "add_negative_keyword", "term": "free ads course", "match_type": "phrase" },
{ "op": "add_negative_keyword", "term": "ppc jobs", "match_type": "phrase" } ] }
// 200 (executed, with "dry_run": false)
{ "applied": true, "action_id": "act_9124", "added": 2, "match_type": "phrase" }
/organizations/{org}/performance
Metrics over a date range, grouped and compared against the previous period. availability says which ad accounts the numbers cover; each by_campaign row also carries clicks, impressions, CTR, CPC, ROAS, conversion value and its search impression share with the share lost to rank and to budget.
| Parameter | Type | Notes |
|---|---|---|
platform |
Typestring | google, the default, or meta, which reads the Meta ad account the same way, counting leads as conversions. |
from, to |
Typedate | By default, the seven days up to the latest day with data, usually yesterday. to alone gives the seven days ending then; from alone runs to the latest day with data. |
campaign |
Typestring | Restrict to one campaign slug, Google only; an unknown slug is not_found. |
group_by |
Typestring | campaign by default, returned as by_campaign, highest spend first. day returns by_day rows with a date, and week returns by_week rows with a week_start, a Monday. On Google also device, location (the region of their country the people were in) and conversion_action; on Meta also ad_set, ad, age, gender, location and placement, and not campaign. Each grouping comes back as by_ and its name. Each row carries spend, conversions and CPA. |
compare |
Typeboolean | true by default: vs_previous gives the change against the period of the same length just before, each value null when that period had no data. |
curl "https://api.glitchads.ai/v1/organizations/acme/performance?from=2026-07-01&to=2026-07-07" \
-H "Authorization: Bearer $GLITCH_API_KEY"
// 200
{
"platform": "google",
"period": { "from": "2026-07-01", "to": "2026-07-07" },
"availability": { "status": "available", "message": null, "unavailable_accounts": [] },
"summary": { "spend": 412.10, "impressions": 18740, "clicks": 1143, "ctr": 0.061,
"conversions": 31, "cpa": 13.29, "roas": 4.2,
"vs_previous": { "spend": "+4%", "conversions": "+12%", "cpa": "-8%" } },
"by_campaign": [
{ "slug": "acme-prospecting", "spend": 212.40, "clicks": 702, "impressions": 11230,
"conversions": 10, "cpa": 21.24, "search_impression_share": 0.41 },
{ "slug": "acme-brand", "spend": 120.20, "clicks": 318, "impressions": 2104,
"conversions": 19, "cpa": 6.33, "search_impression_share": 0.92 }
]
}
/organizations/{org}/search-terms
What people actually searched, dearest first, with availability as on performance.
| Parameter | Type | Notes |
|---|---|---|
campaign |
Typestring | Restrict to one campaign slug; an unknown slug is not_found. |
days |
Typeinteger | 1 to 90, 30 by default, counted back from the latest day with data. |
min_cost |
Typenumber | Only terms costing at least this much. |
limit, cursor |
Type | Pagination. |
// 200
{
"search_terms": [
{ "term": "ppc management for dentists", "campaign": "acme-prospecting",
"clicks": 31, "cost": 42.10, "conversions": 4 },
{ "term": "free ads course", "campaign": "acme-prospecting",
"clicks": 22, "cost": 18.60, "conversions": 0 }
],
"availability": { "status": "available", "message": null, "unavailable_accounts": [] },
"total": 412, "next_cursor": "djE6MjA="
}
The reads above cover the campaigns Glitch runs for the organization. These cover every campaign in one of its Google Ads accounts, whoever made it, which is what an audit of the account needs. They all need performance:read.
{account_id} is the Google Ads customer id, with or without dashes, of an account the organization uses through its campaigns or a member's selection. Because they read campaigns the organization did not make, they answer only for an account no other organization uses: a shared account is refused with validation_error saying why, and an account the organization does not use is not_found. Each response carries availability; an account whose numbers are not in Glitch yet answers empty lists marked not_available, never zeros.
/organizations/{org}/ad-accounts/{account_id}/campaigns
Every campaign in the account that spent in the period or is enabled, most spent first: its type, status, serving status, bidding strategy, budget and budget period with Google's recommended_budget, its start date, glitch_campaign (the slug when Glitch runs it), its totals and search impression share over the period, and by_month. by_device gives the account's totals by device.
| Parameter | Type | Notes |
|---|---|---|
months |
Typeinteger | The last complete calendar months to cover, 1 to 12, 6 by default. |
/organizations/{org}/ad-accounts/{account_id}/keywords
Every keyword with traffic in the last 90 days, most spent first, with its quality score from 1 to 10 and its three parts (expected_ctr, ad_relevance, landing_page_experience, each null until Google rates it), and first_page_cpc.
| Parameter | Type | Notes |
|---|---|---|
limit, cursor |
Type | Pagination. |
/organizations/{org}/ad-accounts/{account_id}/search-terms
What people searched across every campaign in the account, most spent first.
| Parameter | Type | Notes |
|---|---|---|
days |
Typeinteger | 1 to 90, 90 by default. |
min_cost |
Typenumber | Only terms costing at least this much. |
limit, cursor |
Type | Pagination. |
/organizations/{org}/ad-accounts/{account_id}/landing-pages
The pages the account's ads sent people to over the last 90 days, per campaign, most spent first.
| Parameter | Type | Notes |
|---|---|---|
limit, cursor |
Type | Pagination. |
/organizations/{org}/ad-accounts/{account_id}/conversion-actions
Every conversion action in the account, the kind of result it counts, and how many it counted over the last 90 days, most counted first. All of them come in one response.
Twelve types can be read: weekly, performance, quarterly, custom, strategic, strategic_plan, intelligence, competitor, meta_weekly, meta_monthly, meta_quarterly and meta_audit. Report bodies come back as markdown.
Only weekly can be generated through the API today. The others are produced in Glitch, on a schedule or on request, and read back here.
/organizations/{org}/reports/{type}
List reports of a type, newest first. Each row carries the id, type and generated_at; read one to get its body.
/organizations/{org}/reports/{type}/latest
The most recent one, without listing first. not_found when there is none yet.
// GET /v1/organizations/acme/reports/weekly/latest
{
"id": "rep_2841", "type": "weekly", "generated_at": "2026-07-08T07:00:00Z",
"markdown": "# Weekly performance\n\nSpend 412.10 EUR (+4%)…"
}
/organizations/{org}/reports/{type}/{report_id}
One specific report.
/organizations/{org}/reports/{type}
Generate the report for the current period. Asynchronous, and it counts as a write. weekly only. An organization holds one report per period and type: generating again rebuilds that report in place, and it keeps its id.
The organization's Strategic Plan: its Google Ads strategy, written from its business profile, its performance, a live audit of its Google Ads account, keyword demand, and any documents you supply. The same plan the Glitch app produces, and it is also read as the strategic_plan report.
An organization has one Strategic Plan. Writing one takes minutes, so generation is asynchronous: it answers with a task_tracking_id to poll and the plan's id, and generating again rewrites the same plan under the same id. Plans come back as markdown.
/organizations/{org}/strategy
Write the Strategic Plan, or rewrite it. Asynchronous. Needs reports:generate. A request made while the plan is being written answers with the generation already under way. Send documents to put your own material in front of it: each carries a name and content as markdown or plain text, at most five, 20,000 characters each, and documents already uploaded to the organization are always included. The url opens the plan in Glitch once it is written.
Writing a plan changes nothing else: no campaign is touched, and the organization's business profile is left as it stands.
// POST /v1/organizations/acme/strategy
{ "documents": [{ "name": "Q4 joint impact plan", "content": "# Plan\n\nGrow paid search 30%." }] }
// 202 Accepted
{
"status": "queued", "strategy": "str_5137", "task_tracking_id": 90412,
"url": "https://app.glitchads.ai/reports/5137?org=acme"
}
/organizations/{org}/strategy
The organization's Strategic Plan as a list of one, or none before the first is written. The body stays on the detail read. Answers not_found until the organization has a business profile.
/organizations/{org}/strategy/{strategy_id}
The plan, by its id. While a first plan is still being written markdown is null; if it failed, error says why. A plan being rewritten answers with the text it has until the new one is done.
// GET /v1/organizations/acme/strategy/str_5137
{
"id": "str_5137", "status": "succeeded", "title": "Strategic Plan",
"created_at": "2026-09-20T09:14:02Z", "finished_at": "2026-09-20T09:21:47Z",
"url": "https://app.glitchads.ai/reports/5137?org=acme",
"markdown": "# Strategic Plan\n\nSpend where intent is…", "error": null
}
An organization can require a person to sign off every change made through a key or a connected app. An Owner or an Admin turns it on with Write Approval, on the My Account page. It is off unless somebody turns it on.
While it is on, a mutating call does not execute: it answers 202 with approval_required and a queued_recommendation_id. Somebody approves or rejects it in Glitch, on the Actions page. Approving runs the change through exactly the path the direct call would have taken, attributed to whoever asked for it, and the guardrails run again at that moment, so a change the organization's budget cap no longer allows fails rather than going through. Applying a recommendation is the one write that queues nothing new: the recommendation is already waiting, so queued_recommendation_id names it. Reads and dry_run previews are never held, and never counted against the write quota. Undo and detector runs are never held either, so a bad change can be reversed without waiting on a person; undo does use a write from the daily quota.
A queued change that nobody acts on for seven days is closed as rejected with the reason expired: a week-old change describes an account that has since moved.
/queued-writes/{queued_id}
What became of a queued change. status is pending, approved, rejected, failed or withdrawn, and only pending ever changes. approved carries the result; rejected and failed carry an error saying why, which is expired for a change nobody acted on within seven days. withdrawn means it was taken back before anyone decided, because what it would change stopped serving or the detector behind it stopped finding the problem. decided_at is when it stopped being pending. A person has to act, so poll slowly.
// POST /v1/organizations/acme/campaigns/spring-sale/negative-keywords
{ "keywords": ["free"], "dry_run": false }
// 202 Accepted
{
"error": {
"code": "approval_required",
"message": "This organization queues API writes for human sign-off. The change is pending as recommendation 'rec_812' in the dashboard's approval queue; poll /v1/queued-writes/rec_812 for the outcome.",
"doc_url": "https://glitchads.ai/docs/errors#approval_required",
"queued_recommendation_id": "rec_812"
}
}
// GET /v1/queued-writes/rec_812, once somebody has approved it
{
"id": "rec_812", "status": "approved", "operation": "add_negative_keywords",
"requested_at": "2026-09-20T09:14:02Z", "decided_at": "2026-09-20T10:02:40Z",
"result": { "action_id": "act_4471", "added": 1 }
}
/organizations/{org}/campaigns/{slug}/detect
Run one detector. It never changes the campaign. For a campaign that runs in Google Ads, findings that can be acted on are saved as a pending recommendation, replacing the details of any pending recommendation of the same type on that campaign, and carry its recommendation_id. A draft's findings, and keyword_count findings, carry a null recommendation_id. estimated_monthly_saving is left out when the detector does not compute one.
| Parameter | Type | Notes |
|---|---|---|
detector |
Typestring, required | One of high_cpc, no_conversion_keywords, zero_impression, negative_keywords, tcpa, exact_match, hipo, keyword_count, optimized_keywords, new_keywords. |
options |
Typeobject | negative_keywords: days, 1 to 90, default 7. high_cpc: threshold, how many times the campaign's average CPC, 1.0 to 10.0, default 3.0. keyword_count: threshold, 1 to 50, default 15. optimized_keywords: suggested_count, 1 to 50, default 15. tcpa: period_days, 7 to 90, default 30. zero_impression: include_paused, default false. The other four take none, and an option a detector does not take is a validation_error. |
curl -X POST https://api.glitchads.ai/v1/organizations/acme/campaigns/acme-prospecting/detect \
-H "Authorization: Bearer $GLITCH_API_KEY" -H "Content-Type: application/json" \
-d '{"detector":"negative_keywords","options":{"days":14}}'
// 200
{
"detector": "negative_keywords",
"findings": [ { "term": "free ads course", "match_type": "phrase", "cost_14d": 18.60, "clicks": 22,
"conversions": 0, "recommendation_id": "rec_4417" } ],
"estimated_monthly_saving": 39.86
}
/organizations/{org}/recommendations
What the optimisation engine and detector runs have proposed. Changes held for approval on a campaign also appear here, with type api_write; they are approved or rejected in Glitch, not applied through this API. Filter with campaign and status, which is pending by default and also takes applied or dismissed, meaning rejected by a person. A recommendation applied and then undone matches none of the three. estimated_monthly_saving is always null today.
// 200
{
"recommendations": [
{ "id": "rec_4417", "type": "negative_keywords", "campaign": "acme-prospecting",
"summary": "Exclude 'free ads course'", "estimated_monthly_saving": null,
"created_at": "2026-07-08T08:02:11Z" }
],
"total": 1, "next_cursor": null
}
/recommendations/{id}/apply
Apply one pending recommendation. dry_run defaults to true: the preview lists the operations it would run, or one summary operation when the recommendation does not itemise them. Only a pending recommendation can be applied; one already applied, dismissed or undone answers validation_error. A budget recommendation meets the same budget limits as PATCH. Needs recommendations:apply.
// 200 (dry run)
{ "dry_run": true,
"would_apply": [ { "op": "negative_keywords", "summary": "Exclude 'free ads course'" } ],
"estimated_monthly_saving": null }
// 200 (executed)
{ "applied": true, "action_id": "act_8841", "action_ids": [ "act_8841" ] }
/actions/{id}/undo
Reverse an executed action, whichever endpoint made it. Needs recommendations:apply. Undoing a state change starts the reverse transition in the background. restored lists what was reversed. Idempotent: undoing an action already undone returns an empty restored, spends no write quota and changes nothing.
// 200
{ "undone": true, "action_id": "act_8841",
"restored": [ "Reversed: Excluded 'free ads course' (phrase)" ] }
// 200 (already undone: nothing ran)
{ "undone": true, "action_id": "act_8841", "restored": [] }
/organizations/{org}/actions
The action log: every change, what made it, and reason, why it was made when that was recorded. Filter with from, to and source, and platform, google by default or meta. Needs audit:read, which only an Owner's credential can carry.source says what kind of change it was: dashboard for a direct edit, including a PATCH, state change or negative keywords sent through this API, and engine for an applied recommendation, whoever applied it. api is accepted as a filter and matches nothing today. To find who made a change, read the actor block: type is api_key for a key, with prefix its environment prefix and last four characters and user its creator; user for a person, including one acting through an OAuth connection; and engine for Glitch itself.
// 200
{
"actions": [
{ "id": "act_8841", "at": "2026-07-08T09:14:02Z", "source": "engine",
"actor": { "type": "api_key", "prefix": "glads_live_3f2a", "user": "dave@glitchads.ai" },
"op": "add_negative_keyword", "campaign": "acme-prospecting", "platform": "google",
"detail": "Excluded 'free ads course' (phrase)",
"reason": "Spent 18.60 over 14 days with no conversion", "undone": false }
],
"total": 129, "next_cursor": "djE6MjA="
}
/organizations/{org}/audit/export
The access trail, for compliance. Owner only, answering role_denied to anyone else, and needs audit:read. Filter with from and to; without from the export covers the 30 days up to to, or up to now. format is json by default, a total and the events, and also takes csv with the same columns apart from the event id. Each event records its id, the time, request id, operation, path, status code, error code, actor type, the user's account id, key prefix and last four characters, IP address and user agent. It includes failures, not only successful changes, and is not paginated.
curl "https://api.glitchads.ai/v1/organizations/acme/audit/export?from=2026-06-01&format=csv" \
-H "Authorization: Bearer $GLITCH_API_KEY" -o acme-audit.csv
/task-tracking/{id}
The state of any asynchronous job. Poll every 2 to 5 seconds until it is completed or failed; until then it is running. progress is 0 until the task completes, then 100. A completed task on a campaign, such as a campaign or ad creation, carries a result with the campaign slug and its url in Glitch, and a state change adds the state and synced_at. A completed strategy task carries the strategy id and its url instead. A report task carries none. A failed task carries an error with a reason, one of task_error, stuck_timeout, killed_by_restart, worker_shutdown, setup_required, google_ads_refused, google_ads_connection_broken or payment_required, and a message saying what to do next. Four are to handle differently. setup_required: the job cannot run until something is connected, so it will fail the same way until somebody does that, and retrying is wasted. google_ads_refused: Google Ads declined the change and the message carries Google's own reason (a trial campaign whose status cannot change, for one); the campaign has to change in Google Ads first. google_ads_connection_broken: the job could not read Google Ads because the Google login behind the organization's account was revoked or deleted. It fails the same way until the account's owner reconnects Google Ads in Glitch, so retrying before that is wasted. payment_required: the job would have put a campaign live, which an organization on the free plan cannot do; nothing was changed, and nothing will be until the plan is upgraded.
This one needs no scope of its own: any credential that can act in the task's organization can poll it, and any other id answers not_found.
// GET /v1/task-tracking/6102
{
"task_tracking_id": 6102, "type": "CAMPAIGN_STATE_UPDATE", "status": "completed",
"progress": 100,
"result": { "campaign": "acme-prospecting",
"url": "https://app.glitchads.ai/campaigns/google/acme-prospecting?org=acme",
"state": "live", "synced_at": "2026-07-08T09:14:02Z" }
}
Keys are not administered through this API. They are created, rotated and revoked in the platform, on the Connections page under API Keys: any member can create one, and rotation and revocation belong to the key's creator, an Owner or an Admin.
What a key can then do, how it is formatted and how rotation and revocation behave are on the authentication page.
Find waste, look at the fix before taking it, take it, then change your mind.
# 1. find waste in the campaign
curl -s -X POST https://api.glitchads.ai/v1/organizations/acme/campaigns/acme-prospecting/detect \
-H "Authorization: Bearer $GLITCH_API_KEY" -H "Content-Type: application/json" \
-d '{"detector":"negative_keywords"}'
# 2. preview the recommendation it saved
curl -s -X POST https://api.glitchads.ai/v1/recommendations/rec_4417/apply \
-H "Authorization: Bearer $GLITCH_API_KEY" -H "Content-Type: application/json" \
-d '{"dry_run":true}'
# 3. apply it
curl -s -X POST https://api.glitchads.ai/v1/recommendations/rec_4417/apply \
-H "Authorization: Bearer $GLITCH_API_KEY" -H "Content-Type: application/json" \
-d '{"dry_run":false}'
# -> {"applied":true,"action_id":"act_8841","action_ids":["act_8841"]}
# 4. changed your mind
curl -s -X POST https://api.glitchads.ai/v1/actions/act_8841/undo \
-H "Authorization: Bearer $GLITCH_API_KEY"