API v1 REST · JSON

PageRadar API

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.

Base URL 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.

Authentication & scopes

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:readRead your account identity
sites:readList and read sites
sites:writeCreate sites
sites:deleteDelete a site and every monitor under it. Not in the Manager preset — pick it explicitly
monitors:readList and read monitors
monitors:writeCreate and update monitors
monitors:deleteDelete a monitor and its history
alerts:readRead alert lists
checks:runTrigger 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.

Rate limits

Limits apply per account, per class of operation, on top of an overall 100 req/min ceiling.

Class Limit Applies to
Reads100 / minEvery GET
Writes30 / minPOST, PATCH, DELETE
Check runs6 / minOn-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

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
401unauthenticatedMissing or invalid token
403insufficient_scopeThe token lacks the scope this endpoint needs
404not_foundUnknown id — or one that is not yours
409idempotency_in_flightThe first request with this key is still running; retry shortly
422validation_errorField errors in errors
422quota_exceededYour plan has no room; errors.quota gives the numbers
422idempotency_key_conflictKey reused with a different payload — use a new key
429rate_limitedPer-minute limit; honor Retry-After
429run_budget_exhaustedDaily on-demand check budget spent
503quota_lockedTransient contention on a concurrent write; retry

Pagination

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 }
}

Idempotency

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.

  • Same key, same payload → the stored response, with Idempotency-Replayed: true.
  • Same key, different payload → 422 idempotency_key_conflict. Mint one key per distinct request.
  • Errors and no-op results are never stored, so a corrected retry with the same key runs normally.
-H "Idempotency-Key: $(uuidgen)"

Endpoints · Sites

Sites

A site groups everything you monitor for one domain. Create one first — every monitor hangs off it.

GET /v1/sites sites:read

List sites

Your sites, newest first. Cursor-paginated.

Response 200

{
  "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 }
}
POST /v1/sites sites:write

Create a site

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.

Parameters

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.

Request

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"}'

Response 201

{
  "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" }
}
GET /v1/sites/{site} sites:read

Get a site

One of your sites. 404 if the id is not yours.

DELETE /v1/sites/{site} sites:delete

Delete a site

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.

Response 200

{
  "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

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).

GET /v1/sites/{site}/html-monitors monitors:read

List a site's monitors

POST /v1/sites/{site}/html-monitors monitors:write

Create monitors

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.

Parameters

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}/#

Request

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}'

Response 201

{
  "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 } }
}
GET /v1/html-monitors/{monitor} monitors:read

Get a monitor

Includes the live freshness state. This is also the endpoint you poll after triggering a check: last_checked_at advances when it completes.

PATCH /v1/html-monitors/{monitor} monitors:write

Update a monitor

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.

Request

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}'
DELETE /v1/html-monitors/{monitor} monitors:delete

Delete a monitor

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.

GET /v1/html-monitors/{monitor}/alerts alerts:read

List content-change alerts

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.

Response 200

{
  "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 }
}
GET /v1/html-monitors/{monitor}/freshness-alerts alerts:read

List stuck-content alerts

POST /v1/html-monitors/{monitor}/check checks:run

Run a check now

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.

Outcomes

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.

Stuck-content detection (freshness)

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:

Parameters

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:

Response 200

"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

HTTP status monitors

Uptime and status-code monitoring: know when a URL starts answering 404, 500 or a redirect it should not.

GET /v1/sites/{site}/status-monitors monitors:read

List a site's monitors

POST /v1/sites/{site}/status-monitors monitors:write

Create monitors

Multi-status, exactly like HTML monitors: created / skipped / quota-refused per URL.

Parameters

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.

Request

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"}'
GET /v1/status-monitors/{monitor} monitors:read

Get a monitor

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.

Response 200

{
  "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" }
}
PATCH /v1/status-monitors/{monitor} monitors:write

Update a monitor

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.

Request

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"}'
DELETE /v1/status-monitors/{monitor} monitors:delete

Delete a monitor

Permanent, with its alert history. Prefer deactivating to pause.

GET /v1/status-monitors/{monitor}/alerts alerts:read

List status-change alerts

change_type classifies the transition:

Parameters

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.

POST /v1/status-monitors/{monitor}/check checks:run

Run a check now

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

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:

  • 1 credit = 1 executed check of 1 tracker, debited when the check runs.
  • A tracker watches one country. Covering 20 countries for one link = 20 trackers = 20 credits per cycle.
  • Creating a tracker costs nothing, but each creation queues a check immediately — importing 1000 URLs spends 1000 credits within minutes.
  • Credits are monthly. When they run out, checks stop and resume on the 1st of the next month (next_check_at is pushed there).
Supported countries (169) — send the ISO code, never the label

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.

US GB FR ES DE IT CA AU BR MX IN JP KR SG NL CH SE NO DK FI AF DZ AO BJ BW BF BI CM CF TD CG CI DJ EG GQ ER SZ ET GA GM GH GN GW KE LS LR LY MG MW ML MR MA MZ NA NE NG RW SN SL SO ZA SD TZ TG TN UG BS BZ CR CU DO SV GT HT HN JM NI PA TT AR BO CL CO EC FK GF GY PY PE SR UY VE BD BT BN KH CN ID KZ KG LA MY MN MM NP PK PH LK TJ TH TL TM UZ VN HK TW AL AM AT AZ BY BE BA BG HR CY CZ EE GE GR HU IS IE LV LT LU MT MD ME MK PL PT RO RU RS SK SI TR UA IR IQ IL JO KW LB OM PS QA SA SY AE YE FJ NC NZ PG VU BM PR
GET /v1/sites/{site}/affiliate-trackers monitors:read

List a site's trackers

POST /v1/sites/{site}/affiliate-trackers monitors:write

Create trackers

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.

Parameters

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.

Request

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}'

Response 201

{
  "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.

GET /v1/affiliate-trackers/{tracker} monitors:read

Get a tracker

country_code is ISO 3166-1 alpha-2 and immutable; next_check_at tells you when the scheduler will look again.

Response 200

{
  "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" }
}
PATCH /v1/affiliate-trackers/{tracker} monitors:write

Update a tracker

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.

Request

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}}'
DELETE /v1/affiliate-trackers/{tracker} monitors:delete

Delete a tracker

Permanent: every executed check and every alert go with it. A second call is a 404, so retries are safe. Prefer deactivating to pause.

GET /v1/affiliate-trackers/{tracker}/checks results:read

List executed checks (redirect chains)

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.

Response 200

{
  "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 }
}
POST /v1/affiliate-trackers/{tracker}/check checks:run

Run a check now

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.

Outcomes

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.

GET /v1/affiliate-trackers/{tracker}/alerts alerts:read

List a tracker's alerts

alert_type

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.

Account

GET /v1/user account:read

Who am I

The account behind the token. Useful as a connectivity and scope check.

Response 200

{ "data": { "id": 1, "name": "Sam", "email": "[email protected]" } }

Quickstart

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

API Reddit

historique · non versionnée

La première API de PageRadar, antérieure à la v1. Toujours supportée et inchangée ; les nouvelles intégrations doivent utiliser la v1 ci-dessus.

Accédez à vos données de monitoring PageRadar par programmation. Créez des tableaux de bord personnalisés, intégrez vos outils et automatisez vos workflows de veille de marque.

Base URL https://pageradar.io/api

6

Points d'accès

100/min

Limite

REST

JSON API

Authentification

Toutes les requêtes API nécessitent un token Bearer dans l'en-tête Authorization.

Abonnement requis. Un abonnement PageRadar actif est nécessaire pour utiliser l'API. Voir les tarifs

Comment s'authentifier

  1. Créez un compte et souscrivez à un plan
  2. Allez sur Accès API et créez un token
  3. Incluez le token dans chaque requête :
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
     -H "Accept: application/json" \
     https://pageradar.io/api/reddit/monitors

Limite de requêtes

L'API autorise 100 requêtes par minute par utilisateur authentifié. Les informations de limite sont incluses dans les en-têtes de réponse :

En-tête Description
X-RateLimit-Limit Nombre maximum de requêtes par minute (100)
X-RateLimit-Remaining Requêtes restantes dans la fenêtre actuelle
Retry-After Secondes à attendre (uniquement sur les réponses 429)
HTTP/1.1 429 Too Many Requests
Retry-After: 42

{
  "message": "Too Many Attempts."
}

Pagination

Les endpoints de liste renvoient des résultats paginés avec des objets meta et links pour la navigation.

Paramètre Type Default Description
page integer 1 Numéro de page
per_page integer 50 Éléments par 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
  }
}

Codes d'erreur

Code Signification Description
200 OK Requête réussie
401 Non autorisé Token API manquant ou invalide
403 Interdit Pas d'abonnement actif — mettre à niveau
404 Non trouvé La ressource n'existe pas ou ne vous appartient pas
422 Erreur de validation Paramètres invalides — vérifiez l'objet errors dans la réponse
429 Trop de requêtes Limite dépassée — attendez puis réessayez
500 Erreur serveur Un problème est survenu de notre côté

Exemples de réponses d'erreur

401 Unauthorized

{
  "message": "Unauthenticated."
}

403 Forbidden

{
  "error": "subscription_required",
  "message": "An active subscription is required.",
  "upgrade_url": "https://pageradar.io/pricing"
}

Points d'accès

GET /api/reddit/monitors

Lister les monitors

Renvoie tous les monitors de mots-clés Reddit de l'utilisateur authentifié, avec le nombre de mentions et d'alertes non lues.

Réponse

{
  "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"
    }
  ]
}
GET /api/reddit/monitors/{id}

Détail d'un monitor

Renvoie les détails d'un monitor. Renvoie 404 si non trouvé ou s'il ne vous appartient pas.

Path Paramètres

ParamètreTypeDescription
idintegerID du monitor requis

Exemple

curl -H "Authorization: Bearer YOUR_TOKEN" \
     https://pageradar.io/api/reddit/monitors/1
GET /api/reddit/monitors/{id}/mentions

Mentions d'un monitor

Renvoie les mentions paginées d'un monitor spécifique. Supporte les mêmes filtres que Toutes les mentions.

Exemple

curl -H "Authorization: Bearer YOUR_TOKEN" \
     "https://pageradar.io/api/reddit/monitors/1/mentions?subreddit=seo&per_page=20"
GET /api/reddit/mentions

Toutes les mentions

Renvoie toutes les mentions Reddit de tous vos monitors, avec filtrage, tri et pagination.

Query Paramètres

Paramètre Type Default Description
monitor_id integer - Filtrer par ID de monitor
keyword string - Recherche dans keyword_found (correspondance partielle)
subreddit string - Filtrer par nom de subreddit (ex. seo)
found_in string - Où le mot-clé a été trouvé : title, body ou comment
date_from date - Date de début (YYYY-MM-DD)
date_to date - Date de fin (YYYY-MM-DD)
sort string date date, score, or comments
sort_dir string desc asc or desc
per_page integer 50 Résultats par page (max 100)

Exemple

curl -H "Authorization: Bearer YOUR_TOKEN" \
     "https://pageradar.io/api/reddit/mentions?subreddit=seo&date_from=2026-01-01&sort=score&per_page=20"

Réponse

{
  "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 }
}
GET /api/reddit/mentions/{id}

Détail d'une mention

Renvoie une mention avec tous ses détails. Renvoie 404 si non trouvée ou si elle ne vous appartient pas.

Exemple

curl -H "Authorization: Bearer YOUR_TOKEN" \
     https://pageradar.io/api/reddit/mentions/123
GET /api/user

Info utilisateur

Renvoie les informations de base de l'utilisateur authentifié. Utile pour vérifier que votre token fonctionne.

Réponse

{
  "id": 1,
  "name": "John Doe",
  "email": "[email protected]"
}

Champs de réponse

Objet Monitor

Champ Type Description
idintegerIdentifiant unique du monitor
keywordstringMot-clé surveillé
subredditsarray|nullListe de subreddits spécifiques, ou null pour tous
check_frequencystringhourly, daily, or weekly
is_activebooleanSi le monitor est actif
last_checked_atstring|nullDate/heure ISO 8601 du dernier check
mentions_countintegerNombre total de mentions trouvées
unread_alerts_countintegerNombre d'alertes non lues
created_atstringDate de création ISO 8601
updated_atstringDate de dernière mise à jour ISO 8601

Objet Mention

Champ Type Description
idintegerIdentifiant unique de la mention
monitor_idintegerID du monitor parent
keyword_foundstringLe mot-clé exact qui a matché
reddit_idstringID du post Reddit
post_titlestringTitre du post Reddit
post_urlstringURL directe vers le post Reddit
subredditstringNom du subreddit (sans le préfixe r/)
authorstring|nullNom d'utilisateur Reddit de l'auteur
found_instringtitle, body, or comment
context_textstring|nullExtrait de texte autour du mot-clé trouvé
scoreintegerScore de votes Reddit
num_commentsintegerNombre de commentaires sur le post
comment_idstring|nullID du commentaire (si trouvé dans un commentaire)
comment_authorstring|nullAuteur du commentaire (si trouvé dans un commentaire)
created_at_redditstring|nullDate de création du post sur Reddit (ISO 8601)
created_atstringDate de détection de la mention (ISO 8601)

Exemples de code

Lister tous les monitors

curl -H "Authorization: Bearer YOUR_TOKEN" \
     -H "Accept: application/json" \
     https://pageradar.io/api/reddit/monitors

Rechercher des mentions avec filtres

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"

Recherche en direct d'un mot-clé

curl -H "Authorization: Bearer YOUR_TOKEN" \
     -H "Accept: application/json" \
     "https://pageradar.io/api/reddit/search?keyword=your+brand&time=day&limit=50"

Lister tous les 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`);
});

Rechercher des mentions avec filtres

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})`);

Recherche en direct d'un mot-clé

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})`);
});

Lister tous les 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")

Rechercher des mentions avec filtres

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")

Recherche en direct d'un mot-clé

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']}")

Lister les 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";
}

Rechercher des mentions avec filtres

$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";

Recherche en direct d'un mot-clé

$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";
}