Docsv1 Updated 6 November 2026

ClickGuardian API

The ClickGuardian API gives you the same figures you see in the ClickGuardian app: clicks checked, ad clicks blocked, spend stopped, your campaigns, and why clicks were blocked. Use it to put your fraud figures into Zapier, a spreadsheet, a client report or your own dashboard.

Base URLhttps://app.clickguardian.ai/api/v1
HeaderAuthorization: Bearer cg_live_YOUR_KEY

Every request is a GET, and every response is JSON.

Read only.

The API reads your figures. It can't change anything in your account.

The same numbers as the app.

Every figure is worked out the same way the app works it out, so a report built on the API matches what you see in ClickGuardian.

Included with Professional, Growth, Scale and Custom plans.

Free trials of those plans include it too, with lower limits.

On this page
01

Quick start

Step 1: Create a key

In the ClickGuardian app, go to Settings → Connections and find the API keys card. Choose Create key, give it a name, choose which projects it can read, and copy the key. We only show the full key once, so keep it somewhere safe, such as a password manager.

Running ClickGuardian for your clients as a partner agency? Make one agency key in your agency area instead: see Agency keys.

Step 2: Check your key works

curl https://app.clickguardian.ai/api/v1/ping \
  -H "Authorization: Bearer cg_live_YOUR_KEY"

You'll get back your account's name:

JSON
{ "org_id": "6f1c2a90-4b7e-4c1d-9a55-2e8f0b3d7c11", "org_name": "Avroom Ltd", "plan": "growth" }

Step 3: Get your figures

curl
curl "https://app.clickguardian.ai/api/v1/reporting/overview?start_date=2026-09-01&end_date=2026-09-28" \
  -H "Authorization: Bearer cg_live_YOUR_KEY"

That returns the same headline figures as the Overview screen for 1 to 28 September.

02

Authentication

Send your key in the Authorization header on every request:

HTTP
Authorization: Bearer cg_live_Tz41mQe8hV2kPn7RbX0cLw9sYd3FaJ6u
  • Keys start cg_live_ and are 40 characters long.
  • Send requests to app.clickguardian.ai, whichever address you use to sign in to the app.
  • A request without a valid key gets 401 invalid_key.
  • Partner agencies can read every client with one agency key: see Agency keys.

What a key can do

Every key is Read only: it can read reports and the status of your projects, and nothing else. (Keys that can also block and unblock visitors are coming soon.)

Which projects a key can see

When you create a key you choose:

  • All projects. Every project on your account, including ones you add later.
  • Selected projects. Only the projects you tick. Projects you add later are not included.

A key can never see another account's projects. If you ask a key for a project it can't see, the answer is 404 not_found, the same answer as for a project that doesn't exist. You can't change a key's projects after you create it: replace the key instead.

Expiry

When you create a key, choose when it expires: after 30 days, 90 days or 1 year, or never. A week before a key expires, we email the account's owners, and the person who made the key if they're still an owner or admin. An expired key gets 401 invalid_key with the message "This API key has expired".

Revoking and replacing

  • Revoke a key from its menu in the API keys card. Anything using it stops working within a minute.
  • Replace with a new key opens the create form, filled in with the old key's name and projects. Choose an expiry and create the new key. The old key stops working when you click Done on the dialog that shows the new one, so put the new key in place first.

A key is also revoked automatically when every project it could read has been deleted. When someone is removed from your account, the person removing them can choose to revoke the keys they made.

Keeping keys safe

  • Treat a key like a password. Anyone who has it can read your figures.
  • Use keys on a server, in a script or in a tool such as Zapier. Don't put a key in a web page, a mobile app or a public code repository. (The API doesn't accept requests from web pages.)
  • Give each tool its own key, so you can revoke one without breaking the others.
  • If a key might have leaked, revoke it and make a new one.

After you create a key, we only ever show its first few characters again. We store a one-way fingerprint of the key rather than the key itself, so nobody, including us, can show it to you again.

03

Concepts

Projects

Each website you protect is a project in ClickGuardian. Each project has an id like proj_8kq2m4x7r1ab. List yours with GET /projects.

Most endpoints take an optional project_id:

  • Without it, you get every project the key can see, added together, the same as "All projects" in the app.
  • With it, you get that one project.

Dates

  • Dates are YYYY-MM-DD, and both ends are included: start_date=2026-09-01&end_date=2026-09-28 is 28 days.
  • Days run from midnight to midnight UTC.
  • Timestamps in responses are ISO 8601, in UTC.

Your plan's history

Each plan keeps a set amount of history: 6 months on Professional, 12 months on Growth, 24 months on Scale and Custom.

If you ask for dates before that, we return what your plan keeps and set meta.clamped to true. We do the same if end_date is after today: the range stops at today. If the whole range is older than your plan keeps, data is empty, meta.start_date and meta.end_date are null, and meta.clamped is true.

The response shape

Report endpoints return:

JSON 9 lines
{
  "data": [ ... ],
  "meta": {
    "start_date": "2026-09-01",
    "end_date": "2026-09-28",
    "retention_months": 12,
    "clamped": false
  }
}
  • meta.start_date and meta.end_date are the dates actually used, after your plan's history is applied.
  • meta.retention_months is how much history your plan keeps.
  • meta.clamped is true when we shortened the range you asked for.

Caching

We keep a copy of each answer for up to 10 minutes (project status: 1 minute). Asking the same question again within that time gets the same answer, quickly. New data arrives in ClickGuardian every few minutes, so this rarely matters, but don't expect a figure to change from one second to the next.

Field stability

Within v1 we only ever add fields and options. We never rename or remove a field, or change what it means. Build your integration to ignore fields it doesn't know.

Missing values

A figure is null when we can't work it out honestly, rather than showing a made-up number:

  • Money figures are null when we don't know your cost per click. Connect your ad account, or set your typical cost per click in Settings → Your business.
  • Percentages and scores are null when there's nothing to work them out from (for example, no ad clicks that day).
  • "Previous period" figures are null when the whole previous period is older than your plan keeps. If only part of it is, they cover the part your plan keeps.
04

Endpoints

EndpointWhat it returns
GET /pingChecks your key, and returns your account's name
GET /projectsThe projects your key can see
GET /statusThe status of every project your key can see
GET /projects/{project_id}/statusThe status of one project
GET /reporting/overviewHeadline figures for a date range
GET /reporting/overview_dailyThe same, one row per day
GET /reporting/campaignsYour campaigns, with spend and suspicious clicks
GET /reporting/campaigns_dailyTracked and suspicious clicks per campaign per day
GET /reporting/rules_dailyWhy clicks were blocked, per day
GET /reporting/geoBlocked ad clicks by country
GET /reporting/adgroupsAd groups, with suspicious clicks (paged)
GET /reporting/adsAds, with suspicious clicks (paged)
GET /reporting/keywordsKeywords, with suspicious clicks (paged)
GET

/ping

Checks your key works. Returns your account.

Example request
curl https://app.clickguardian.ai/api/v1/ping \
  -H "Authorization: Bearer cg_live_YOUR_KEY"
Example response
{ "org_id": "6f1c2a90-4b7e-4c1d-9a55-2e8f0b3d7c11", "org_name": "Avroom Ltd", "plan": "growth" }
Response fields
FieldTypeDescription
org_idstringYour account's id
org_namestringYour account's name
planstringYour plan's id: professional, growth, scale or custom. Older plans end in -legacy, for example growth-legacy. Empty if your account is managed by a partner
GET

/projects

The projects your key can see. Returns a list (not wrapped in data).

Example request
curl https://app.clickguardian.ai/api/v1/projects \
  -H "Authorization: Bearer cg_live_YOUR_KEY"
Example response 4 lines
[
  { "project_id": "proj_8kq2m4x7r1ab", "name": "Avroom", "domain": "avroom.co.uk" },
  { "project_id": "proj_3fz9w2p6c0de", "name": "Brightside Dental", "domain": "brightsidedental.co.uk" }
]
Response fields
FieldTypeDescription
project_idstringThe project's id, for project_id parameters
namestringThe project's name in ClickGuardian
domainstringThe website's domain
GET

/status

The status of every project your key can see: is the tracking code installed, are your ad accounts connected and syncing, how much is being blocked, and anything that needs your attention.

Parameters
ParameterRequiredValuesDescription
start_dateNoYYYY-MM-DDStart of the range for the traffic figures. Default: 27 days ago, so the default range is 28 days
end_dateNoYYYY-MM-DDEnd of the range. Default: today
needs_attentionNotrue, falsetrue lists only projects that need attention; false only those that don't
Example request
curl https://app.clickguardian.ai/api/v1/status \
  -H "Authorization: Bearer cg_live_YOUR_KEY"
Example response 57 lines
{
  "data": [
    {
      "project_id": "proj_8kq2m4x7r1ab",
      "name": "Avroom",
      "domain": "avroom.co.uk",
      "state": "7% of ad clicks blocked",
      "tone": "ok",
      "needs_attention": false,
      "issues": [],
      "tracking": {
        "installed": true,
        "evidence": "data",
        "old_tracker_only": false,
        "has_received_data": true,
        "receiving_data": true,
        "first_event_at": "2026-03-30T00:00:00.000Z",
        "last_event_at": "2026-09-28T16:00:00.000Z"
      },
      "ad_accounts": {
        "google_ads": {
          "connected": true,
          "account_id": "1234567890",
          "account_name": "Avroom Ads",
          "sync": "completed",
          "last_synced_at": "2026-09-28T06:00:16.561Z",
          "problem": null,
          "reconnect_needed": false
        },
        "microsoft_ads": null
      },
      "traffic": {
        "start_date": "2026-09-01",
        "end_date": "2026-09-28",
        "paid_clicks": 5890,
        "blocked_clicks": 412,
        "blocked_pct_of_ad_clicks": 6.99
      },
      "checks": {
        "overall": "healthy",
        "checked_at": "2026-09-28T07:01:12.000Z",
        "items": [
          { "check": "recent_data_seen", "label": "Recent visits received", "status": "pass", "checked_at": "2026-09-28T07:01:12.000Z" },
          { "check": "tracking_installed", "label": "Tracking code found on the homepage", "status": "pass", "checked_at": "2026-09-28T07:01:12.000Z" }
        ]
      }
    }
  ],
  "meta": {
    "start_date": "2026-09-01",
    "end_date": "2026-09-28",
    "retention_months": 12,
    "clamped": false,
    "websites": 1,
    "needing_attention": 0
  }
}
Response fields
FieldTypeDescription
project_id, name, domainstringThe project
statestringOne line on how the project is doing, as the app's project switcher shows it: for example "Setting up", "No code yet", "No ad account", "No recent data", "Too few ad clicks" or "7% of ad clicks blocked"
tonestringok, setup (still being set up) or busy (20% or more of ad clicks blocked)
needs_attentionbooleantrue when something in issues needs fixing
issuesstring[]Plain-English problems, most important first. Empty when all is well
tracking.installedbooleanThe ClickGuardian tracking code is on the website
tracking.evidencestringHow we know: data (we're receiving visits), inspection (we found the code on the page) or none
tracking.old_tracker_onlybooleanOnly an old version of the tracking code is installed
tracking.has_received_databooleanWe've ever received a visit
tracking.receiving_databooleanVisits are arriving now
tracking.first_event_at, tracking.last_event_atstring or nullFirst and latest visit
ad_accounts.google_adsobjectThe linked Google Ads account
ad_accounts.microsoft_adsobject or nullThe linked Microsoft Ads account, or null when Microsoft Ads isn't available on your account
….connectedbooleanAn ad account is linked to this project
….account_id, ….account_namestring or nullThe linked account
….syncstringcompleted, syncing, manual, error or not_connected
….last_synced_atstring or nullWhen we last synced your campaigns
….problemstring or nullWhat went wrong with the last sync, in plain English
….reconnect_neededbooleanYou need to reconnect the account to fix it
traffic.paid_clicksnumberAd clicks we tracked in the range
traffic.blocked_clicksnumberAd clicks we blocked in the range
traffic.blocked_pct_of_ad_clicksnumber or nullBlocked ad clicks as a percentage of traffic.paid_clicks
checks.overallstringOur daily checks: healthy, monitor, attention or unknown
checks.items[]object[]Each check: check (id), label, status (pass, warn, fail or skip), checked_at
meta.websitesnumberProjects in the list
meta.needing_attentionnumberOf those, how many need attention
Heads up. traffic.paid_clicks counts ad clicks; overview.paid_clicks counts visits from ads. They're close, but not the same number.
GET

/projects/{project_id}/status

The same as /status, for one project. Takes the same parameters. data is a list with one item (or none, if you ask for needs_attention=true and the project is fine).

Example request
curl https://app.clickguardian.ai/api/v1/projects/proj_8kq2m4x7r1ab/status \
  -H "Authorization: Bearer cg_live_YOUR_KEY"
GET

/reporting/overview

The headline figures from the Overview screen, for a date range, with the same-length period before it for comparison.

Parameters
ParameterRequiredValuesDescription
start_dateYesYYYY-MM-DDFirst day
end_dateYesYYYY-MM-DDLast day
project_idNoa project idOne project. Leave out for all your key can see
Example request
curl "https://app.clickguardian.ai/api/v1/reporting/overview?start_date=2026-09-01&end_date=2026-09-28" \
  -H "Authorization: Bearer cg_live_YOUR_KEY"
Example response 31 lines
{
  "data": [
    {
      "start_date": "2026-09-01",
      "end_date": "2026-09-28",
      "previous_start_date": "2026-08-04",
      "previous_end_date": "2026-08-31",
      "clicks_checked": 18420,
      "paid_clicks": 6312,
      "organic_clicks": 12108,
      "genuine_clicks": 14736,
      "suspicious_clicks": 2210,
      "highly_suspicious_clicks": 1474,
      "genuine_pct": 80,
      "suspicious_pct": 12,
      "highly_suspicious_pct": 8,
      "quality_score": 74,
      "blocked_clicks": 412,
      "blocked_clicks_previous": 389,
      "spend_stopped": 1174.2,
      "spend_stopped_previous": 1108.65,
      "spend_flagged": 273.6,
      "currency": "GBP",
      "cost_per_click": 2.85,
      "cost_per_click_source": "campaigns",
      "fake_leads": 14,
      "fake_leads_previous": 11
    }
  ],
  "meta": { "start_date": "2026-09-01", "end_date": "2026-09-28", "retention_months": 12, "clamped": false }
}
Response fields
FieldTypeDescription
start_date, end_datestringThe range
previous_start_date, previous_end_datestring or nullThe same-length period just before it
clicks_checkednumberEvery visit we checked, paid and organic
paid_clicksnumberVisits that came from an ad
organic_clicksnumberVisits that didn't
genuine_clicksnumberVisits with a quality score of 60 or more
suspicious_clicksnumberVisits scoring 30 to 59
highly_suspicious_clicksnumberVisits scoring under 30
genuine_pct, suspicious_pct, highly_suspicious_pctnumber or nullEach as a percentage of clicks_checked
quality_scorenumber or nullYour traffic's overall quality score, 0 to 100. null when there were no visits
blocked_clicksnumberAd clicks we blocked
blocked_clicks_previousnumber or nullThe same, previous period
spend_stoppednumber or nullBlocked ad clicks multiplied by your cost per click
spend_stopped_previousnumber or nullThe same, previous period, at this period's cost per click
spend_flaggednumber or nullSuspicious ad clicks we flagged but didn't block, at the same cost per click. Not included in spend_stopped
currencystringThe currency of the money figures
cost_per_clicknumber or nullThe cost per click used
cost_per_click_sourcestring or nullcampaigns (from your ad accounts), settings (the figure you set in Settings) or null (unknown)
fake_leadsnumberForm submissions we marked as suspicious or spam
fake_leads_previousnumber or nullThe same, previous period
GET

/reporting/overview_daily

The Overview chart: one row per day. Takes start_date, end_date and project_id, as overview.

Example request
curl "https://app.clickguardian.ai/api/v1/reporting/overview_daily?start_date=2026-09-01&end_date=2026-09-28" \
  -H "Authorization: Bearer cg_live_YOUR_KEY"
Example response 7 lines
{
  "data": [
    { "date": "2026-09-01", "clicks_checked": 652, "paid_clicks": 221, "organic_clicks": 431, "genuine_clicks": 529, "suspicious_clicks": 74, "highly_suspicious_clicks": 49, "blocked_clicks": 15, "blocked_pct_of_ad_clicks": 6.47, "spend_stopped": 42.75, "currency": "GBP" },
    { "date": "2026-09-02", "clicks_checked": 698, "paid_clicks": 240, "organic_clicks": 458, "genuine_clicks": 561, "suspicious_clicks": 81, "highly_suspicious_clicks": 56, "blocked_clicks": 17, "blocked_pct_of_ad_clicks": 6.77, "spend_stopped": 48.45, "currency": "GBP" }
  ],
  "meta": { "start_date": "2026-09-01", "end_date": "2026-09-28", "retention_months": 12, "clamped": false }
}
Response fields
FieldTypeDescription
datestringThe day (UTC)
clicks_checked, paid_clicks, organic_clicksnumberAs in overview, for the day
genuine_clicks, suspicious_clicks, highly_suspicious_clicksnumberAs in overview, for the day
blocked_clicksnumberAd clicks we blocked that day
blocked_pct_of_ad_clicksnumber or nullBlocked ad clicks as a percentage of the ad clicks we tracked that day. (paid_clicks counts visits, so this won't divide out exactly from the row)
spend_stoppednumber or nullThe day's blocked clicks at the range's cost per click. The days add up to overview.spend_stopped, give or take rounding
currencystringThe currency
GET

/reporting/campaigns

Your campaigns, as the Campaigns screen shows them: figures from your ad platform alongside the clicks we tracked and flagged. You get every campaign, up to 5,000, unless you ask for a page with limit.

Parameters
ParameterRequiredValuesDescription
start_date, end_dateYesYYYY-MM-DDThe range
project_idNoa project idOne project
ad_platformNogoogle, microsoft, metaOne ad platform only
limitNo1 to 5,000Campaigns per page. Default: every campaign
cursorNofrom meta.page.next_cursorThe next page
Example request
curl "https://app.clickguardian.ai/api/v1/reporting/campaigns?start_date=2026-09-01&end_date=2026-09-28" \
  -H "Authorization: Bearer cg_live_YOUR_KEY"
Example response 43 lines
{
  "data": [
    {
      "campaign_id": "21458710233",
      "campaign_name": "Emergency plumber - Search",
      "ad_platform": "google",
      "campaign_status": "ENABLED",
      "channel_type": "SEARCH",
      "currency": "GBP",
      "impressions": 18240,
      "clicks": 1406,
      "spend": 3982.1,
      "conversions": 96,
      "all_conversions": 104,
      "ctr_pct": 7.71,
      "avg_cpc": 2.83,
      "tracked_clicks": 1512,
      "paid_clicks": 1389,
      "suspicious_clicks": 171,
      "suspicious_pct": 12.31,
      "blocked_clicks": 98,
      "wasted_spend": 277.56,
      "health": "Worth watching"
    }
  ],
  "meta": {
    "start_date": "2026-09-01",
    "end_date": "2026-09-28",
    "retention_months": 12,
    "clamped": false,
    "page": { "limit": 5000, "total_rows": 1, "next_cursor": null },
    "totals": {
      "impressions": 18240,
      "clicks": 1406,
      "tracked_clicks": 1512,
      "paid_clicks": 1389,
      "suspicious_clicks": 171,
      "blocked_clicks": 98,
      "spend": [{ "currency": "GBP", "amount": 3982.1 }],
      "wasted_spend": [{ "currency": "GBP", "amount": 277.56 }]
    }
  }
}
Response fields
FieldTypeDescription
campaign_id, campaign_namestringThe campaign
ad_platformstringgoogle, microsoft or meta
campaign_statusstringThe status on the ad platform, for example ENABLED or PAUSED
channel_typestringThe campaign type on the ad platform, for example SEARCH
currencystringThe ad account's currency. Empty until the account has synced
impressions, clicks, spend, conversions, all_conversionsnumberFrom your ad platform
ctr_pctnumberClick-through rate, from your ad platform
avg_cpcnumberAverage cost per click, from your ad platform
tracked_clicksnumberClicks our tracking code saw for this campaign
paid_clicksnumberOf those, paid ad clicks
suspicious_clicksnumberPaid clicks we flagged as suspicious
suspicious_pctnumberSuspicious clicks as a percentage of paid clicks
blocked_clicksnumberClicks we blocked
wasted_spendnumberBlocked clicks multiplied by this campaign's own cost per click (its spend divided by its clicks). The Campaigns screen's "Wasted"
healthstringThe Campaigns screen's verdict: Needs attention, Worth watching, Looks clean or Too quiet to tell

Campaigns come most suspicious clicks first, then highest spend. meta adds:

FieldDescription
page.limitCampaigns per page
page.total_rowsCampaigns across all pages: the Campaigns screen's "All N campaigns"
page.next_cursorPass as cursor for the next page. null on the last page
totalsimpressions, clicks, tracked_clicks, paid_clicks, suspicious_clicks and blocked_clicks added up across every campaign, and spend and wasted_spend per currency, as a list of currency and amount. Always the whole list's, whichever page you're on
Heads up. campaigns counts paid clicks our tracking code saw on each campaign; overview counts paid visits. The two screens in the app differ in the same way, so don't add one to the other. In the same way, wasted_spend prices each campaign at its own cost per click, while overview.spend_stopped uses one rate for the whole account, so their totals differ.
GET

/reporting/campaigns_daily

Tracked and suspicious clicks per campaign per day: the small charts on the Campaigns screen.

Parameters
ParameterRequiredValuesDescription
start_date, end_dateYesYYYY-MM-DDThe range
project_idNoa project idOne project
ad_platformNogoogle, microsoft, metaOne ad platform only
campaign_idsNoup to 500 ids, comma-separatedOnly these campaigns
Example request
curl "https://app.clickguardian.ai/api/v1/reporting/campaigns_daily?start_date=2026-09-01&end_date=2026-09-28" \
  -H "Authorization: Bearer cg_live_YOUR_KEY"
Example response 7 lines
{
  "data": [
    { "date": "2026-09-01", "campaign_id": "21458710233", "campaign_name": "Emergency plumber - Search", "tracked_clicks": 54, "suspicious_clicks": 6 },
    { "date": "2026-09-02", "campaign_id": "21458710233", "campaign_name": "Emergency plumber - Search", "tracked_clicks": 61, "suspicious_clicks": 9 }
  ],
  "meta": { "start_date": "2026-09-01", "end_date": "2026-09-28", "retention_months": 12, "clamped": false }
}

Very large accounts asking for a long range may get 400 bad_params: "Too many campaign rows between … for one request; ask for fewer days, or narrow the request with campaign_ids".

GET

/reporting/rules_daily

Why we blocked ad clicks, per day, grouped as on the Blocked threats screen. Each blocked click is counted once, under the check that decided it. Takes start_date, end_date and project_id.

Example request
curl "https://app.clickguardian.ai/api/v1/reporting/rules_daily?start_date=2026-09-01&end_date=2026-09-28" \
  -H "Authorization: Bearer cg_live_YOUR_KEY"
Example response 7 lines
{
  "data": [
    { "date": "2026-09-07", "rule_id": "datacenter_ip_detection", "reason": "Coming from a data centre, not a home or office", "reason_group_id": "vpn", "reason_group": "Hiding where they were", "blocked_clicks": 6 },
    { "date": "2026-09-07", "rule_id": "headless_automation_detection", "reason": "Browser running without a screen", "reason_group_id": "bots", "reason_group": "Automated software, not people", "blocked_clicks": 2 }
  ],
  "meta": { "start_date": "2026-09-01", "end_date": "2026-09-28", "retention_months": 12, "clamped": false }
}
Response fields
FieldTypeDescription
datestringThe day
rule_idstringThe check's id. New checks are added over time
reasonstring or nullThe check in plain English, as Blocked threats shows it
reason_group_id, reason_groupstring or nullThe group it belongs to: vpn "Hiding where they were", bots "Automated software, not people", behaviour "Didn't behave like a customer", limit "Clicked far more than a customer would", geo "Outside the areas you cover"
blocked_clicksnumberAd clicks blocked by this check that day
GET

/reporting/geo

Blocked ad clicks by country. The countries add up to overview.blocked_clicks. Takes start_date, end_date and project_id.

Example request
curl "https://app.clickguardian.ai/api/v1/reporting/geo?start_date=2026-09-01&end_date=2026-09-28" \
  -H "Authorization: Bearer cg_live_YOUR_KEY"
Example response 8 lines
{
  "data": [
    { "country_code": "GB", "blocked_clicks": 288 },
    { "country_code": "US", "blocked_clicks": 61 },
    { "country_code": "", "blocked_clicks": 9 }
  ],
  "meta": { "start_date": "2026-09-01", "end_date": "2026-09-28", "retention_months": 12, "clamped": false }
}
Response fields
FieldTypeDescription
country_codestringTwo-letter country code (ISO 3166-1). Empty when we couldn't tell the country
blocked_clicksnumberAd clicks we blocked from there
GET

/reporting/adgroups, /reporting/ads, /reporting/keywords

The Campaigns screen's drill-down: suspicious clicks by ad group, by ad and by keyword. These lists can be long, so they come in pages.

These need tracking parameters on your ad clicks. For Google Ads, switch on Enriched campaign tracking in Settings → Connections (it appears once Google Ads is connected).

Parameters
ParameterRequiredValuesDescription
start_date, end_dateYesYYYY-MM-DDThe range
project_idNoa project idOne project
campaign_idNoa campaign idOnly this campaign
adgroup_idNoan ad group idOnly this ad group (ads and keywords only)
searchNoup to 100 charactersNames containing this
sortNosuspicious_clicks (default), suspicious_pct, tracked_clicks, paid_clicks, blocked_clicks, nameSort order
orderNodesc (default), ascDirection
statusNolive (default), paused, allAd groups and ads only
limitNo1 to 500Rows per page. Default 100
cursorNofrom meta.page.next_cursorThe next page

Until you choose a campaign (or, for ads, a campaign or ad group), only ad groups and ads with clicks are listed, as in the app. Keywords are always listed only when they have clicks.

Example request
curl "https://app.clickguardian.ai/api/v1/reporting/keywords?start_date=2026-09-01&end_date=2026-09-28" \
  -H "Authorization: Bearer cg_live_YOUR_KEY"
Example response 14 lines
{
  "data": [
    { "keyword": "emergency plumber near me", "match_type": "p", "ad_platform": "google", "tracked_clicks": 412, "paid_clicks": 398, "suspicious_clicks": 61, "suspicious_pct": 14.81, "blocked_clicks": 37, "health": "Worth watching" }
  ],
  "meta": {
    "start_date": "2026-09-01",
    "end_date": "2026-09-28",
    "retention_months": 12,
    "clamped": false,
    "page": { "limit": 100, "total_rows": 238, "next_cursor": "eyJvIjoxMDB9" },
    "totals": { "tracked_clicks": 5120, "paid_clicks": 4876, "suspicious_clicks": 611, "blocked_clicks": 402 },
    "notice": null
  }
}

Fields:

FieldDatasetsTypeDescription
campaign_id, campaign_nameadgroups, adsstringThe campaign
adgroup_id, adgroup_nameadgroups, adsstringThe ad group
ad_id, ad_name, ad_typeadsstringThe ad. ad_type is as your ad platform names it, for example RESPONSIVE_SEARCH_AD
statusadgroups, adsstringThe status on the ad platform
keywordkeywordsstringThe keyword
match_typekeywordsstringe (exact), p (phrase) or b (broad). Empty when the ad platform doesn't tell us
ad_platformallstringThe ad platform, for example google or microsoft
tracked_clicksallnumberClicks our tracking code saw
paid_clicksallnumberOf those, paid ad clicks
suspicious_clicksallnumberClicks we flagged as suspicious
suspicious_pctallnumberSuspicious clicks as a percentage of paid clicks (keywords: of tracked clicks)
blocked_clicksallnumberClicks we blocked
healthallstringNeeds attention, Worth watching, Looks clean or Too quiet to tell

meta adds:

FieldDescription
page.limitRows per page
page.total_rowsRows across all pages
page.next_cursorPass as cursor for the next page. null on the last page
totalstracked_clicks, paid_clicks, suspicious_clicks and blocked_clicks across every page
noticenull, or { "code": "needs_tracking_parameters", "message": "…" } when the list is empty because your clicks don't carry tracking parameters yet
05

Agency keys

If you run ClickGuardian for your clients as a partner agency, one agency key reads any of your clients. You don't need a key inside each client.

Make an agency key

In your agency area, open API keys. Give the key a name, choose when it expires (after 30 days, 90 days or 1 year, or never) and choose Create key. Agency owners and admins can make them. We only show the full key once, so keep it somewhere safe.

  • Anyone holding an agency key can read every one of your clients. Treat it like a password.
  • Agency keys are switched on agency by agency. If you don't see API keys in your agency area, ask us to switch them on.
  • Like every key, an agency key is Read only.

Name the client on every call

An agency key reads nothing until you say which client. Send client_id on every request:

curl
curl "https://app.clickguardian.ai/api/v1/reporting/overview?client_id=8d3f0c2a-5b71-4e9a-a0c4-2f6e9b1d7a35&start_date=2026-09-01&end_date=2026-09-28" \
  -H "Authorization: Bearer cg_live_YOUR_AGENCY_KEY"
  • Get each client's id from GET /clients.
  • Every endpoint above takes it: /projects, /status, /projects/{project_id}/status and every /reporting/ endpoint. You get exactly what that client's own key would get, for every website the client has.
  • Without client_id you get 400 bad_params. A client that isn't yours, or doesn't exist, gets 404 not_found, the same answer either way.
  • Each client keeps the history its own account keeps. meta.retention_months says how much.
  • Send requests to app.clickguardian.ai, not your own domain.

GET /ping with an agency key answers with your agency rather than an account:

JSON
{ "key_kind": "agency", "tenant_id": "42", "tenant_name": "Northpoint Digital", "client_count": 120 }
Response fields
FieldTypeDescription
key_kindstringagency
tenant_idstringYour agency's id
tenant_namestringYour agency's name
client_countnumberHow many clients the key can read
GET

/clients

Your clients, oldest first, with the client_id every other call needs. Only agency keys can list clients: an account's own key gets 403 forbidden.

Parameters
ParameterRequiredValuesDescription
limitNo1 to 200Clients per page. Default 100
cursorNofrom meta.next_cursorThe next page
Example request
curl https://app.clickguardian.ai/api/v1/clients \
  -H "Authorization: Bearer cg_live_YOUR_KEY"
Example response 7 lines
{
  "data": [
    { "client_id": "8d3f0c2a-5b71-4e9a-a0c4-2f6e9b1d7a35", "client_name": "Brightside Dental", "api_access": true, "created_at": "2026-11-03T11:17:52.596837+00:00" },
    { "client_id": "1b7e4d90-3c2f-4a6b-8e15-9f0a2c6d4b71", "client_name": "Avroom", "api_access": true, "created_at": "2026-11-04T09:41:07.112093+00:00" }
  ],
  "meta": { "total": 120, "limit": 100, "next_cursor": "eyJvIjoxMDB9" }
}
Response fields
FieldTypeDescription
client_idstringThe id to send as client_id
client_namestringThe client's name in ClickGuardian
api_accessbooleanfalse when we've switched the API off for this client. Calls for it get 403 not_entitled
created_atstringWhen the client was added
meta.totalnumberClients across all pages
meta.limitnumberClients per page
meta.next_cursorstring or nullPass as cursor for the next page. null on the last page

Status across all your clients

GET /status with an agency key and no client_id reads your clients a page at a time and lists every website of each one. Every row carries client_id and client_name. It takes start_date, end_date and needs_attention, as GET /status does, and:

ParameterRequiredValuesDescription
limitNo1 to 25Clients per page. Default 10
cursorNofrom meta.next_cursorThe next page of clients

meta has start_date, end_date, clamped, websites and needing_attention, as for one client, plus:

FieldDescription
clientsClients read on this page
skipped_clientsClients on this page we didn't read, each with client_id, client_name and a reason
total_clientsAll your clients
next_cursorPass as cursor for the next page. null on the last page

A page counts as one request, plus one for each client it reads.

Agency key limits

LimitAgency key
Requests per minute, per agency key300
Requests per minute, per agency key, for any one client60
Requests per day, whole agency (all agency keys together)50,000
New report queries per minute, per client60 units
New report queries per minute, whole agency60 units
Agency keys per agency10
  • Agency key requests don't count against a client's own daily request limit.
  • New report queries cost the same units as for any key (see Limits). An answer from our 10-minute copy costs no units, though it still counts as a request.
  • Days reset at midnight UTC.

Agency keys and AI assistants

Agency keys work with the REST API only for now. The MCP server answers an agency key with 403.

06

Limits

LimitProfessional, GrowthScale, CustomFree trial
Requests per minute, per key6012060
Requests per day, per key5,00020,000500
Requests per day, whole account (all keys together)5,00020,000500
New report queries per minute, whole account60 units60 units60 units
Keys per account101010
  • Days reset at midnight UTC.
  • New report queries. A request we answer from our 10-minute copy costs nothing here (it still counts as a request). A request that needs fresh figures costs units: 5 for overview, overview_daily, geo and campaigns_daily; 1 for everything else. That's about 12 fresh overview requests a minute, or 60 of the others.
  • Keys. Revoked and expired keys don't count towards the 10.
  • Per connection. Each IP address can make up to 300 requests a minute, and can try up to 20 keys a minute that we don't recognise.
  • Page sizes. campaigns: every campaign by default, or up to 5,000 per page. adgroups, ads, keywords: up to 500 rows per page.
  • Agency keys have limits of their own: see Agency keys.

When you go over a limit, you get 429 rate_limited with a Retry-After header saying how many seconds to wait. Wait that long, then try again. Asking for the same thing again straight away won't help. (Going over the 300-a-minute connection limit also gets a 429, but that response may not be JSON or include Retry-After: wait a minute.)

07

Errors

Errors look like this, with a matching HTTP status:

JSON
{ "error": { "code": "bad_params", "message": "start_date and end_date (YYYY-MM-DD) are required" } }
HTTPcodeExample messagesWhat to do
400bad_params"start_date and end_date (YYYY-MM-DD) are required", "start_date and end_date must be YYYY-MM-DD, start on or before end"Fix the request. The message says what's wrong
400bad_params"client_id is required with an agency key; GET /v1/clients lists your clients", "client_id must be a client id from GET /v1/clients"Agency keys: add a client_id from /clients
401invalid_key"Missing or malformed Authorization header", "API key not recognized", "This API key has expired"Check the header and the key. Make a new key if it has expired
401revoked"This API key has been revoked"Make a new key
403not_entitled"API access is not included on this plan"Move to Professional, Growth, Scale or Custom
403not_entitled"This account is paused. Restart the subscription to use the API again"Restart your subscription
403not_entitled"Agency API keys are not switched on for this agency", "This agency is paused. Agency API keys work again when it is reactivated", "API access is switched off for this client"Agency keys: contact support
403forbidden"Only agency keys can list clients"/clients is for agency keys
403forbidden"This key does not have the "projects:read" scope"Make a new key
403forbidden"The public API is temporarily unavailable"Try again later
404not_found"No website with that project_id on this account"Check the id with /projects. The key may not cover that project
404not_found"No client with that client_id for this key"Agency keys: check the id with /clients
404not_found"Unknown dataset "…""Check the endpoint name
404not_found"Not found"Send requests to app.clickguardian.ai
429rate_limited"Rate limit exceeded", "Daily request limit reached for this account", "Too many new report queries for this account this minute, try again shortly", "Too many unrecognized API keys, try again in a minute"Wait for Retry-After seconds
429rate_limited"Rate limit exceeded for this client, try again shortly", "Daily request limit reached for this agency", "Too many new report queries for this agency this minute, try again shortly"Agency keys: wait for Retry-After seconds
429rate_limited"The API is busy, try again shortly", "Our data store is busy, try again in a minute", "The API is at capacity, try again later"Our side is busy. Wait for Retry-After seconds
502upstream_error"Data fetch failed, try again shortly", "Key lookup failed, try again shortly", "Account lookup failed, try again shortly", "Website lookup failed, try again shortly"Our side. Try again in a minute
500internal_error"Unexpected error, try again shortly"Our side. Try again; if it keeps happening, contact support
500internal_error"This account is not set up correctly, contact support"Contact support

Retry 429, 502 and 500, waiting longer each time. Don't retry 400, 401, 403 or 404 without changing something.

08

MCP server

AI assistants such as Claude Code, Cursor and your own agents can ask ClickGuardian for your figures through the Model Context Protocol (MCP), using an API key you already have. Every tool runs the same code as the endpoints above, so an assistant's numbers match the API and the app.

  • Read only. Like the API, the MCP server can read your figures and nothing else.
  • Same keys, same limits. It uses your API keys, sees the same projects, and counts against the same limits.
  • Same plans. Included with Professional, Growth, Scale and Custom.
  • Not agency keys, yet. The MCP server works with a key made inside an account. An agency key gets 403.

Connect

Server address:

HTTP
https://mcp.clickguardian.ai/mcp

Add it to your assistant with your key in the Authorization header:

claude mcp add --transport http clickguardian https://mcp.clickguardian.ai/mcp --header "Authorization: Bearer cg_live_YOUR_KEY"

Then ask a question, for example "How many ad clicks did we block last week, and why?"

How it works

  • Address. https://mcp.clickguardian.ai/mcp, also at https://app.clickguardian.ai/api/mcp. Streamable HTTP, POST only. Works with clients on the 2025 protocol and the 2026-07-28 revision.
  • Projects. A key sees the same projects through MCP as through the API: all of them, or the ones you chose. Name a project by its id or its domain, for example avroom.co.uk.
  • Limits. Each tool call counts as one API request against your key and account limits. Connecting and listing tools cost nothing.
  • Partner accounts. The server only answers at the addresses above, not on a partner's own domain.

Tools

Ten tools, all read only. Each one is an API endpoint, shaped for an assistant.

ToolWhat it answersSame as
list_projectsThe projects your key can see, with ids and domains. Called first when you haven't named a website/projects
get_project_statusWhether protection is working: tracking code, ad accounts and syncs, share of ad clicks blocked, and anything that needs fixing/status
get_overviewHeadline figures for a period, with the period beforeoverview
get_daily_trendThe same figures per day, to find when something changedoverview_daily
get_campaignsCampaigns ranked by suspicious clicks, with wasted spend and a health verdictcampaigns
get_campaign_trendTracked and suspicious clicks per campaign per daycampaigns_daily
get_block_reasonsWhy ad clicks were blocked, in total or per dayrules_daily
get_countriesAd clicks blocked, by countrygeo
get_campaign_breakdownSuspicious clicks by ad group, ad or keyword, page by pageadgroups, ads, keywords
explain_metricsHow each figure is defined, so an assistant compares like with like

get_project_status needs a key with the projects:read scope. Keys made before project status existed don't have it: make a new key.

Inputs

InputValuesNotes
projectA project id or domainOptional. Leave it out for every project your key can see
periodyesterday, last_7_days, last_28_days, last_30_days, this_month, last_month, customDefault last_28_days. With custom, add start_date and end_date
limit1 to 100Rows to return. Default 25; day-by-day tools default to 100 and keep the latest days

Periods are worked out in UTC days and include today, as the app's date picker does. Other inputs, such as ad_platform, campaign_ids, sort and cursor, are the API's own parameters.

What an answer looks like

Each answer has two parts: a one-paragraph summary made only of numbers, dates, project ids and our own words, and the API's own data and meta. Campaign, ad group, ad and keyword names appear only in the data, never in the summary.

Tool call
tools/call get_overview { "project": "avroom.co.uk", "start_date": "2026-10-01", "end_date": "2026-10-07" }
Summary 1 to 7 Oct 2026, website proj_8kq2m4x7r1ab: 2,470 visits checked, 947 from ads. 298 suspicious and 173 highly suspicious visits. 122 ad clicks blocked (113 in the 7 days before). £417.24 of ad spend stopped at £3.42 a click. 8 fake leads.
JSON 11 lines
{
  "data": [ ... the overview row ... ],
  "meta": {
    "start_date": "2026-10-01",
    "end_date": "2026-10-07",
    "retention_months": 6,
    "clamped": false,
    "period": "custom",
    "summary": "1 to 7 Oct 2026, website proj_8kq2m4x7r1ab: ..."
  }
}

The summary is in meta too, because some assistants, Claude Code included, only show the model the data part of an answer.

Prompts

Ready-made prompts your assistant can offer:

  • fraud_report A client-ready summary of what was blocked and saved over a period, starting from protection status.
  • investigate_spike Whether a jump on a given day was an attack, a change in traffic or a tracking problem.
  • check_setup What is and isn't working, in plain English, and what to fix first.

MCP errors

Only sign-in problems come back as HTTP errors, so your assistant knows to ask you to reconnect. Everything else comes back as a tool answer marked as an error, which the assistant can explain.

WhenWhat the assistant gets
No key, or an unknown, expired or revoked oneHTTP 401, asking it to sign in again
Account paused, or a plan without the APIHTTP 403 with the API's message
A tool your key has no scope forHTTP 403 insufficient_scope, naming the scope
A partner's domainHTTP 404
An agency keyHTTP 403, saying agency keys work with the REST API only for now
A limit reached, or our data store busyAn error answer ending "Try again in N seconds."
A project your key can't see, or a bad inputAn error answer with the API's message
Coming soon. Connect from Claude.ai and ChatGPT with one click, by signing in, instead of pasting a key.
09

Recipes

Yesterday's figures every morning

Python
import os, datetime, requests

yesterday = (datetime.datetime.now(datetime.timezone.utc).date() - datetime.timedelta(days=1)).isoformat()
res = requests.get(
    "https://app.clickguardian.ai/api/v1/reporting/overview",
    params={"start_date": yesterday, "end_date": yesterday},
    headers={"Authorization": f"Bearer {os.environ['CLICKGUARDIAN_API_KEY']}"},
)
row = res.json()["data"][0]
print(f"{row['blocked_clicks']} ad clicks blocked, {row['currency']} {row['spend_stopped']} stopped")

Every keyword, all pages

JavaScript
const base = 'https://app.clickguardian.ai/api/v1/reporting/keywords'
const headers = { Authorization: `Bearer ${process.env.CLICKGUARDIAN_API_KEY}` }
let cursor = null
const rows = []
do {
  const url = new URL(base)
  url.search = new URLSearchParams({
    start_date: '2026-09-01',
    end_date: '2026-09-28',
    limit: '500',
    ...(cursor ? { cursor } : {}),
  })
  const body = await (await fetch(url, { headers })).json()
  rows.push(...body.data)
  cursor = body.meta.page.next_cursor
} while (cursor)

In Zapier

Use Webhooks by Zapier → GET. Set the URL to an endpoint, for example https://app.clickguardian.ai/api/v1/reporting/overview, add start_date and end_date as query string parameters, and add a header Authorization with the value Bearer cg_live_YOUR_KEY.

Which projects need attention

curl
curl "https://app.clickguardian.ai/api/v1/status?needs_attention=true" \
  -H "Authorization: Bearer cg_live_YOUR_KEY"
10

Help

My figures don't match the app.

Check you're looking at the same dates and the same project. API days run midnight to midnight UTC, and we keep a copy of each answer for up to 10 minutes. Paid clicks on campaigns and on overview are counted differently, as they are in the app.

I get 404 for my project.

Check the id with /projects. If it's not in the list, your key doesn't cover that project: make a key with All projects, or one that includes it.

A money figure is null.

We don't know your cost per click yet. Connect your ad account in Settings → Connections, or set your typical cost per click in Settings → Your business.

clamped is true.

We shortened the range you asked for: either it went back further than your plan keeps, or it ended after today.

Ad groups, ads or keywords are empty.

For Google Ads, switch on Enriched campaign tracking in Settings → Connections. Figures fill in from the next click onwards. meta.notice tells you when missing tracking parameters are the reason.

I'm getting 429.

You've reached a limit. Wait for the number of seconds in the Retry-After header. If you're asking for the same report repeatedly, you'll get the same answer for 10 minutes anyway.

I lost my key.

We can't show it again. Make a new key, and revoke the old one.

My key stopped working.

It may have been revoked, replaced or expired; every project it could read may have been deleted; or your plan may have changed. The error message says which.

Can I call the API from my website's JavaScript?

No. That would show your key to anyone who visits. Call it from a server, a script or a tool such as Zapier.

Still stuck?

Email support@clickguardian.ai with the time of the request and the error message. Never send us your full key.