<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Daniel Root</title>
    <description>The latest articles on DEV Community by Daniel Root (@daniel_root_5c360ddb87563).</description>
    <link>https://dev.to/daniel_root_5c360ddb87563</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4144817%2Fe518d3af-9559-41b6-b204-072e06677138.jpg</url>
      <title>DEV Community: Daniel Root</title>
      <link>https://dev.to/daniel_root_5c360ddb87563</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/daniel_root_5c360ddb87563"/>
    <language>en</language>
    <item>
      <title>Let an AI agent set up uptime and change alerts with one API call</title>
      <dc:creator>Daniel Root</dc:creator>
      <pubDate>Wed, 30 Sep 2026 14:10:47 +0000</pubDate>
      <link>https://dev.to/daniel_root_5c360ddb87563/let-an-ai-agent-set-up-uptime-and-change-alerts-with-one-api-call-2gnf</link>
      <guid>https://dev.to/daniel_root_5c360ddb87563/let-an-ai-agent-set-up-uptime-and-change-alerts-with-one-api-call-2gnf</guid>
      <description>&lt;p&gt;AI agents are good at fetching a page once and reasoning about it. They are less good at the boring middle: a durable schedule, a paid subscription, and a signed delivery channel that keeps working after the chat ends. That is the gap we built PingWhen's agent API for.&lt;/p&gt;

&lt;p&gt;If your agent can read a short doc, POST JSON, and hand a human a checkout link, it can set up page-change and uptime alerts without scraping, cron, or a free-tier dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the agent needs to know first
&lt;/h2&gt;

&lt;p&gt;Two public docs are enough:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://pingwhen.app/llms.txt" rel="noopener noreferrer"&gt;https://pingwhen.app/llms.txt&lt;/a&gt; — short product summary for LLM loaders&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pingwhen.app/for-agents" rel="noopener noreferrer"&gt;https://pingwhen.app/for-agents&lt;/a&gt; — the create / poll / webhook / cancel flow&lt;/li&gt;
&lt;li&gt;OpenAPI at &lt;a href="https://pingwhen.app/v1/openapi.json" rel="noopener noreferrer"&gt;https://pingwhen.app/v1/openapi.json&lt;/a&gt; if you want a machine-readable schema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no free plan and no agent-only price. Agents pay the same Stripe subscription as humans: &lt;strong&gt;Starter $9/mo&lt;/strong&gt; (10 watches, every 15 min) or &lt;strong&gt;Plus $19/mo&lt;/strong&gt; (40 watches, every 5 min).&lt;/p&gt;

&lt;h2&gt;
  
  
  The flow in five steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Read &lt;code&gt;/llms.txt&lt;/code&gt; or &lt;code&gt;/for-agents&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /v1/watches&lt;/code&gt; with the URL to watch and where to send alerts.&lt;/li&gt;
&lt;li&gt;Give the human the returned &lt;code&gt;checkout_url&lt;/code&gt; (or open it in an approved browser tool).&lt;/li&gt;
&lt;li&gt;Poll &lt;code&gt;GET /v1/watches/:id?token=...&lt;/code&gt; until &lt;code&gt;status&lt;/code&gt; is &lt;code&gt;active&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Receive signed webhook POSTs when the page changes, goes down, or comes back.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Payment is required before polling starts. The create call inserts the watch as &lt;code&gt;pending_payment&lt;/code&gt;, opens Stripe Checkout, and returns immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a watch
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://pingwhen.app/v1/watches &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "url": "https://example.com/status",
    "notify": "https://agent.example/hooks/pingwhen",
    "events": ["change", "down", "up"],
    "plan": "starter",
    "success_url": "https://agent.example/paid",
    "cancel_url": "https://agent.example/abandoned"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;notify&lt;/code&gt; may be an HTTPS webhook URL (preferred for agents), an email, or a US/CA phone number. &lt;code&gt;events&lt;/code&gt; is any subset of &lt;code&gt;change&lt;/code&gt;, &lt;code&gt;down&lt;/code&gt;, and &lt;code&gt;up&lt;/code&gt;. &lt;code&gt;plan&lt;/code&gt; is &lt;code&gt;starter&lt;/code&gt; or &lt;code&gt;plus&lt;/code&gt;. &lt;code&gt;success_url&lt;/code&gt; and &lt;code&gt;cancel_url&lt;/code&gt; are optional Stripe return URLs.&lt;/p&gt;

&lt;p&gt;A typical &lt;strong&gt;201&lt;/strong&gt; response looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"watch_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"w_..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending_payment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"manage_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mt_..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"starter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checkout_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://checkout.stripe.com/c/pay/cs_..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"poll"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GET /v1/watches/w_...?token=mt_..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cancel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DELETE /v1/watches/w_...?token=mt_..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"instructions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Open checkout_url in a browser. Then poll until status=active."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store &lt;code&gt;manage_token&lt;/code&gt;. You need it to poll, cancel, and verify webhook signatures. If &lt;code&gt;notify&lt;/code&gt; is an email for an existing customer who still has spare quota, Checkout is skipped and &lt;code&gt;status&lt;/code&gt; comes back &lt;code&gt;active&lt;/code&gt; right away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Poll until active
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="s2"&gt;"https://pingwhen.app/v1/watches/w_...?token=mt_..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Status values: &lt;code&gt;pending_payment&lt;/code&gt; | &lt;code&gt;active&lt;/code&gt; | &lt;code&gt;past_due&lt;/code&gt; | &lt;code&gt;canceled&lt;/code&gt; | &lt;code&gt;expired&lt;/code&gt;. Once active, the JSON includes a Stripe Customer Portal URL so the human can manage billing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify webhook signatures
&lt;/h2&gt;

&lt;p&gt;Alerts to an HTTPS &lt;code&gt;notify&lt;/code&gt; URL are JSON POSTs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"change"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"watch_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"w_..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checked_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-20T20:04:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"http_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"excerpt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text now includes $19/mo"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;event&lt;/code&gt; is one of &lt;code&gt;change&lt;/code&gt; | &lt;code&gt;down&lt;/code&gt; | &lt;code&gt;up&lt;/code&gt; | &lt;code&gt;active&lt;/code&gt;. Headers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;User-Agent: PingWhenBot/1.0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X-PingWhen-Signature: sha256=&amp;lt;hex&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;X-PingWhen-Event: change&lt;/code&gt; (same as the body &lt;code&gt;event&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The signature is &lt;strong&gt;HMAC-SHA256 of the raw request body&lt;/strong&gt;, using &lt;code&gt;manage_token&lt;/code&gt; as the secret. Header format is exactly &lt;code&gt;sha256=&lt;/code&gt; plus the hex digest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyPingWhen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signatureHeader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;manageToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;manageToken&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signatureHeader&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Python:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_pingwhen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signature_header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;manage_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;manage_token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;raw_body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signature_header&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the raw bytes you received, not a re-serialized JSON object. Respond with &lt;strong&gt;410 Gone&lt;/strong&gt; (or &lt;code&gt;{"ok":false,"unsubscribe":true}&lt;/code&gt;) to delete the watch. We retry failed deliveries a couple of times before giving up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits (read these before you wire it in)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;We watch the &lt;strong&gt;HTML we receive&lt;/strong&gt;: visible text after scripts/styles are stripped. We do &lt;strong&gt;not&lt;/strong&gt; run a browser or execute JavaScript. If the words you care about are missing from view-source, use a JS-capable monitor or watch the JSON API the page loads instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No free plan.&lt;/strong&gt; Starter is $9/mo for 10 watches every 15 minutes; Plus is $19/mo for 40 watches every 5 minutes.&lt;/li&gt;
&lt;li&gt;Watch URLs go through &lt;strong&gt;SSRF protections&lt;/strong&gt; (public HTTP(S) only). Private/local targets are refused.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;first check is a silent baseline&lt;/strong&gt;. You get alerted on later changes, not on the initial snapshot.&lt;/li&gt;
&lt;li&gt;Webhook destinations must be HTTPS. Email and US/CA SMS also work if that fits the human better than a hook.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not sure whether a URL is a good candidate? The free one-shot &lt;a href="https://pingwhen.app/check" rel="noopener noreferrer"&gt;page check&lt;/a&gt; fetches it the same way our watcher would and shows status, size, and a text preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits in an agent loop
&lt;/h2&gt;

&lt;p&gt;A practical pattern: the human asks the agent to "watch this status page and ping my webhook if it dies or the copy changes." The agent reads &lt;code&gt;/for-agents&lt;/code&gt;, creates the watch, returns the checkout link, polls to confirm payment, and then treats signed webhooks as durable events. Cancel a single watch with &lt;code&gt;DELETE /v1/watches/:id?token=...&lt;/code&gt;; cancel the subscription through the portal URL once the watch is active.&lt;/p&gt;

&lt;p&gt;We built PingWhen for the simple case — public, server-rendered pages and JSON URLs — not as a general browser automation platform. If that matches what your agent needs, start at &lt;a href="https://pingwhen.app/for-agents" rel="noopener noreferrer"&gt;/for-agents&lt;/a&gt; or the homepage at &lt;a href="https://pingwhen.app" rel="noopener noreferrer"&gt;pingwhen.app&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://pingwhen.app/blog/let-an-ai-agent-set-up-uptime-and-change-alerts" rel="noopener noreferrer"&gt;PingWhen blog&lt;/a&gt;. Written with AI assistance for PingWhen, the page-change and uptime alert tool we make.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>webhooks</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Monitor your robots.txt and sitemap for accidental changes</title>
      <dc:creator>Daniel Root</dc:creator>
      <pubDate>Sat, 26 Sep 2026 20:21:15 +0000</pubDate>
      <link>https://dev.to/daniel_root_5c360ddb87563/monitor-your-robotstxt-and-sitemap-for-accidental-changes-45ac</link>
      <guid>https://dev.to/daniel_root_5c360ddb87563/monitor-your-robotstxt-and-sitemap-for-accidental-changes-45ac</guid>
      <description>&lt;p&gt;Two SEO accidents come up again and again:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A staging &lt;code&gt;robots.txt&lt;/code&gt; with &lt;code&gt;Disallow: /&lt;/code&gt; gets deployed to production.&lt;/li&gt;
&lt;li&gt;A migration or plugin update changes the sitemap generator, and whole sections disappear from &lt;code&gt;sitemap.xml&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither breaks the site for visitors, so nobody notices. Search engines do notice, just slowly: Google says it &lt;a href="https://developers.google.com/search/docs/crawling-indexing/robots/robots_txt" rel="noopener noreferrer"&gt;generally caches robots.txt for up to 24 hours&lt;/a&gt;, and the effect on indexing and traffic can take much longer to show up in reports. By the time someone asks "why is organic down?", the cause is weeks old.&lt;/p&gt;

&lt;p&gt;The fix is boring: watch the few files that control crawling, and add a smoke test to your deploys.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to watch
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/robots.txt&lt;/code&gt;&lt;/strong&gt;: plain text, small, and should almost never change. Any change is worth a human look.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your sitemap index and the child sitemaps that matter.&lt;/strong&gt; If your sitemap is split (&lt;code&gt;sitemap-pages.xml&lt;/code&gt;, &lt;code&gt;sitemap-products.xml&lt;/code&gt;...), watch the index plus the ones for your most important sections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The visible text of 3-5 money pages&lt;/strong&gt; (home, pricing, top landing pages), which catches a template change that wipes copy.&lt;/li&gt;
&lt;li&gt;Other plain-text control files you rely on, such as &lt;code&gt;/ads.txt&lt;/code&gt; or &lt;code&gt;/.well-known/security.txt&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What a text-change monitor will not catch
&lt;/h2&gt;

&lt;p&gt;Most change monitors, ours included, compare a page's visible text. That's great for robots.txt and sitemaps, but several indexing controls live in markup or headers, not in text:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;meta name="robots" content="noindex"&amp;gt;&lt;/code&gt; (an attribute, not visible text)&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;X-Robots-Tag: noindex&lt;/code&gt; HTTP header&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rel="canonical"&lt;/code&gt; and &lt;code&gt;hreflang&lt;/code&gt; changes&lt;/li&gt;
&lt;li&gt;tags injected by JavaScript after load&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For those, use Search Console's indexing reports, a scheduled crawl (Screaming Frog, Sitebulb and similar tools can run on a schedule), or better, a check that runs on every deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  A post-deploy SEO smoke test
&lt;/h2&gt;

&lt;p&gt;Drop this into your CI after deploy (or run it from cron). It exits non-zero if production looks de-indexed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# seo-smoke.sh: fail loudly if production is telling crawlers to go away&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-uo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;site&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://example.com"&lt;/span&gt;
&lt;span class="nv"&gt;pages&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;/ /pricing /blog&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0

&lt;span class="nv"&gt;robots&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$site&lt;/span&gt;&lt;span class="s2"&gt;/robots.txt"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"robots.txt not reachable"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Eiq&lt;/span&gt; &lt;span class="s1"&gt;'^[[:space:]]*disallow:[[:space:]]*/[[:space:]]*$'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$robots&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"robots.txt contains 'Disallow: /'. Check which user-agent group it's in"&lt;/span&gt;
  &lt;span class="nv"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="k"&gt;fi

for &lt;/span&gt;path &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsSI&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$site$path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;: not reachable"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$site$path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qi&lt;/span&gt; &lt;span class="s1"&gt;'^x-robots-tag:.*noindex'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$headers&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;: X-Robots-Tag noindex"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
  &lt;span class="k"&gt;fi
  if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Eiq&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;meta[^&amp;gt;]*name=.?robots[^&amp;gt;]*noindex'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;: meta robots noindex"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
  &lt;span class="k"&gt;fi
done

&lt;/span&gt;&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$site&lt;/span&gt;&lt;span class="s2"&gt;/sitemap.xml"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;loc&amp;gt;'&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"sitemap.xml lists &lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="s2"&gt; URLs"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; 10 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"sitemap looks too small"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="nv"&gt;$fail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adjust before trusting it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The robots check flags &lt;code&gt;Disallow: /&lt;/code&gt; anywhere in the file, including groups for specific bots you meant to block. Treat a hit as "look at this", not proof.&lt;/li&gt;
&lt;li&gt;The meta check assumes &lt;code&gt;name&lt;/code&gt; comes before &lt;code&gt;content&lt;/code&gt;, which is the common order but not guaranteed.&lt;/li&gt;
&lt;li&gt;Set the sitemap threshold to something meaningful for your site, e.g. 80% of last week's count. If you use a sitemap index, count the child sitemaps instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Between deploys: change alerts
&lt;/h2&gt;

&lt;p&gt;The smoke test covers your deploys. It doesn't cover a CMS plugin update at 2am, a teammate editing robots.txt in an admin panel, or a hosting platform "helpfully" changing defaults. That's where a change monitor on the files above earns its keep.&lt;/p&gt;

&lt;p&gt;Some practical notes for sitemaps specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your generator rewrites &lt;code&gt;&amp;lt;lastmod&amp;gt;&lt;/code&gt; on every build, a text monitor will alert on every deploy. Full ISO timestamps like &lt;code&gt;2026-09-26T10:00:00+00:00&lt;/code&gt; are ignored by monitors that mask timestamps (PingWhen does), but date-only values like &lt;code&gt;2026-09-26&lt;/code&gt; are not. Watch a child sitemap that changes rarely, or accept a deploy-time alert.&lt;/li&gt;
&lt;li&gt;Very large sitemaps (tens of thousands of URLs) are better handled by the count check above than by a diff.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  With PingWhen
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pingwhen.app" rel="noopener noreferrer"&gt;PingWhen&lt;/a&gt; watches public URLs and emails you, or POSTs a signed JSON webhook, when their text changes, when they go down, and when they come back. Because robots.txt is plain text, every edit produces an alert with a short excerpt of the added text. Point the webhook at your CI (anything that can take a JSON POST) and you can re-run the smoke test automatically whenever robots.txt or a sitemap changes.&lt;/p&gt;

&lt;p&gt;It's $9/month for 10 URLs checked every 15 minutes, and there's no free plan. You can check whether your files are reachable and readable with our free &lt;a href="https://pingwhen.app/check" rel="noopener noreferrer"&gt;page check&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://pingwhen.app/blog/monitor-robots-txt-and-sitemap-changes" rel="noopener noreferrer"&gt;PingWhen blog&lt;/a&gt;. Written with AI assistance for PingWhen, the page-change and uptime alert tool we make.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>devops</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>How to get notified when a web page changes (without writing a scraper)</title>
      <dc:creator>Daniel Root</dc:creator>
      <pubDate>Sat, 26 Sep 2026 20:21:11 +0000</pubDate>
      <link>https://dev.to/daniel_root_5c360ddb87563/how-to-get-notified-when-a-web-page-changes-without-writing-a-scraper-531c</link>
      <guid>https://dev.to/daniel_root_5c360ddb87563/how-to-get-notified-when-a-web-page-changes-without-writing-a-scraper-531c</guid>
      <description>&lt;p&gt;You want to know when a page changes: a "registration opens soon" notice, a competitor's pricing table, a changelog with no RSS, a government page that says "updates will be posted here". Most guides jump straight to a tool. It's worth spending five minutes first, because the right URL matters more than the right tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 0: check whether a feed already exists
&lt;/h2&gt;

&lt;p&gt;A feed is the most reliable change notification there is, and it's free.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Look in the page source for &lt;code&gt;&amp;lt;link rel="alternate" type="application/rss+xml"&amp;gt;&lt;/code&gt; or &lt;code&gt;application/atom+xml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Try the usual paths: &lt;code&gt;/feed&lt;/code&gt;, &lt;code&gt;/rss&lt;/code&gt;, &lt;code&gt;/atom.xml&lt;/code&gt;, &lt;code&gt;/index.xml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;GitHub has Atom feeds for releases (&lt;code&gt;https://github.com/OWNER/REPO/releases.atom&lt;/code&gt;), tags (&lt;code&gt;/tags.atom&lt;/code&gt;) and commits (&lt;code&gt;/commits/main.atom&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Subreddits have one at &lt;code&gt;https://www.reddit.com/r/NAME/.rss&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If there's a feed, put it in any feed reader (or an automation tool that emails you new items) and stop here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: the view-source test
&lt;/h2&gt;

&lt;p&gt;Open the page, then open its source: &lt;code&gt;Ctrl+U&lt;/code&gt; / &lt;code&gt;Cmd+Option+U&lt;/code&gt;, or prefix the address with &lt;code&gt;view-source:&lt;/code&gt;. Search for the words you care about.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The text is in the source.&lt;/strong&gt; The page is server-rendered. Any text-based monitor will see what you see, and a simple, cheap tool is enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The source is mostly &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags and an empty &lt;code&gt;&amp;lt;div id="root"&amp;gt;&lt;/code&gt;.&lt;/strong&gt; The page is rendered by JavaScript in the browser. A plain HTTP fetch sees an empty shell.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For JavaScript pages you have two options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find the data URL.&lt;/strong&gt; Open DevTools, go to the Network tab, filter by Fetch/XHR, and reload. The page is usually loading JSON from an API. If that JSON URL works when you open it in a new tab (no login cookie needed), watch the JSON instead of the page. It's smaller, it changes only when the data changes, and many monitors handle JSON well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a monitor that runs a real browser&lt;/strong&gt; (Visualping, PageCrawl, or self-hosted changedetection.io with its Playwright fetcher). These are heavier and usually cost more per check, but they work on single-page apps.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 2: pick the narrowest URL you can
&lt;/h2&gt;

&lt;p&gt;Whatever tool you use, noise usually comes from the page, not the tool. Before you set anything up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prefer the specific page (one product, one notice, one job posting) over a homepage or listing.&lt;/li&gt;
&lt;li&gt;Look for a print view, a "text only" version, or a filtered URL (&lt;code&gt;?category=...&lt;/code&gt;) that shows only what you need.&lt;/li&gt;
&lt;li&gt;Watch out for text that changes on every load: "posted 5 minutes ago", rotating testimonials, view counters, "people are looking at this now". A monitor that compares the whole page will alert on every one of those. Either pick a different URL or use a tool that lets you select a region.&lt;/li&gt;
&lt;li&gt;Markup-only churn (cache-busting file names, CSRF tokens in attributes, analytics snippets) is harmless for monitors that compare &lt;em&gt;visible text&lt;/em&gt;. It's a problem for ones that compare raw HTML.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: choose how you want to be alerted
&lt;/h2&gt;

&lt;p&gt;There are three broad families. None is "best"; they trade money for effort differently.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;th&gt;Good at&lt;/th&gt;
&lt;th&gt;Watch out for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Browser extension, local checks&lt;/td&gt;
&lt;td&gt;Distill (local monitors)&lt;/td&gt;
&lt;td&gt;Free, can see pages you're logged into&lt;/td&gt;
&lt;td&gt;Only checks while your browser is running&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-hosted&lt;/td&gt;
&lt;td&gt;urlwatch, changedetection.io&lt;/td&gt;
&lt;td&gt;Free and flexible (selectors, filters, JS rendering)&lt;/td&gt;
&lt;td&gt;You run and update it, and it's down when your box is&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosted service&lt;/td&gt;
&lt;td&gt;Visualping, PageCrawl, ChangeTower, PingWhen&lt;/td&gt;
&lt;td&gt;Checks while your laptop is closed, no maintenance&lt;/td&gt;
&lt;td&gt;Monthly cost; free tiers are usually small&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you're comfortable with a terminal, the smallest possible self-hosted version is a cron job. This one hashes the page's text and emails you when the hash changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# watch.sh URL: run from cron, e.g. */15 * * * * /home/me/watch.sh https://example.com/notice&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.pagewatch/&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sha256sum&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-c1-16&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nv"&gt;new&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 20 &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s1"&gt;'pagewatch/1.0'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/&amp;lt;script[^&amp;gt;]*&amp;gt;.*&amp;lt;\/script&amp;gt;//g'&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/&amp;lt;[^&amp;gt;]*&amp;gt;/ /g'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s1"&gt;'[:space:]'&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sha256sum&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;old&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$old&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$new&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$old&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt; changed"&lt;/span&gt; | mail &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"Page changed: &lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; you@example.com
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$new&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Honest caveats: the &lt;code&gt;sed&lt;/code&gt; is crude (a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; block that spans several lines survives it), it needs a working &lt;code&gt;mail&lt;/code&gt; command, and because of &lt;code&gt;curl -f&lt;/code&gt; it silently skips a run when the site is down instead of telling you. It's a fine starting point for one or two pages. Past that, a real tool pays for itself in time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: expect a baseline, then tune
&lt;/h2&gt;

&lt;p&gt;Every monitor has to store a first copy before it can tell you anything changed, so the first check is silent. If you then get alerts that don't matter, go back to Step 2 before you blame the tool. A narrower URL fixes most noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where PingWhen fits
&lt;/h2&gt;

&lt;p&gt;We built &lt;a href="https://pingwhen.app" rel="noopener noreferrer"&gt;PingWhen&lt;/a&gt; for the simple case: a public, server-rendered page (or a JSON URL) where you just want an email or a webhook when the text changes, plus a "down" and "back up" alert on the same URL. It compares the page's visible text (scripts and styles removed, ISO timestamps masked, JSON keys sorted) and sends one alert with a short excerpt of what's new. It doesn't render JavaScript or let you pick a CSS selector, and there's no free plan: it's $9/month for 10 pages checked every 15 minutes.&lt;/p&gt;

&lt;p&gt;Not sure whether a page is a good candidate? Our free &lt;a href="https://pingwhen.app/check" rel="noopener noreferrer"&gt;page check&lt;/a&gt; fetches it once, the way our watcher would, and tells you whether it's up and whether we can see its text.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://pingwhen.app/blog/get-notified-when-a-web-page-changes" rel="noopener noreferrer"&gt;PingWhen blog&lt;/a&gt;. Written with AI assistance for PingWhen, the page-change and uptime alert tool we make.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>automation</category>
      <category>monitoring</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
