On this page
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"const res = await fetch('https://app.clickguardian.ai/api/v1/ping', {
headers: { Authorization: `Bearer ${process.env.CLICKGUARDIAN_API_KEY}` },
})
console.log(await res.json())import os, requests
res = requests.get(
"https://app.clickguardian.ai/api/v1/ping",
headers={"Authorization": f"Bearer {os.environ['CLICKGUARDIAN_API_KEY']}"},
)
print(res.json()) 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 "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.
Authentication
Send your key in the Authorization header on every request:
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.
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-28is 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
{
"data": [ ... ],
"meta": {
"start_date": "2026-09-01",
"end_date": "2026-09-28",
"retention_months": 12,
"clamped": false
}
} meta.start_dateandmeta.end_dateare the dates actually used, after your plan's history is applied.meta.retention_monthsis how much history your plan keeps.meta.clampedistruewhen 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
nullwhen 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
nullwhen there's nothing to work them out from (for example, no ad clicks that day). - "Previous period" figures are
nullwhen the whole previous period is older than your plan keeps. If only part of it is, they cover the part your plan keeps.
Endpoints
| Endpoint | What it returns |
|---|---|
| GET /ping | Checks your key, and returns your account's name |
| GET /projects | The projects your key can see |
| GET /status | The status of every project your key can see |
| GET /projects/{project_id}/status | The status of one project |
| GET /reporting/overview | Headline figures for a date range |
| GET /reporting/overview_daily | The same, one row per day |
| GET /reporting/campaigns | Your campaigns, with spend and suspicious clicks |
| GET /reporting/campaigns_daily | Tracked and suspicious clicks per campaign per day |
| GET /reporting/rules_daily | Why clicks were blocked, per day |
| GET /reporting/geo | Blocked ad clicks by country |
| GET /reporting/adgroups | Ad groups, with suspicious clicks (paged) |
| GET /reporting/ads | Ads, with suspicious clicks (paged) |
| GET /reporting/keywords | Keywords, with suspicious clicks (paged) |
/ping
Checks your key works. Returns your account.
curl https://app.clickguardian.ai/api/v1/ping \
-H "Authorization: Bearer cg_live_YOUR_KEY"const res = await fetch('https://app.clickguardian.ai/api/v1/ping', {
headers: { Authorization: `Bearer ${process.env.CLICKGUARDIAN_API_KEY}` },
})
console.log(await res.json())import os, requests
res = requests.get(
"https://app.clickguardian.ai/api/v1/ping",
headers={"Authorization": f"Bearer {os.environ['CLICKGUARDIAN_API_KEY']}"},
)
print(res.json()) Example response
{ "org_id": "6f1c2a90-4b7e-4c1d-9a55-2e8f0b3d7c11", "org_name": "Avroom Ltd", "plan": "growth" } | Field | Type | Description |
|---|---|---|
org_id | string | Your account's id |
org_name | string | Your account's name |
plan | string | Your 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 |
/projects
The projects your key can see. Returns a list (not wrapped in data).
curl https://app.clickguardian.ai/api/v1/projects \
-H "Authorization: Bearer cg_live_YOUR_KEY"const res = await fetch('https://app.clickguardian.ai/api/v1/projects', {
headers: { Authorization: `Bearer ${process.env.CLICKGUARDIAN_API_KEY}` },
})
console.log(await res.json())import os, requests
res = requests.get(
"https://app.clickguardian.ai/api/v1/projects",
headers={"Authorization": f"Bearer {os.environ['CLICKGUARDIAN_API_KEY']}"},
)
print(res.json()) Example response
[
{ "project_id": "proj_8kq2m4x7r1ab", "name": "Avroom", "domain": "avroom.co.uk" },
{ "project_id": "proj_3fz9w2p6c0de", "name": "Brightside Dental", "domain": "brightsidedental.co.uk" }
] | Field | Type | Description |
|---|---|---|
project_id | string | The project's id, for project_id parameters |
name | string | The project's name in ClickGuardian |
domain | string | The website's domain |
/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.
| Parameter | Required | Values | Description |
|---|---|---|---|
start_date | No | YYYY-MM-DD | Start of the range for the traffic figures. Default: 27 days ago, so the default range is 28 days |
end_date | No | YYYY-MM-DD | End of the range. Default: today |
needs_attention | No | true, false | true lists only projects that need attention; false only those that don't |
curl https://app.clickguardian.ai/api/v1/status \
-H "Authorization: Bearer cg_live_YOUR_KEY"const res = await fetch('https://app.clickguardian.ai/api/v1/status', {
headers: { Authorization: `Bearer ${process.env.CLICKGUARDIAN_API_KEY}` },
})
console.log(await res.json())import os, requests
res = requests.get(
"https://app.clickguardian.ai/api/v1/status",
headers={"Authorization": f"Bearer {os.environ['CLICKGUARDIAN_API_KEY']}"},
)
print(res.json()) Example response
{
"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
}
} | Field | Type | Description |
|---|---|---|
project_id, name, domain | string | The project |
state | string | One 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" |
tone | string | ok, setup (still being set up) or busy (20% or more of ad clicks blocked) |
needs_attention | boolean | true when something in issues needs fixing |
issues | string[] | Plain-English problems, most important first. Empty when all is well |
tracking.installed | boolean | The ClickGuardian tracking code is on the website |
tracking.evidence | string | How we know: data (we're receiving visits), inspection (we found the code on the page) or none |
tracking.old_tracker_only | boolean | Only an old version of the tracking code is installed |
tracking.has_received_data | boolean | We've ever received a visit |
tracking.receiving_data | boolean | Visits are arriving now |
tracking.first_event_at, tracking.last_event_at | string or null | First and latest visit |
ad_accounts.google_ads | object | The linked Google Ads account |
ad_accounts.microsoft_ads | object or null | The linked Microsoft Ads account, or null when Microsoft Ads isn't available on your account |
….connected | boolean | An ad account is linked to this project |
….account_id, ….account_name | string or null | The linked account |
….sync | string | completed, syncing, manual, error or not_connected |
….last_synced_at | string or null | When we last synced your campaigns |
….problem | string or null | What went wrong with the last sync, in plain English |
….reconnect_needed | boolean | You need to reconnect the account to fix it |
traffic.paid_clicks | number | Ad clicks we tracked in the range |
traffic.blocked_clicks | number | Ad clicks we blocked in the range |
traffic.blocked_pct_of_ad_clicks | number or null | Blocked ad clicks as a percentage of traffic.paid_clicks |
checks.overall | string | Our daily checks: healthy, monitor, attention or unknown |
checks.items[] | object[] | Each check: check (id), label, status (pass, warn, fail or skip), checked_at |
meta.websites | number | Projects in the list |
meta.needing_attention | number | Of those, how many need attention |
traffic.paid_clicks counts ad clicks; overview.paid_clicks counts visits from ads. They're close, but not the same number. /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).
curl https://app.clickguardian.ai/api/v1/projects/proj_8kq2m4x7r1ab/status \
-H "Authorization: Bearer cg_live_YOUR_KEY"const res = await fetch('https://app.clickguardian.ai/api/v1/projects/proj_8kq2m4x7r1ab/status', {
headers: { Authorization: `Bearer ${process.env.CLICKGUARDIAN_API_KEY}` },
})
console.log(await res.json())import os, requests
res = requests.get(
"https://app.clickguardian.ai/api/v1/projects/proj_8kq2m4x7r1ab/status",
headers={"Authorization": f"Bearer {os.environ['CLICKGUARDIAN_API_KEY']}"},
)
print(res.json()) /reporting/overview
The headline figures from the Overview screen, for a date range, with the same-length period before it for comparison.
| Parameter | Required | Values | Description |
|---|---|---|---|
start_date | Yes | YYYY-MM-DD | First day |
end_date | Yes | YYYY-MM-DD | Last day |
project_id | No | a project id | One project. Leave out for all your key can see |
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"const url = new URL('https://app.clickguardian.ai/api/v1/reporting/overview')
url.search = new URLSearchParams({ start_date: '2026-09-01', end_date: '2026-09-28' })
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.CLICKGUARDIAN_API_KEY}` },
})
console.log(await res.json())import os, requests
res = requests.get(
"https://app.clickguardian.ai/api/v1/reporting/overview",
params={"start_date": "2026-09-01", "end_date": "2026-09-28"},
headers={"Authorization": f"Bearer {os.environ['CLICKGUARDIAN_API_KEY']}"},
)
print(res.json()) Example response
{
"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 }
} | Field | Type | Description |
|---|---|---|
start_date, end_date | string | The range |
previous_start_date, previous_end_date | string or null | The same-length period just before it |
clicks_checked | number | Every visit we checked, paid and organic |
paid_clicks | number | Visits that came from an ad |
organic_clicks | number | Visits that didn't |
genuine_clicks | number | Visits with a quality score of 60 or more |
suspicious_clicks | number | Visits scoring 30 to 59 |
highly_suspicious_clicks | number | Visits scoring under 30 |
genuine_pct, suspicious_pct, highly_suspicious_pct | number or null | Each as a percentage of clicks_checked |
quality_score | number or null | Your traffic's overall quality score, 0 to 100. null when there were no visits |
blocked_clicks | number | Ad clicks we blocked |
blocked_clicks_previous | number or null | The same, previous period |
spend_stopped | number or null | Blocked ad clicks multiplied by your cost per click |
spend_stopped_previous | number or null | The same, previous period, at this period's cost per click |
spend_flagged | number or null | Suspicious ad clicks we flagged but didn't block, at the same cost per click. Not included in spend_stopped |
currency | string | The currency of the money figures |
cost_per_click | number or null | The cost per click used |
cost_per_click_source | string or null | campaigns (from your ad accounts), settings (the figure you set in Settings) or null (unknown) |
fake_leads | number | Form submissions we marked as suspicious or spam |
fake_leads_previous | number or null | The same, previous period |
/reporting/overview_daily
The Overview chart: one row per day. Takes start_date, end_date and project_id, as overview.
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"const url = new URL('https://app.clickguardian.ai/api/v1/reporting/overview_daily')
url.search = new URLSearchParams({ start_date: '2026-09-01', end_date: '2026-09-28' })
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.CLICKGUARDIAN_API_KEY}` },
})
console.log(await res.json())import os, requests
res = requests.get(
"https://app.clickguardian.ai/api/v1/reporting/overview_daily",
params={"start_date": "2026-09-01", "end_date": "2026-09-28"},
headers={"Authorization": f"Bearer {os.environ['CLICKGUARDIAN_API_KEY']}"},
)
print(res.json()) Example response
{
"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 }
} | Field | Type | Description |
|---|---|---|
date | string | The day (UTC) |
clicks_checked, paid_clicks, organic_clicks | number | As in overview, for the day |
genuine_clicks, suspicious_clicks, highly_suspicious_clicks | number | As in overview, for the day |
blocked_clicks | number | Ad clicks we blocked that day |
blocked_pct_of_ad_clicks | number or null | Blocked 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_stopped | number or null | The day's blocked clicks at the range's cost per click. The days add up to overview.spend_stopped, give or take rounding |
currency | string | The currency |
/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.
| Parameter | Required | Values | Description |
|---|---|---|---|
start_date, end_date | Yes | YYYY-MM-DD | The range |
project_id | No | a project id | One project |
ad_platform | No | google, microsoft, meta | One ad platform only |
limit | No | 1 to 5,000 | Campaigns per page. Default: every campaign |
cursor | No | from meta.page.next_cursor | The next page |
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"const url = new URL('https://app.clickguardian.ai/api/v1/reporting/campaigns')
url.search = new URLSearchParams({ start_date: '2026-09-01', end_date: '2026-09-28' })
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.CLICKGUARDIAN_API_KEY}` },
})
console.log(await res.json())import os, requests
res = requests.get(
"https://app.clickguardian.ai/api/v1/reporting/campaigns",
params={"start_date": "2026-09-01", "end_date": "2026-09-28"},
headers={"Authorization": f"Bearer {os.environ['CLICKGUARDIAN_API_KEY']}"},
)
print(res.json()) Example response
{
"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 }]
}
}
} | Field | Type | Description |
|---|---|---|
campaign_id, campaign_name | string | The campaign |
ad_platform | string | google, microsoft or meta |
campaign_status | string | The status on the ad platform, for example ENABLED or PAUSED |
channel_type | string | The campaign type on the ad platform, for example SEARCH |
currency | string | The ad account's currency. Empty until the account has synced |
impressions, clicks, spend, conversions, all_conversions | number | From your ad platform |
ctr_pct | number | Click-through rate, from your ad platform |
avg_cpc | number | Average cost per click, from your ad platform |
tracked_clicks | number | Clicks our tracking code saw for this campaign |
paid_clicks | number | Of those, paid ad clicks |
suspicious_clicks | number | Paid clicks we flagged as suspicious |
suspicious_pct | number | Suspicious clicks as a percentage of paid clicks |
blocked_clicks | number | Clicks we blocked |
wasted_spend | number | Blocked clicks multiplied by this campaign's own cost per click (its spend divided by its clicks). The Campaigns screen's "Wasted" |
health | string | The 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:
| Field | Description |
|---|---|
page.limit | Campaigns per page |
page.total_rows | Campaigns across all pages: the Campaigns screen's "All N campaigns" |
page.next_cursor | Pass as cursor for the next page. null on the last page |
totals | impressions, 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 |
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. /reporting/campaigns_daily
Tracked and suspicious clicks per campaign per day: the small charts on the Campaigns screen.
| Parameter | Required | Values | Description |
|---|---|---|---|
start_date, end_date | Yes | YYYY-MM-DD | The range |
project_id | No | a project id | One project |
ad_platform | No | google, microsoft, meta | One ad platform only |
campaign_ids | No | up to 500 ids, comma-separated | Only these campaigns |
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"const url = new URL('https://app.clickguardian.ai/api/v1/reporting/campaigns_daily')
url.search = new URLSearchParams({ start_date: '2026-09-01', end_date: '2026-09-28' })
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.CLICKGUARDIAN_API_KEY}` },
})
console.log(await res.json())import os, requests
res = requests.get(
"https://app.clickguardian.ai/api/v1/reporting/campaigns_daily",
params={"start_date": "2026-09-01", "end_date": "2026-09-28"},
headers={"Authorization": f"Bearer {os.environ['CLICKGUARDIAN_API_KEY']}"},
)
print(res.json()) Example response
{
"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".
/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.
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"const url = new URL('https://app.clickguardian.ai/api/v1/reporting/rules_daily')
url.search = new URLSearchParams({ start_date: '2026-09-01', end_date: '2026-09-28' })
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.CLICKGUARDIAN_API_KEY}` },
})
console.log(await res.json())import os, requests
res = requests.get(
"https://app.clickguardian.ai/api/v1/reporting/rules_daily",
params={"start_date": "2026-09-01", "end_date": "2026-09-28"},
headers={"Authorization": f"Bearer {os.environ['CLICKGUARDIAN_API_KEY']}"},
)
print(res.json()) Example response
{
"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 }
} | Field | Type | Description |
|---|---|---|
date | string | The day |
rule_id | string | The check's id. New checks are added over time |
reason | string or null | The check in plain English, as Blocked threats shows it |
reason_group_id, reason_group | string or null | The 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_clicks | number | Ad clicks blocked by this check that day |
/reporting/geo
Blocked ad clicks by country. The countries add up to overview.blocked_clicks. Takes start_date, end_date and project_id.
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"const url = new URL('https://app.clickguardian.ai/api/v1/reporting/geo')
url.search = new URLSearchParams({ start_date: '2026-09-01', end_date: '2026-09-28' })
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.CLICKGUARDIAN_API_KEY}` },
})
console.log(await res.json())import os, requests
res = requests.get(
"https://app.clickguardian.ai/api/v1/reporting/geo",
params={"start_date": "2026-09-01", "end_date": "2026-09-28"},
headers={"Authorization": f"Bearer {os.environ['CLICKGUARDIAN_API_KEY']}"},
)
print(res.json()) Example response
{
"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 }
} | Field | Type | Description |
|---|---|---|
country_code | string | Two-letter country code (ISO 3166-1). Empty when we couldn't tell the country |
blocked_clicks | number | Ad clicks we blocked from there |
/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).
| Parameter | Required | Values | Description |
|---|---|---|---|
start_date, end_date | Yes | YYYY-MM-DD | The range |
project_id | No | a project id | One project |
campaign_id | No | a campaign id | Only this campaign |
adgroup_id | No | an ad group id | Only this ad group (ads and keywords only) |
search | No | up to 100 characters | Names containing this |
sort | No | suspicious_clicks (default), suspicious_pct, tracked_clicks, paid_clicks, blocked_clicks, name | Sort order |
order | No | desc (default), asc | Direction |
status | No | live (default), paused, all | Ad groups and ads only |
limit | No | 1 to 500 | Rows per page. Default 100 |
cursor | No | from meta.page.next_cursor | The 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.
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"const url = new URL('https://app.clickguardian.ai/api/v1/reporting/keywords')
url.search = new URLSearchParams({ start_date: '2026-09-01', end_date: '2026-09-28' })
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.CLICKGUARDIAN_API_KEY}` },
})
console.log(await res.json())import os, requests
res = requests.get(
"https://app.clickguardian.ai/api/v1/reporting/keywords",
params={"start_date": "2026-09-01", "end_date": "2026-09-28"},
headers={"Authorization": f"Bearer {os.environ['CLICKGUARDIAN_API_KEY']}"},
)
print(res.json()) Example response
{
"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:
| Field | Datasets | Type | Description |
|---|---|---|---|
campaign_id, campaign_name | adgroups, ads | string | The campaign |
adgroup_id, adgroup_name | adgroups, ads | string | The ad group |
ad_id, ad_name, ad_type | ads | string | The ad. ad_type is as your ad platform names it, for example RESPONSIVE_SEARCH_AD |
status | adgroups, ads | string | The status on the ad platform |
keyword | keywords | string | The keyword |
match_type | keywords | string | e (exact), p (phrase) or b (broad). Empty when the ad platform doesn't tell us |
ad_platform | all | string | The ad platform, for example google or microsoft |
tracked_clicks | all | number | Clicks our tracking code saw |
paid_clicks | all | number | Of those, paid ad clicks |
suspicious_clicks | all | number | Clicks we flagged as suspicious |
suspicious_pct | all | number | Suspicious clicks as a percentage of paid clicks (keywords: of tracked clicks) |
blocked_clicks | all | number | Clicks we blocked |
health | all | string | Needs attention, Worth watching, Looks clean or Too quiet to tell |
meta adds:
| Field | Description |
|---|---|
page.limit | Rows per page |
page.total_rows | Rows across all pages |
page.next_cursor | Pass as cursor for the next page. null on the last page |
totals | tracked_clicks, paid_clicks, suspicious_clicks and blocked_clicks across every page |
notice | null, or { "code": "needs_tracking_parameters", "message": "…" } when the list is empty because your clicks don't carry tracking parameters yet |
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 "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}/statusand every/reporting/endpoint. You get exactly what that client's own key would get, for every website the client has. - Without
client_idyou get400 bad_params. A client that isn't yours, or doesn't exist, gets404 not_found, the same answer either way. - Each client keeps the history its own account keeps.
meta.retention_monthssays 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 } | Field | Type | Description |
|---|---|---|
key_kind | string | agency |
tenant_id | string | Your agency's id |
tenant_name | string | Your agency's name |
client_count | number | How many clients the key can read |
/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.
| Parameter | Required | Values | Description |
|---|---|---|---|
limit | No | 1 to 200 | Clients per page. Default 100 |
cursor | No | from meta.next_cursor | The next page |
curl https://app.clickguardian.ai/api/v1/clients \
-H "Authorization: Bearer cg_live_YOUR_KEY"const res = await fetch('https://app.clickguardian.ai/api/v1/clients', {
headers: { Authorization: `Bearer ${process.env.CLICKGUARDIAN_API_KEY}` },
})
console.log(await res.json())import os, requests
res = requests.get(
"https://app.clickguardian.ai/api/v1/clients",
headers={"Authorization": f"Bearer {os.environ['CLICKGUARDIAN_API_KEY']}"},
)
print(res.json()) Example response
{
"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" }
} | Field | Type | Description |
|---|---|---|
client_id | string | The id to send as client_id |
client_name | string | The client's name in ClickGuardian |
api_access | boolean | false when we've switched the API off for this client. Calls for it get 403 not_entitled |
created_at | string | When the client was added |
meta.total | number | Clients across all pages |
meta.limit | number | Clients per page |
meta.next_cursor | string or null | Pass 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:
| Parameter | Required | Values | Description |
|---|---|---|---|
limit | No | 1 to 25 | Clients per page. Default 10 |
cursor | No | from meta.next_cursor | The next page of clients |
meta has start_date, end_date, clamped, websites and needing_attention, as for one client, plus:
| Field | Description |
|---|---|
clients | Clients read on this page |
skipped_clients | Clients on this page we didn't read, each with client_id, client_name and a reason |
total_clients | All your clients |
next_cursor | Pass 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
| Limit | Agency key |
|---|---|
| Requests per minute, per agency key | 300 |
| Requests per minute, per agency key, for any one client | 60 |
| Requests per day, whole agency (all agency keys together) | 50,000 |
| New report queries per minute, per client | 60 units |
| New report queries per minute, whole agency | 60 units |
| Agency keys per agency | 10 |
- 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.
Limits
| Limit | Professional, Growth | Scale, Custom | Free trial |
|---|---|---|---|
| Requests per minute, per key | 60 | 120 | 60 |
| Requests per day, per key | 5,000 | 20,000 | 500 |
| Requests per day, whole account (all keys together) | 5,000 | 20,000 | 500 |
| New report queries per minute, whole account | 60 units | 60 units | 60 units |
| Keys per account | 10 | 10 | 10 |
- 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,geoandcampaigns_daily; 1 for everything else. That's about 12 freshoverviewrequests 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.)
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" } } | HTTP | code | Example messages | What to do |
|---|---|---|---|
| 400 | bad_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 |
| 400 | bad_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 |
| 401 | invalid_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 |
| 401 | revoked | "This API key has been revoked" | Make a new key |
| 403 | not_entitled | "API access is not included on this plan" | Move to Professional, Growth, Scale or Custom |
| 403 | not_entitled | "This account is paused. Restart the subscription to use the API again" | Restart your subscription |
| 403 | not_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 |
| 403 | forbidden | "Only agency keys can list clients" | /clients is for agency keys |
| 403 | forbidden | "This key does not have the "projects:read" scope" | Make a new key |
| 403 | forbidden | "The public API is temporarily unavailable" | Try again later |
| 404 | not_found | "No website with that project_id on this account" | Check the id with /projects. The key may not cover that project |
| 404 | not_found | "No client with that client_id for this key" | Agency keys: check the id with /clients |
| 404 | not_found | "Unknown dataset "…"" | Check the endpoint name |
| 404 | not_found | "Not found" | Send requests to app.clickguardian.ai |
| 429 | rate_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 |
| 429 | rate_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 |
| 429 | rate_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 |
| 502 | upstream_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 |
| 500 | internal_error | "Unexpected error, try again shortly" | Our side. Try again; if it keeps happening, contact support |
| 500 | internal_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.
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:
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"{
"mcpServers": {
"clickguardian": {
"url": "https://mcp.clickguardian.ai/mcp",
"headers": { "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 athttps://app.clickguardian.ai/api/mcp. Streamable HTTP,POSTonly. 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.
| Tool | What it answers | Same as |
|---|---|---|
list_projects | The projects your key can see, with ids and domains. Called first when you haven't named a website | /projects |
get_project_status | Whether protection is working: tracking code, ad accounts and syncs, share of ad clicks blocked, and anything that needs fixing | /status |
get_overview | Headline figures for a period, with the period before | overview |
get_daily_trend | The same figures per day, to find when something changed | overview_daily |
get_campaigns | Campaigns ranked by suspicious clicks, with wasted spend and a health verdict | campaigns |
get_campaign_trend | Tracked and suspicious clicks per campaign per day | campaigns_daily |
get_block_reasons | Why ad clicks were blocked, in total or per day | rules_daily |
get_countries | Ad clicks blocked, by country | geo |
get_campaign_breakdown | Suspicious clicks by ad group, ad or keyword, page by page | adgroups, ads, keywords |
explain_metrics | How 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
| Input | Values | Notes |
|---|---|---|
project | A project id or domain | Optional. Leave it out for every project your key can see |
period | yesterday, last_7_days, last_28_days, last_30_days, this_month, last_month, custom | Default last_28_days. With custom, add start_date and end_date |
limit | 1 to 100 | Rows 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.
tools/call get_overview { "project": "avroom.co.uk", "start_date": "2026-10-01", "end_date": "2026-10-07" } JSON
{
"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.
| When | What the assistant gets |
|---|---|
| No key, or an unknown, expired or revoked one | HTTP 401, asking it to sign in again |
| Account paused, or a plan without the API | HTTP 403 with the API's message |
| A tool your key has no scope for | HTTP 403 insufficient_scope, naming the scope |
| A partner's domain | HTTP 404 |
| An agency key | HTTP 403, saying agency keys work with the REST API only for now |
| A limit reached, or our data store busy | An error answer ending "Try again in N seconds." |
| A project your key can't see, or a bad input | An error answer with the API's message |
Recipes
Yesterday's figures every morning
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
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 "https://app.clickguardian.ai/api/v1/status?needs_attention=true" \
-H "Authorization: Bearer cg_live_YOUR_KEY" 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.