Create and manage everything you monitor — sites, HTML/SEO changes, stuck-content detection and HTTP status codes — from your own code or an AI agent. More monitor types land on this same surface as they ship.
https://pageradar.io/api/v1
Create a token →
Pick a preset or individual scopes
OpenAPI 3.1 spec →
Machine-readable contract
Using an AI agent?
Point it at this page — it is written to be read by both
Ownership. Every id you pass is resolved against your own account. Anything belonging to
someone else answers 404, exactly like an id that does not exist — so there is
nothing to learn from probing.
Send your token as a Bearer credential on every request:
curl https://pageradar.io/api/v1/sites \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/json"
A token carries scopes, chosen when you create it at API access. Each endpoint below states the scope it needs.
| Scope | Grants |
|---|---|
account:read | Read your account identity |
sites:read | List and read sites |
sites:write | Create sites |
sites:delete | Delete a site and every monitor under it. Not in the Manager preset — pick it explicitly |
monitors:read | List and read monitors |
monitors:write | Create and update monitors |
monitors:delete | Delete a monitor and its history |
alerts:read | Read alert lists |
checks:run | Trigger on-demand checks |
Scopes are frozen when a token is created. A 403 insufficient_scope
names the scope you are missing; an existing token cannot gain it, so create a new one. Tokens issued before
scopes existed are read-only on v1.
Limits apply per account, per class of operation, on top of an overall 100 req/min ceiling.
| Class | Limit | Applies to |
|---|---|---|
| Reads | 100 / min | Every GET |
| Writes | 30 / min | POST, PATCH, DELETE |
| Check runs | 6 / min | On-demand checks, plus a daily budget per plan |
A 429 carries Retry-After. Honor it —
rate_limited is the per-minute wall, run_budget_exhausted
the daily check budget. Scheduled checks keep running either way; the budget only limits manual triggers.
Errors are RFC 9457
problem documents. Branch on code, show detail to a
human, and quote request_id when reporting a problem.
{
"type": "https://pageradar.io/problems/quota_exceeded",
"title": "Quota exceeded",
"status": 422,
"detail": "Quota exceeded",
"code": "quota_exceeded",
"request_id": "bff36e1c-e584-4bf8-a86a-f2bb1b69613a",
"errors": {
"quota": { "monitor_type": "html_change", "limit": 10, "used": 10, "remaining": 0, "requested": 1 }
}
}
| Status | Code | Meaning |
|---|---|---|
| 401 | unauthenticated | Missing or invalid token |
| 403 | insufficient_scope | The token lacks the scope this endpoint needs |
| 404 | not_found | Unknown id — or one that is not yours |
| 409 | idempotency_in_flight | The first request with this key is still running; retry shortly |
| 422 | validation_error | Field errors in errors |
| 422 | quota_exceeded | Your plan has no room; errors.quota gives the numbers |
| 422 | idempotency_key_conflict | Key reused with a different payload — use a new key |
| 429 | rate_limited | Per-minute limit; honor Retry-After |
| 429 | run_budget_exhausted | Daily on-demand check budget spent |
| 503 | quota_locked | Transient contention on a concurrent write; retry |
Lists are cursor-paginated: follow links.next until it is
null. Cursors stay correct while rows are being inserted, which page numbers do not.
per_page defaults to 25, maximum 100.
{
"data": [ /* … */ ],
"links": { "first": null, "last": null, "prev": null, "next": "https://…?cursor=eyJpZCI6NDJ9" },
"meta": { "path": "…", "per_page": 25 }
}
Creation and check-run endpoints accept an optional Idempotency-Key header. Send one
whenever a retry is possible: the original response is replayed for 24 hours instead of the work happening twice.
Idempotency-Replayed: true.422 idempotency_key_conflict. Mint one key per distinct request.-H "Idempotency-Key: $(uuidgen)"
Endpoints · Sites
A site groups everything you monitor for one domain. Create one first — every monitor hangs off it.
/v1/sites
scope
sites:read
Your sites, newest first. Cursor-paginated.
{
"data": [
{ "id": 12, "name": "Example", "domain": "example.com",
"created_at": "2026-08-01T10:00:00+00:00", "updated_at": "2026-08-01T10:00:00+00:00" }
],
"links": { "next": null }
}
/v1/sites
scope
sites:write
Domains are normalized before storage — scheme and www are stripped, so
https://www.example.com/ and example.com are the
same site. A domain you already have is refused with
422 validation_error rather than silently creating a second one; list your
sites first, or treat that 422 as "already there".
Adding a page that is already monitored behaves differently, and more forgivingly: it is
reported in skipped_duplicates and costs nothing. You can re-send the same
list of URLs safely — see Create monitors.
| Field | Type | Description | |
|---|---|---|---|
domain |
string | required | The domain to monitor, e.g. example.com. Max 255 characters. |
name |
string | optional | A label for your dashboard. Defaults to the domain. |
curl -X POST https://pageradar.io/api/v1/sites \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"domain": "example.com", "name": "Example"}'
{
"data": { "id": 12, "name": "Example", "domain": "example.com",
"created_at": "2026-09-01T09:12:00+00:00", "updated_at": "2026-09-01T09:12:00+00:00" }
}
/v1/sites/{site}
scope
sites:read
One of your sites. 404 if the id is not yours.
/v1/sites/{site}
scope
sites:delete
The widest-reaching call in this API. It removes the site and every monitor under it — HTML, status code, Core Web Vitals urls, sitemap, robots.txt, keywords, screenshots, affiliate trackers — with all of their history. There is no undo and no trash. Confirm with a human first, and prefer deleting individual monitors.
The response states exactly what went with it, and the capacity freed per monitor type. A second call on the
same id is a 404, so retries are safe.
{
"data": {
"id": 12, "domain": "example.com",
"deleted": { "html_monitors": 3, "status_code_monitors": 5, "urls": 2,
"sitemap_monitors": 0, "robots_txt_monitors": 1, "keyword_trackers": 0,
"screenshot_monitors": 0, "affiliate_redirect_trackers": 0 }
},
"meta": {
"quota": { "html_change": { "limit": 10, "used": 4, "remaining": 6 },
"status_code": { "limit": 20, "used": 9, "remaining": 11 } }
}
}
Endpoints · HTML & SEO monitors
Watch a page for changes to its SEO elements — title, meta description, canonical, headings, Open Graph, schema — and optionally detect a page whose content has stopped moving (see freshness).
/v1/sites/{site}/html-monitors
scope
monitors:read
/v1/sites/{site}/html-monitors
scope
monitors:write
Creates one monitor per URL. The response is multi-status: each URL comes back as created,
skipped (already monitored) or refused for lack of quota — a partial success is a normal outcome, not an error.
201 when anything was created, 200 when everything was
a duplicate, 422 quota_exceeded when nothing fits. Monitors are created active and
their first check is queued immediately.
| Field | Type | Description | |
|---|---|---|---|
urls |
string[] | required | 1 to 100 URLs to monitor. |
check_frequency |
enum | optional | hourly or daily. Default daily. |
elements_to_monitor |
string[] | optional | e.g. title, meta_description, canonical, h1, open_graph. |
monitor_full_html |
boolean | optional | Hash the whole page instead of the parsed elements. |
focus_keywords |
string[] | optional | Flag changes that affect these keywords. |
excluded_properties |
string[] | optional | Elements to ignore when diffing. |
tags |
string[] | optional | Your own labels. |
email_notifications_enabled |
boolean | optional | Default true. |
slack_notifications_enabled |
boolean | optional | Default false. |
slack_webhook_url |
string | optional | Required if Slack notifications are on. |
freshness_check_enabled |
boolean | optional | Turn on stuck-content detection. |
freshness_threshold_hours |
integer | conditional | Required with freshness. 1–168; at least 24 unless check_frequency is hourly. |
freshness_url_pattern |
string | optional | Regex selecting which content links count, e.g. #^/\d{4}/# |
curl -X POST https://pageradar.io/api/v1/sites/12/html-monitors \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"urls": ["https://example.com/news"], "check_frequency": "hourly",
"freshness_check_enabled": true, "freshness_threshold_hours": 12}'
{
"data": {
"created": [ { "id": 88, "type": "html_change", "url": "https://example.com/news",
"is_active": true, "check_frequency": "hourly",
"freshness": { "enabled": true, "threshold_hours": 12, "url_pattern": null,
"frozen_since": null, "unchanged_checks": 0, "last_new_content_at": null },
"last_checked_at": null } ],
"skipped_duplicates": [],
"quota_exceeded": []
},
"meta": { "quota": { "limit": 10, "used": 4, "remaining": 6 } }
}
/v1/html-monitors/{monitor}
scope
monitors:read
Includes the live freshness state. This is also the endpoint you poll after triggering a check:
last_checked_at advances when it completes.
/v1/html-monitors/{monitor}
scope
monitors:write
Send only the keys you want to change.
Editable:
is_active, check_frequency,
monitor_full_html, elements_to_monitor,
excluded_properties, focus_keywords,
custom_settings, tags,
email_notifications_enabled, slack_notifications_enabled,
slack_webhook_url, freshness_check_enabled,
freshness_threshold_hours, freshness_url_pattern.
Not editable: url — it identifies the monitor; delete and
recreate to watch another page. Unknown keys are ignored rather than rejected.
Quota counts active monitors. Switching is_active from false to true
consumes a slot and can return 422 quota_exceeded; switching it off always
works and frees the slot while keeping the history.
curl -X PATCH https://pageradar.io/api/v1/html-monitors/88 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"check_frequency": "daily", "freshness_threshold_hours": 48}'
/v1/html-monitors/{monitor}
scope
monitors:delete
Permanent: the monitor and all of its snapshots and alerts are gone. Prefer
PATCH {"is_active": false} when the intent is only to pause monitoring — it frees
the quota slot too, and the history survives. Returns the freed quota; a second call is a
404.
/v1/html-monitors/{monitor}/alerts
scope
alerts:read
Cursor-paginated, newest first. severity is one of
critical, high, medium,
low. Before/after values are clipped at 2000 characters, with
values_truncated telling you when that happened.
{
"data": [
{ "id": 501, "html_monitor_id": 88, "change_type": "modified", "element_type": "title",
"old_value": "Old title", "new_value": "New title", "values_truncated": false,
"severity": "high", "focus_keyword_affected": null, "is_read": false,
"detected_at": "2026-09-01T07:15:00+00:00" }
],
"links": { "next": null }
}
/v1/html-monitors/{monitor}/freshness-alerts
scope
alerts:read
/v1/html-monitors/{monitor}/check
scope
checks:run
Queues one check. There is no run id: on 202, poll
GET /v1/html-monitors/{'{'}monitor{'}'} until
last_checked_at advances, then read the alert lists. Polling every 15–30 seconds
is plenty. Do not poll by calling this endpoint again.
| Field | Type | Description | |
|---|---|---|---|
202 queued |
The check is on its way. | ||
200 not_run · inactive |
Activate the monitor first (PATCH is_active=true). | ||
200 not_run · already_queued |
A check is already in flight; meta.retry_after_seconds is the worst-case wait. | ||
422 quota_exceeded |
More active monitors than the plan allows. | ||
429 run_budget_exhausted |
Daily on-demand budget spent; honor Retry-After. |
A news page, a blog index or a category listing is supposed to keep producing new links. When it stops — a broken feed, a stalled publishing pipeline — nothing errors: the page still returns 200 with valid HTML. Freshness mode watches the content links of a page and tells you when they stop changing.
Turn it on at creation or with a PATCH:
| Field | Type | Description | |
|---|---|---|---|
freshness_check_enabled |
boolean | required | Turns the mode on. |
freshness_threshold_hours |
integer | required | How long without new content is too long. 1–168 hours; at least 24 unless check_frequency is hourly. |
freshness_url_pattern |
string | optional | Regex restricting which links count as content, e.g. #^/\d{4}/# for dated article URLs. |
Then read the state on the monitor:
"freshness": {
"enabled": true,
"threshold_hours": 12,
"url_pattern": "#^/\\d{4}/#",
"unchanged_checks": 4, // consecutive checks with no new content
"frozen_since": "2026-08-31T18:00:00+00:00", // non-null once the page is considered stuck
"last_new_content_at": "2026-08-31T06:00:00+00:00"
}
Do not expect an alert right after enabling it. Freezing needs at least three consecutive
unchanged checks plus the threshold, so the signal comes from state moving over time — watch
unchanged_checks and frozen_since, not an immediate
notification.
Endpoints · HTTP status monitors
Uptime and status-code monitoring: know when a URL starts answering 404, 500 or a redirect it should not.
/v1/sites/{site}/status-monitors
scope
monitors:read
/v1/sites/{site}/status-monitors
scope
monitors:write
Multi-status, exactly like HTML monitors: created / skipped / quota-refused per URL.
| Field | Type | Description | |
|---|---|---|---|
urls |
string[] | required | 1 to 100 URLs to monitor. |
check_frequency |
enum | optional | hourly, daily or weekly. Default hourly. |
tags |
string[] | optional | Your own labels. |
slack_notifications_enabled |
boolean | optional | Default false. |
slack_webhook_url |
string | optional | Required if Slack notifications are on. |
additional_notification_emails |
string[] | optional | Up to 2 extra recipients, on top of your account email. |
send_confirmation_email |
boolean | optional | Email you a creation summary. Off by default for API calls. |
curl -X POST https://pageradar.io/api/v1/sites/12/status-monitors \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"urls": ["https://example.com/checkout"], "check_frequency": "hourly"}'
/v1/status-monitors/{monitor}
scope
monitors:read
current_status_code is null until the first check
completes. 0 is the reserved value for a connection error — a site that could not
be reached at all.
{
"data": { "id": 140, "type": "status_code", "site_id": 12,
"url": "https://example.com/checkout", "is_active": true,
"check_frequency": "hourly",
"current_status_code": 200, "previous_status_code": 503,
"notifications": { "slack_enabled": false, "slack_webhook_url": null, "additional_emails": null },
"tags": null, "last_checked_at": "2026-09-01T08:00:00+00:00" }
}
/v1/status-monitors/{monitor}
scope
monitors:write
Editable:
is_active, check_frequency,
tags, slack_notifications_enabled,
slack_webhook_url, additional_notification_emails.
Not editable: url. Same quota rules on activation as HTML monitors.
curl -X PATCH https://pageradar.io/api/v1/status-monitors/140 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"check_frequency": "weekly", "slack_notifications_enabled": true,
"slack_webhook_url": "https://hooks.slack.com/services/XXX"}'
/v1/status-monitors/{monitor}
scope
monitors:delete
Permanent, with its alert history. Prefer deactivating to pause.
/v1/status-monitors/{monitor}/alerts
scope
alerts:read
change_type classifies the transition:
| Field | Type | Description | |
|---|---|---|---|
critical |
2xx → 4xx/5xx | ||
recovery |
4xx/5xx → 2xx | ||
redirect |
2xx → 3xx | ||
change |
Everything else — including transitions to and from 0 |
Detecting real downtime: a site that becomes completely unreachable records status
0, and 200 → 0 is classified
change, not critical. Test the codes themselves:
new_status_code === 0 || new_status_code >= 400.
/v1/status-monitors/{monitor}/check
scope
checks:run
Same semantics as the HTML check: 202 then poll the monitor. Transient errors
(timeouts, 5xx) are re-confirmed about two minutes later before being recorded, so allow roughly three minutes
before concluding anything.
Endpoints · Affiliate link monitoring
Trace an affiliate link's full redirect chain from a specific country, and get told when the chain changes, when the final destination moves, or when a network starts blocking you.
Read this before automating at scale. Checks here cost credits, not just quota:
next_check_at is pushed there).
This is a proxy-network capability, not the ISO list: a valid ISO code that is not here is a
422. The machine-readable version is the
country_code enum in
openapi.json.
/v1/sites/{site}/affiliate-trackers
scope
monitors:read
/v1/sites/{site}/affiliate-trackers
scope
monitors:write
One tracker per URL, for one country. The dedup identity is the triple
(site, url, country): the same link in another country is a new tracker, the same triple is skipped.
Multi-status — 201 when anything was created,
200 when everything was already tracked.
| Field | Type | Description | |
|---|---|---|---|
urls |
string[] | required | Up to 100 — lower than the dashboard on purpose, since each one queues a check that costs a credit. |
country_code |
string | required | ISO 3166-1 alpha-2 from the supported list. An unsupported code is a 422. |
name |
string | optional | Base name, suffixed #1, #2… with several URLs. Defaults to the link host. |
check_frequency_hours |
integer | optional | 24 (daily), 48 or 168 (weekly). Default 24. |
tags |
string[] | optional | Your own labels. |
alert_settings |
object | optional | redirect_change, final_domain_change, status_error, blocked_detected — all default true. timeout is always on and cannot be disabled. |
curl -X POST https://pageradar.io/api/v1/sites/12/affiliate-trackers \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"urls": ["https://partner.example.com/go/abc"],
"country_code": "ES", "name": "Bookmaker", "check_frequency_hours": 24}'
{
"data": {
"created": [ { "id": 501, "type": "affiliate_redirect", "url": "https://partner.example.com/go/abc",
"country_code": "ES", "is_active": true, "check_frequency_hours": 24 } ],
"skipped_duplicates": []
},
"meta": { "credits": { "limit": 5000, "used": 1204, "remaining": 3796,
"period_end": "2026-09-30T23:59:59+00:00",
"checks_queued": 1 } }
}
Reading meta.credits correctly: credits are spent when a check
runs, not when a tracker is created — so used does not yet include the
checks this call just queued. checks_queued tells you how many are about to be
debited.
/v1/affiliate-trackers/{tracker}
scope
monitors:read
country_code is ISO 3166-1 alpha-2 and immutable;
next_check_at tells you when the scheduler will look again.
{
"data": { "id": 501, "type": "affiliate_redirect", "site_id": 12,
"name": "Bookmaker ES", "url": "https://partner.example.com/go/abc",
"country_code": "ES", "is_active": true, "check_frequency_hours": 24,
"alert_settings": { "redirect_change": true, "final_domain_change": true,
"status_error": true, "timeout": true, "blocked_detected": true },
"tags": ["sports"], "last_checked_at": "2026-09-08T06:30:00+00:00",
"next_check_at": "2026-09-09T06:30:00+00:00" }
}
/v1/affiliate-trackers/{tracker}
scope
monitors:write
Editable:
is_active, name,
check_frequency_hours (24 / 48 / 168),
tags, alert_settings.
Not editable: initial_url and
country_code. With the site they are the tracker's identity, and the stored
checks were run against them — changing either would duplicate an existing tracker and orphan its history.
Delete and recreate instead. alert_settings is merged, so a partial
PATCH keeps the toggles it does not mention.
To stop spending credits on a link, deactivate it
({"is_active": false}) rather than deleting it: the checks stop and the
history stays.
curl -X PATCH https://pageradar.io/api/v1/affiliate-trackers/501 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"check_frequency_hours": 168, "alert_settings": {"status_error": false}}'
/v1/affiliate-trackers/{tracker}
scope
monitors:delete
Permanent: every executed check and every alert go with it. A second call is a
404, so retries are safe. Prefer deactivating to pause.
/v1/affiliate-trackers/{tracker}/checks
scope
results:read
One entry per executed check, newest first, with the hop-by-hop chain as observed from the tracker's
country. final_domain is the field to watch: a change
there is how affiliate link hijacking shows up. final_status_code is
0 when the chain could not be completed at all.
The chain is third-party content, so it is bounded: withheld entirely when the stored chain exceeds ~64 KB,
and otherwise capped at 25 hops. redirect_chain_truncated tells you when what you
see is not the whole story.
{
"data": [
{ "id": 9001, "tracker_id": 501, "final_status_code": 200,
"final_domain": "bookmaker.example", "total_redirects": 3,
"total_response_time_ms": 812, "is_successful": true, "error_message": null,
"redirect_chain": [
{ "url": "https://partner.example.com/go/abc", "status_code": 302, "host": "partner.example.com" },
{ "url": "https://track.example.net/c/1", "status_code": 302, "host": "track.example.net" },
{ "url": "https://bookmaker.example/es/", "status_code": 200, "host": "bookmaker.example" }
],
"redirect_chain_truncated": false,
"checked_at": "2026-09-08T06:30:12+00:00" }
],
"links": { "next": null }
}
/v1/affiliate-trackers/{tracker}/check
scope
checks:run
Queues one check of the redirect chain from the tracker's country. It spends one credit.
There is no run id: on 202, poll
GET /v1/affiliate-trackers/{'{'}tracker{'}'} until
last_checked_at advances, then read /checks.
| Field | Type | Description | |
|---|---|---|---|
202 queued |
On its way. One credit will be spent when it runs. | ||
200 not_run · inactive |
Activate the tracker first (PATCH is_active=true). | ||
200 not_run · already_queued |
A check is already in flight; poll the tracker instead. | ||
429 rate_limited |
Per-minute burst limit (6/min). Retry in seconds. | ||
429 run_budget_exhausted |
Shared DAILY on-demand budget across all monitor types. Retry tomorrow. | ||
429 affiliate_credits_exhausted |
MONTHLY credit allowance spent. errors.credits.period_end says when it resets — and your SCHEDULED checks are paused until then too. |
Three different 429s, three different waits: seconds for the burst limit,
a day for the run budget, until the 1st of next month for credits.
Branch on code, never on the status alone.
/v1/affiliate-trackers/{tracker}/alerts
scope
alerts:read
| Field | Type | Description | |
|---|---|---|---|
status_change |
The final destination now answers a different status code. | ||
domain_change |
The link now lands on a different domain — check for hijacking. | ||
redirect_count_change |
The chain gained or lost at least 2 hops. | ||
performance_degradation |
The chain became more than twice as slow, and over 10 seconds. | ||
potential_block |
The chain used to complete and no longer does — the network may be blocking that country. | ||
redirect_path_change |
The set of intermediate domains changed. |
Severity is one of critical, high, medium, low. Email is sent for critical and high only.
/v1/user
scope
account:read
The account behind the token. Useful as a connectivity and scope check.
{ "data": { "id": 1, "name": "Sam", "email": "[email protected]" } }
Watch a news page and get told when it stops publishing:
# 1. the site
curl -sX POST https://pageradar.io/api/v1/sites \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"domain": "example.com"}' # -> data.id = SITE_ID
# 2. an hourly monitor with stuck-content detection
curl -sX POST https://pageradar.io/api/v1/sites/$SITE_ID/html-monitors \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"urls": ["https://example.com/news"], "check_frequency": "hourly",
"freshness_check_enabled": true, "freshness_threshold_hours": 12}'
# 3. later: is the page stuck?
curl -s https://pageradar.io/api/v1/html-monitors/$MONITOR_ID \
-H "Authorization: Bearer $TOKEN" # -> data.freshness.frozen_since
PageRadar's first API, predating v1. Still supported and unchanged; new integrations should use the v1 surface above, which is where the other features live.
Access your PageRadar monitoring data programmatically. Build custom dashboards, integrate with your tools, and automate your brand monitoring workflows.
https://pageradar.io/api
6
Endpoints
100/min
Rate Limit
REST
JSON API
All API requests require a Bearer token in the Authorization header.
Subscription required. An active PageRadar subscription is needed to use the API. View pricing
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json" \
https://pageradar.io/api/reddit/monitors
The API allows 100 requests per minute per authenticated user. Rate limit information is included in response headers:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Maximum requests per minute (100) |
X-RateLimit-Remaining |
Requests remaining in current window |
Retry-After |
Seconds to wait (only on 429 responses) |
HTTP/1.1 429 Too Many Requests
Retry-After: 42
{
"message": "Too Many Attempts."
}
List endpoints return paginated results with meta and links objects for navigation.
| Parameter | Type | Default | Description |
|---|---|---|---|
page |
integer | 1 | Page number |
per_page |
integer | 50 | Items per page (max 100) |
{
"data": [ ... ],
"links": {
"first": "https://pageradar.io/api/reddit/mentions?page=1",
"last": "https://pageradar.io/api/reddit/mentions?page=5",
"prev": null,
"next": "https://pageradar.io/api/reddit/mentions?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 5,
"per_page": 50,
"to": 50,
"total": 237
}
}
| Code | Meaning | Description |
|---|---|---|
| 200 | OK | Request was successful |
| 401 | Unauthorized | Missing or invalid API token |
| 403 | Forbidden | No active subscription — upgrade your plan |
| 404 | Not Found | Resource doesn't exist or doesn't belong to you |
| 422 | Validation Error | Invalid parameters — check the errors object in the response |
| 429 | Too Many Requests | Rate limit exceeded — wait and retry |
| 500 | Server Error | Something went wrong on our end |
401 Unauthorized
{
"message": "Unauthenticated."
}
403 Forbidden
{
"error": "subscription_required",
"message": "An active subscription is required.",
"upgrade_url": "https://pageradar.io/pricing"
}
Endpoints
/api/reddit/monitors
Returns all Reddit keyword monitors for the authenticated user, including mention counts and unread alert counts.
{
"data": [
{
"id": 1,
"keyword": "pageradar",
"subreddits": ["seo", "webdev"],
"check_frequency": "hourly",
"is_active": true,
"last_checked_at": "2026-03-19T08:00:00+00:00",
"mentions_count": 42,
"unread_alerts_count": 3,
"created_at": "2026-01-15T10:30:00+00:00",
"updated_at": "2026-03-19T08:00:00+00:00"
}
]
}
/api/reddit/monitors/{id}
Returns details for a single monitor. Returns 404 if not found or not owned by you.
| Parameter | Type | Description |
|---|---|---|
id | integer | Monitor ID required |
curl -H "Authorization: Bearer YOUR_TOKEN" \
https://pageradar.io/api/reddit/monitors/1
/api/reddit/monitors/{id}/mentions
Returns paginated mentions for a specific monitor. Supports the same query filters as List All Mentions.
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://pageradar.io/api/reddit/monitors/1/mentions?subreddit=seo&per_page=20"
/api/reddit/mentions
Returns all Reddit mentions across all your monitors, with powerful filtering, sorting, and pagination.
| Parameter | Type | Default | Description |
|---|---|---|---|
monitor_id |
integer | - | Filter by monitor ID |
keyword |
string | - | Search within keyword_found (partial match) |
subreddit |
string | - | Filter by subreddit name (e.g. seo) |
found_in |
string | - | Where keyword was found: title, body, or comment |
date_from |
date | - | Start date (YYYY-MM-DD) |
date_to |
date | - | End date (YYYY-MM-DD) |
sort |
string | date | date, score, or comments |
sort_dir |
string | desc | asc or desc |
per_page |
integer | 50 | Results per page (max 100) |
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://pageradar.io/api/reddit/mentions?subreddit=seo&date_from=2026-01-01&sort=score&per_page=20"
{
"data": [
{
"id": 123,
"monitor_id": 1,
"keyword_found": "pageradar",
"reddit_id": "abc123",
"post_title": "Best SEO monitoring tools in 2026?",
"post_url": "https://reddit.com/r/seo/comments/abc123",
"subreddit": "seo",
"author": "seo_expert",
"found_in": "body",
"context_text": "I've been using pageradar for my clients...",
"score": 42,
"num_comments": 15,
"comment_id": null,
"comment_author": null,
"created_at_reddit": "2026-03-18T14:30:00+00:00",
"created_at": "2026-03-18T15:00:00+00:00"
}
],
"links": { "first": "...?page=1", "last": "...?page=3", "prev": null, "next": "...?page=2" },
"meta": { "current_page": 1, "last_page": 3, "per_page": 50, "total": 142 }
}
/api/reddit/mentions/{id}
Returns a single mention with full details. Returns 404 if not found or not owned by you.
curl -H "Authorization: Bearer YOUR_TOKEN" \
https://pageradar.io/api/reddit/mentions/123
/api/reddit/search
Live
Search Reddit in real-time for any keyword. This endpoint queries the Reddit API directly and returns matching posts — no monitor needed.
Perfect for one-off brand checks, prospecting, or building custom monitoring dashboards.
| Parameter | Type | Default | Description |
|---|---|---|---|
keyword |
string | - | Keyword to search for required |
subreddits[] |
array | all | Limit to specific subreddits (e.g. subreddits[]=seo&subreddits[]=webdev) |
sort |
string | new | new, relevance, hot, top, or comments |
time |
string | week | hour, day, week, month, year, or all |
limit |
integer | 25 | Number of Reddit posts to scan (max 100) |
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://pageradar.io/api/reddit/search?keyword=elon+musk&time=day&limit=25"
{
"data": [
{
"reddit_id": "1ry61df",
"post_title": "what's the cringiest fact about elon musk?",
"post_url": "https://reddit.com/r/AskReddit/comments/1ry61df",
"subreddit": "AskReddit",
"author": "Thick-Topic-521",
"found_in": "title",
"context_text": "what's the cringiest fact about elon musk?",
"keyword_found": "elon musk",
"score": 2,
"num_comments": 4,
"created_at_reddit": "2026-03-19T17:01:26+00:00"
}
],
"keyword": "elon musk",
"subreddits": "all",
"sort": "new",
"time": "day",
"total": 8
}
/api/user
Returns basic information about the authenticated user. Useful for verifying your token works.
{
"id": 1,
"name": "John Doe",
"email": "[email protected]"
}
| Field | Type | Description |
|---|---|---|
id | integer | Unique monitor ID |
keyword | string | Keyword being tracked |
subreddits | array|null | List of specific subreddits, or null for all |
check_frequency | string | hourly, daily, or weekly |
is_active | boolean | Whether the monitor is actively checking |
last_checked_at | string|null | ISO 8601 timestamp of last check |
mentions_count | integer | Total number of mentions found |
unread_alerts_count | integer | Number of unread alerts |
created_at | string | ISO 8601 creation timestamp |
updated_at | string | ISO 8601 last update timestamp |
| Field | Type | Description |
|---|---|---|
id | integer | Unique mention ID |
monitor_id | integer | Parent monitor ID |
keyword_found | string | The exact keyword that was matched |
reddit_id | string | Reddit post ID |
post_title | string | Reddit post title |
post_url | string | Direct URL to the Reddit post |
subreddit | string | Subreddit name (without r/ prefix) |
author | string|null | Reddit username of the author |
found_in | string | title, body, or comment |
context_text | string|null | Text excerpt around the keyword match |
score | integer | Reddit upvote score |
num_comments | integer | Number of comments on the post |
comment_id | string|null | Comment ID (if found in a comment) |
comment_author | string|null | Comment author (if found in a comment) |
created_at_reddit | string|null | When the post was created on Reddit (ISO 8601) |
created_at | string | When we detected this mention (ISO 8601) |
List all monitors
curl -H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/json" \
https://pageradar.io/api/reddit/monitors
Search mentions with filters
curl -H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/json" \
"https://pageradar.io/api/reddit/mentions?subreddit=seo&sort=score&date_from=2026-01-01&per_page=20"
Live search for a keyword
curl -H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/json" \
"https://pageradar.io/api/reddit/search?keyword=your+brand&time=day&limit=50"
List all monitors
const API_TOKEN = 'YOUR_TOKEN';
const BASE_URL = 'https://pageradar.io/api';
const response = await fetch(`${BASE_URL}/reddit/monitors`, {
headers: {
'Authorization': `Bearer ${API_TOKEN}`,
'Accept': 'application/json',
},
});
const { data } = await response.json();
data.forEach(monitor => {
console.log(`${monitor.keyword}: ${monitor.mentions_count} mentions`);
});
Search mentions with filters
const params = new URLSearchParams({
subreddit: 'seo',
sort: 'score',
date_from: '2026-01-01',
per_page: '20',
});
const response = await fetch(`${BASE_URL}/reddit/mentions?${params}`, {
headers: {
'Authorization': `Bearer ${API_TOKEN}`,
'Accept': 'application/json',
},
});
const { data, meta } = await response.json();
console.log(`Found ${meta.total} mentions (page ${meta.current_page}/${meta.last_page})`);
Live search for a keyword
const params = new URLSearchParams({
keyword: 'your brand',
time: 'day',
limit: '50',
});
const response = await fetch(`${BASE_URL}/reddit/search?${params}`, {
headers: {
'Authorization': `Bearer ${API_TOKEN}`,
'Accept': 'application/json',
},
});
const { data, total } = await response.json();
console.log(`Found ${total} live mentions`);
data.forEach(mention => {
console.log(`[r/${mention.subreddit}] ${mention.post_title} (score: ${mention.score})`);
});
List all monitors
import requests
API_TOKEN = "YOUR_TOKEN"
BASE_URL = "https://pageradar.io/api"
headers = {"Authorization": f"Bearer {API_TOKEN}", "Accept": "application/json"}
response = requests.get(f"{BASE_URL}/reddit/monitors", headers=headers)
monitors = response.json()["data"]
for m in monitors:
print(f"{m['keyword']}: {m['mentions_count']} mentions, {m['unread_alerts_count']} unread")
Search mentions with filters
params = {
"subreddit": "seo",
"sort": "score",
"date_from": "2026-01-01",
"per_page": 100,
}
response = requests.get(f"{BASE_URL}/reddit/mentions", headers=headers, params=params)
data = response.json()
for mention in data["data"]:
print(f"r/{mention['subreddit']}: {mention['post_title']} (score: {mention['score']})")
print(f"\nTotal: {data['meta']['total']} mentions across {data['meta']['last_page']} pages")
Live search for a keyword
params = {
"keyword": "your brand",
"time": "day",
"limit": 50,
}
response = requests.get(f"{BASE_URL}/reddit/search", headers=headers, params=params)
result = response.json()
print(f"Found {result['total']} live mentions for '{result['keyword']}'")
for mention in result["data"]:
print(f" [{mention['found_in']}] r/{mention['subreddit']}: {mention['post_title']}")
List all monitors (Guzzle)
$client = new \GuzzleHttp\Client([
'base_uri' => 'https://pageradar.io/api/',
'headers' => [
'Authorization' => 'Bearer YOUR_TOKEN',
'Accept' => 'application/json',
],
]);
$response = $client->get('reddit/monitors');
$monitors = json_decode($response->getBody(), true)['data'];
foreach ($monitors as $monitor) {
echo "{$monitor['keyword']}: {$monitor['mentions_count']} mentions\n";
}
Search mentions with filters
$response = $client->get('reddit/mentions', [
'query' => [
'subreddit' => 'seo',
'sort' => 'score',
'date_from' => '2026-01-01',
'per_page' => 100,
],
]);
$data = json_decode($response->getBody(), true);
foreach ($data['data'] as $mention) {
echo "r/{$mention['subreddit']}: {$mention['post_title']} (score: {$mention['score']})\n";
}
echo "Total: {$data['meta']['total']} mentions\n";
Live search for a keyword
$response = $client->get('reddit/search', [
'query' => [
'keyword' => 'your brand',
'time' => 'day',
'limit' => 50,
],
]);
$result = json_decode($response->getBody(), true);
echo "Found {$result['total']} live mentions for '{$result['keyword']}'\n";
foreach ($result['data'] as $mention) {
echo " [{$mention['found_in']}] r/{$mention['subreddit']}: {$mention['post_title']}\n";
}