# PingWhen for agents

Pay the same Starter ($9/mo, 10 watches, every 15 min) or Plus ($19/mo, 40 watches, every 5 min) Stripe subscription as humans. No agent-only price. No x402 required.

## Alert types

- **Website watch**: a public URL changes, goes down, or comes back up. `POST https://pingwhen.app/v1/watches`
- **Food recalls**: an FDA or USDA FSIS food recall may include food sold in your state (or nationwide), optionally only if it mentions a food. Each state has a page, such as /recalls/food/texas. Email confirms, then Starter at $9/mo. Webhooks pay at checkout. `POST https://pingwhen.app/v1/recalls/subscribe`
- **Vehicle and product recalls**: a public NHTSA recall may apply to your year, make, and model or VIN, or a CPSC recall to a product (/recalls/products). Email confirms, then Starter at $9/mo. Webhooks pay at checkout. `POST https://pingwhen.app/v1/recalls/subscribe`
- **River flow**: a USGS gauge crosses a discharge (cfs) or gage-height (ft) line you set. `POST https://pingwhen.app/river-flow/watch`
- **Global Entry interview**: a Global Entry, NEXUS, or SENTRI interview opens at an enrollment center. One webhook is free and active immediately, every 15 minutes. A second center, or a check every minute, is Starter ($9). `POST https://pingwhen.app/global-entry/watch`
- **County tax sales**: an official county tax-sale page changes; for parcel-list counties, pick parcels added, removed, or a new sale date. `POST https://pingwhen.app/tax-sale/watch`
- **SEC filings**: a company files with the SEC (any form, 8-K, Form 4 insider, 13D/13G, 10-K/10-Q, or S-1). One ticker free, checked every hour; a webhook starts with no browser. Starter checks every 5 minutes. `POST https://pingwhen.app/v1/sec-filings`
- **Drug recalls**: an FDA drug recall matches a drug, brand, or firm. Class I only, or any class. 1 free by webhook with no browser step; email confirms by link. `POST https://pingwhen.app/v1/drug-recalls/watch`
- **Campground alerts**: a federal campground is listed open after being closed. Forest Service status, and National Park Service season dates plus closure alerts, checked once a day. Not a reservation or a cancellation list. `POST https://pingwhen.app/campsites/watch`
- **Device recalls**: an FDA device recall matches a device, product code, or firm. Class I only, or any class. 1 free by webhook with no browser step; email confirms by link. `POST https://pingwhen.app/v1/device-recalls/watch`

### Website watch

Any public URL. Alerts when the visible text changes, when it goes down, and when it comes back up. Starter $9/mo (10 watches, every 15 min) or Plus $19/mo (40 watches, every 5 min).

`POST https://pingwhen.app/v1/watches` (JSON)

```json
{
  "url": "https://example.com",
  "notify": "https://webhook.site/your-id",
  "events": ["change", "down"],
  "plan": "starter"
}
```

`notify` may be a webhook URL (preferred) or an email.

We check that the URL is public, create the watch as pending payment, and return a Stripe checkout link. Response `201`:

```json
{
  "watch_id": "w_...",
  "status": "pending_payment",
  "manage_token": "mt_...",
  "plan": "starter",
  "checkout_url": "https://checkout.stripe.com/c/pay/cs_live_...",
  "checkout_expires_at": "2026-10-18T01:23:37.000Z",
  "poll": "GET /v1/watches/w_...?token=mt_...",
  "cancel": "DELETE /v1/watches/w_...?token=mt_...",
  "instructions": "Open checkout_url in a browser. A human or an authorized agent wallet/card completes the Stripe subscription. Then poll until status=active."
}
```

Poll before payment:

```json
{
  "watch_id": "w_...",
  "status": "pending_payment",
  "url": "https://example.com/",
  "notify": "https://webhook.site/your-id",
  "events": ["change", "down"],
  "last_status": null,
  "last_checked_at": null,
  "last_http": null,
  "portal_url": null
}
```

`DELETE /v1/watches/w_...?token=mt_...` answers `204`.

An email that belongs to a paying customer with a free slot skips Checkout and comes back `active`. Human form: `POST https://pingwhen.app/go` with `notify`, `url`, `watch_change`, `watch_down`, `watch_up`. Landing pages: /website-change-alert, /page-change-notification, /alert-me-when-website-changes, /uptime-email-alert, /notify-when-page-comes-back-up, /visualping-alternative, /distill-io-alternative, /uptimerobot-email-only, /watch-competitor-pricing-page.

### Food recalls

Public FDA food enforcement reports and USDA FSIS recalls for meat, poultry, and egg products, matched on where each notice says the food was sold. FDA's `distribution_pattern` and FSIS's state list are read as state codes; a notice that says nationwide or "the U.S.", or names no state, counts for every state; a few cities (Los Angeles, Chicago, …) count for their state; a notice sold only in other countries ("Canada") is left out (online sales and US territories such as Guam are not). The first check is a quiet baseline. An email watch for a whole state (no `food`) is a digest: at most one email a day at 7 AM in that state's time zone (Eastern for nationwide), or with `every` `weekly` one on Thursdays at 7 AM, FDA and FSIS merged, most serious first (Class I, Class II, public health alert, Class III). A watch with `food`, and every webhook watch, is checked about every 12 hours and sends as soon as a check finds a new recall: one email, or one webhook POST per check. FDA files one report per product, so reports from one recall event are grouped into one recall. Each email lists up to 10 recalls and links to a page with all of them; digest emails also link to switch between daily and weekly. A food watch uses a product slot: by email the confirm link opens Starter checkout ($9/mo) unless the address already has a plan with room. Pages: https://pingwhen.app/recalls/food, one page per state at https://pingwhen.app/recalls/food/<state> (for example https://pingwhen.app/recalls/food/texas, https://pingwhen.app/recalls/food/new-york, https://pingwhen.app/recalls/food/district-of-columbia, and https://pingwhen.app/recalls/food/puerto-rico; a two-letter code such as https://pingwhen.app/recalls/food/tx redirects), and https://pingwhen.app/recalls/food/<food>. All of these are in https://pingwhen.app/sitemap.xml.

`POST https://pingwhen.app/v1/recalls/subscribe` (JSON)

| field | value |
| --- | --- |
| `email` | where to send alerts. The watch stays `pending_confirm` until the link is opened. That link opens Starter checkout unless the address already has a plan with room. |
| `notify` | instead of `email`: an https webhook URL, one watch on Starter $9/mo (10 watches, every 15 min) or Plus $19/mo (40 watches, every 5 min), paid at Stripe Checkout |
| `state` | a two-letter code (`TX`), a state name (`Texas`), or `US` / `nationwide` for recalls with no specific states |
| `food` | optional: only notices that mention this food, brand, or product (`peanut butter`, `Trader Joe's`) |
| `every` | optional, email with no `food` only: `daily` (default) or `weekly`. The watch `url` carries `&every=weekly`. |

```json
{ "email": "you@example.com", "state": "TX", "food": "peanut butter" }
```

Response `202`, the same for every address (an address that already has this alert is told so by email):

```json
{
  "status": "pending_confirm",
  "state": "check_email",
  "message": "Check your email. We sent a link to that address. This alert is $9 a month. Open the link to turn it on.",
  "url": "https://pingwhen.app/recalls/food?state=TX&q=Peanut%20butter",
  "match": "food",
  "disclaimer": "These are possible matches from public FDA and USDA FSIS data. Verify on the official notice."
}
```

A webhook watch answers `status` `pending_payment`, `plan` `starter`, plus `watch_id`, `manage_token`, `checkout_url`, `poll`, and `cancel`. After payment, each signed `recall` POST (`X-PingWhen-Event: recall`, `X-PingWhen-Signature: sha256=<HMAC-SHA256 of the raw body, keyed with manage_token>`) is:

```json
{
  "event": "recall",
  "watch_id": "w_...",
  "url": "https://pingwhen.app/recalls/food?state=TX&q=Peanut%20butter",
  "checked_at": "2026-10-17T02:05:43.000Z",
  "disclaimer": "These are possible matches from public FDA and USDA FSIS data. Verify on the official notice.",
  "state": "TX",
  "food": "Peanut butter",
  "recalls": [
    {
      "id": "fda:H-2001-2026",
      "source": "FDA",
      "title": "Creamy peanut butter, 16 oz",
      "firm": "Example Foods",
      "url": "https://www.accessdata.fda.gov/scripts/ires/index.cfm?Event=99001",
      "sold_in": ["TX", "OK", "LA"]
    }
  ]
}
```

`sold_in` is a list of state codes, `"nationwide"`, or `"not_stated"`. Each recall is delivered once per watch. Human form: `POST https://pingwhen.app/recalls/subscribe` with `food=1`, `notify`, `state`, and optional `mentions` and `every`; email answers `HTTP/2 303` to `/check-email/ce_...` (the same page for every address). A watch sent with `food` and no `state` matches that food anywhere, as before.

### Vehicle and product recalls

Public NHTSA vehicle recalls (matched by make, model and year, or a VIN decoded with NHTSA vPIC) and CPSC product recalls (by category or keyword). Pages: https://pingwhen.app/recalls/vehicles and https://pingwhen.app/recalls/products. Checked about once a day; the first check is a quiet baseline. An alert is a candidate match, not a VIN-exact confirmation.

**Webhook (one vehicle or product per watch).** Same as river and tax-sale webhooks: https only, private and metadata addresses refused, one watch on Starter $9/mo (10 watches, every 15 min) or Plus $19/mo (40 watches, every 5 min), confirmed by paying at Stripe Checkout.

`POST https://pingwhen.app/v1/recalls/subscribe` (JSON)

```json
{ "notify": "https://webhook.site/your-id", "vehicles": ["2021 Chevrolet Silverado"] }
```

Instead of `vehicles` (one entry: year make model, or a VIN), send `category` (car-seats, cribs, strollers, toys, batteries, furniture) or `keyword` (3+ characters). Response `201`:

```json
{
  "watch_id": "w_...",
  "status": "pending_payment",
  "manage_token": "mt_...",
  "plan": "starter",
  "url": "https://pingwhen.app/recalls/chevrolet/silverado/2021",
  "checkout_url": "https://checkout.stripe.com/c/pay/cs_live_...",
  "poll": "GET /v1/watches/w_...?token=mt_...",
  "cancel": "DELETE /v1/watches/w_...?token=mt_..."
}
```

More than one vehicle or product answers `400 {"error":"one_subject"}`. After payment an `active` event arrives, then one signed `recall` POST per check that finds new recalls (`X-PingWhen-Event: recall`, `X-PingWhen-Signature: sha256=<HMAC-SHA256 of the raw body, keyed with manage_token>`):

```json
{
  "event": "recall",
  "watch_id": "w_...",
  "url": "https://pingwhen.app/recalls/chevrolet/silverado/2021",
  "checked_at": "2026-10-17T02:05:43.000Z",
  "disclaimer": "These are possible matches from public recall data. Confirm with NHTSA or the manufacturer using your VIN.",
  "make": "Chevrolet",
  "model": "Silverado",
  "year": 2021,
  "campaigns": [
    {
      "id": "26V083000",
      "component": "POWER TRAIN:AUTOMATIC TRANSMISSION:CONTROL MODULE:SOFTWARE",
      "summary": "General Motors, LLC is recalling certain 2020 Chevrolet Silverado 2500, 3500, GMC Sierra 2500, 3500, and 2020-2022 Chevrolet Silverado 1500 and GMC Sierra 1500 vehicles that may have been repaired incorrectly under recall 24V797. ..."
    }
  ]
}
```

A product watch sends `category` or `keyword` and `recalls: [{id, title, url}]` instead of the vehicle fields. Human form: `POST https://pingwhen.app/recalls/subscribe` with `notify` (email or https webhook URL), `area` (`vehicles` or `products`) and `watch` (e.g. `2021 Chevrolet Silverado`, a VIN, or `Car seats`); a webhook answers `HTTP/2 303` to `https://checkout.stripe.com/...`.

**Email (vehicle or product).** Starter $9/mo covers 50 vehicles and 10 product watches. Plus $19/mo covers 500 vehicles. The confirm link opens checkout unless that address already has a plan with room.

```json
{ "email": "you@example.com", "vehicles": ["2018 Honda Accord"] }
```

Response `202`, the same for every address:

```json
{
  "status": "pending_confirm",
  "state": "check_email",
  "message": "Check your email. We sent a link to that address. This alert is $9 a month. Open the link to turn it on.",
  "match": "make_model_year",
  "disclaimer": "Candidate recalls from public NHTSA/CPSC data. Not a VIN-exact confirmation."
}
```

Nothing starts until the emailed link is opened. That link opens Starter checkout at $9 a month unless the address already has a plan with room. An account that already pays and needs a bigger plan is asked to sign in and upgrade. An email account on Plus can also add `"webhook"` to copy its alerts to a webhook.

### River flow

Any USGS stream gauge. Alerts once when a fresh reading crosses a line you set, then stays quiet until it moves back past the line. One watch on Starter $9/mo (10 watches, every 15 min) or Plus $19/mo (40 watches, every 5 min). A free account does not include watches.

`POST https://pingwhen.app/river-flow/watch`. Form posts are `application/x-www-form-urlencoded`. Send `Origin: https://pingwhen.app` as a browser would. They answer with a redirect, not JSON.

| field | value |
| --- | --- |
| `notify` | email, or an https webhook URL |
| `gauge` | a gauge name from the list (`Big Hole River near Melrose, MT`) or any USGS site number, 7 to 15 digits (`06025500`) |
| `direction` | `above` or `below` |
| `parameter` | `00060` discharge in cfs, or `00065` gage height in ft |
| `threshold` | a number: 0 to 2,000,000 cfs, or -50 to 200 ft |
| `from` | optional page to come back to on an error, e.g. `/river-flow/mt/big-hole` |

Gauge lookup: the named gauges are on https://pingwhen.app/river-flow and https://pingwhen.app/river-flow/<state> (also in https://pingwhen.app/sitemap.xml). `GET https://pingwhen.app/river-flow/now/06025500` returns the latest fresh reading, `{"cfs":477,"ft":1.87}`, to pick a sensible line.

```sh
curl -i https://pingwhen.app/river-flow/watch -H "Origin: https://pingwhen.app" \
  --data-urlencode notify=you@example.com --data-urlencode "gauge=Big Hole River near Melrose, MT" \
  -d direction=above -d parameter=00060 -d threshold=600
```

```
HTTP/2 303
location: /check-email/ce_...
```

The watch is `pending_confirm` and a confirmation email goes out. The emailed link activates the watch for a paying account with a free slot; otherwise it opens Stripe Checkout (`303` to `https://checkout.stripe.com/...`) and the watch goes live after payment. With `notify=https://...` (gauge `06025500` works the same as the name) the answer is `HTTP/2 303` to `https://checkout.stripe.com/...`; paying is the confirmation, and an `active` webhook arrives first. A bad form comes back `200` with every problem at once, each under its field, e.g. `<span class="field-error" id="notify-error" role="alert">Enter an email or an https webhook URL.</span>`.

Webhook event `crossing`:

```json
{
  "event": "crossing",
  "watch_id": "w_...",
  "url": "https://pingwhen.app/river-flow/mt/big-hole?parameter=00060&direction=above&threshold=470",
  "gauge": "Big Hole River near Melrose, MT",
  "gauge_page": "https://pingwhen.app/river-flow/mt/big-hole",
  "usgs_site": "06025500",
  "usgs_url": "https://waterdata.usgs.gov/monitoring-location/USGS-06025500/",
  "parameter": "00060",
  "unit": "cfs",
  "direction": "above",
  "threshold": 470,
  "value": 477,
  "provisional": true,
  "reading_time": "2026-10-17T00:15:00+00:00"
}
```

### Global Entry interview

Global Entry, NEXUS, and SENTRI interview openings at a CBP enrollment center. One shared check per center. The first webhook is active immediately, every 15 minutes. A second center, or a check every minute, is Starter $9/mo. Cancel after you book.

`POST https://pingwhen.app/global-entry/watch`

JSON (`Content-Type: application/json`) is the agent path. Do not send an `Origin` header on that request.

| field | value |
| --- | --- |
| `notify` | email, or an https webhook URL |
| `center` | location id (`5140`), center name, or page path |
| `program` | `Global Entry`, `NEXUS`, or `SENTRI` |
| `before` | `any`, `14d`, `30d`, or `90d` |
| `every` | omit or `15m` for the free 15-minute check; `minute` for Starter |

Creating, polling, and deleting use HTTP only. There is no browser form and no confirmation email for a webhook.

```sh
curl -sS -X POST https://pingwhen.app/global-entry/watch -H "content-type: application/json" -d '{"notify":"https://example.com/hooks/global-entry","center":"5140","program":"Global Entry","before":"any"}'
```

The first webhook for that URL is free. Response `201`:

```json
{
  "watch_id": "w_...",
  "status": "active",
  "manage_token": "mt_...",
  "plan": "free",
  "interval_sec": 900,
  "url": "https://pingwhen.app/global-entry/ny/jfk-international?location=5140&program=ge&before=any",
  "events": ["opening"],
  "center": "JFK International Global Entry EC",
  "program": "Global Entry",
  "window": "any",
  "poll": "GET /v1/watches/w_...?token=mt_...",
  "cancel": "DELETE /v1/watches/w_...?token=mt_..."
}
```

`center` is a location id (`5140`), a center name, or a page path. `program` is `Global Entry`, `NEXUS`, or `SENTRI` (TSA PreCheck is not in the public location list). `before` is the window: `any`, `14d` (2 weeks), `30d`, or `90d`. `every` is optional: omit it or send `15m` for the free check. `minute` asks for a check every minute and returns `checkout_url`.

Poll with the `poll` URL (`GET https://pingwhen.app/v1/watches/w_...?token=mt_...`). The body has `events: ["opening"]`, `center`, `program`, and `window`. Delete with the `cancel` URL (`DELETE`, `204`). A second center on the same webhook, or `every` set to `minute`, answers `201` with `status` `pending_payment`, `interval_sec` `60`, and `checkout_url` for Starter ($9/mo). After payment the watch checks every minute. A signed-in paying account with room is active at that minute rate and skips checkout.

An email `notify` answers `202` `{"status":"pending_confirm","state":"check_email"}` with no id or token. The first center on that address is free and checks every 15 minutes. Another center continues to Starter checkout.

Webhook event `opening` (signed like every PingWhen webhook):

```json
{
  "event": "opening",
  "watch_id": "w_...",
  "url": "https://pingwhen.app/global-entry/ny/jfk-international?location=5140&program=ge&before=any",
  "location_id": 5140,
  "center": "JFK International Global Entry EC",
  "city": "Jamaica",
  "state": "NY",
  "program": "Global Entry",
  "before": "any",
  "booking_url": "https://ttp.cbp.dhs.gov/",
  "checked_at": "2026-10-23T10:41:00.000Z",
  "slots": [
    {
      "start": "2026-10-23T06:40",
      "end": "2026-10-23T06:50",
      "timezone": "America/New_York",
      "when": "Fri, Oct 23, 2026, 6:40 AM EDT"
    },
    {
      "start": "2026-10-23T06:50",
      "end": "2026-10-23T07:00",
      "timezone": "America/New_York",
      "when": "Fri, Oct 23, 2026, 6:50 AM EDT"
    }
  ]
}
```

Human form only: `POST https://pingwhen.app/global-entry/watch` as `application/x-www-form-urlencoded`, with `notify`, `center`, `program`, and `before`. Only that form sends `Origin: https://pingwhen.app`, the way a browser would, and it answers with a redirect, not JSON. Email answers `HTTP/2 303` to `/check-email/ce_...`. Pages: https://pingwhen.app/global-entry and one page per center (also in https://pingwhen.app/sitemap.xml).

### County tax sales

Official county tax-sale pages. One watch on Starter $9/mo (10 watches, every 15 min) or Plus $19/mo (40 watches, every 5 min). A free account does not include watches. Verify every parcel on the county site.

`POST https://pingwhen.app/tax-sale/watch`. Form posts are `application/x-www-form-urlencoded`. Send `Origin: https://pingwhen.app` as a browser would. They answer with a redirect, not JSON.

| field | value |
| --- | --- |
| `notify` | email, or an https webhook URL |
| `county` | a county name, e.g. `Harris County, Texas` |
| `criteria_shown` + `criteria` | parcel-list counties only: send `criteria_shown=1` and one or more of `criteria=added`, `criteria=removed`, `criteria=date`. Leave both out for every change. |

Parcel-list counties (added parcels, removed parcels, a new sale date): Harris County, Texas; Muscogee County, Georgia.

Page-text counties (any wording change; timestamps and menus ignored): Charleston County, South Carolina; Chatham County, Georgia; Dallas County, Texas; Tarrant County, Texas; Travis County, Texas; Fulton County, Georgia; DeKalb County, Georgia; Gwinnett County, Georgia; Cobb County, Georgia; Miami-Dade County, Florida; Duval County, Florida; Maricopa County, Arizona; Pima County, Arizona; Denver, Colorado; El Paso County, Colorado; Arapahoe County, Colorado; Mecklenburg County, North Carolina; Wake County, North Carolina; Greenville County, South Carolina; Shelby County, Tennessee; Cook County, Illinois; Philadelphia, Pennsylvania.

```sh
curl -i https://pingwhen.app/tax-sale/watch -H "Origin: https://pingwhen.app" \
  --data-urlencode notify=you@example.com --data-urlencode "county=Harris County, Texas" \
  -d criteria_shown=1 -d criteria=added -d criteria=date
```

```
HTTP/2 303
location: /check-email/ce_...
```

Then a confirmation email. The emailed link activates the watch for a paying account with a free slot; otherwise it opens Stripe Checkout (`303` to `https://checkout.stripe.com/...`) and the watch goes live after payment. A webhook `notify` answers `HTTP/2 303` to `https://checkout.stripe.com/...`. A subset of criteria is kept on the watch URL as `#only=added,date`; the fragment is never sent to the county.

Webhook event `change`: `{event, watch_id, url, checked_at, http_status, excerpt}`, where `excerpt` reads like `Sale date changed from November 3, 2026 to December 1, 2026. Added parcel: 013 021 022 (9 MAIN ST).`

### SEC filings

EDGAR filings for one ticker: any form, 8-K, Form 4 (insider), 13D/13G, 10-K/10-Q, or S-1. Webhook watches are created, polled, and deleted over HTTPS with no browser and no checkout for the first ticker. The first check is a quiet baseline. After that, a free watch is checked every hour. A paying Starter account is checked every 5 minutes (10 tickers); Plus covers 50. Email confirms from a link; the second ticker on an email or a webhook goes to Stripe Checkout.

`POST https://pingwhen.app/v1/sec-filings` (JSON). No `Origin` header.

| field | value |
| --- | --- |
| `ticker` | a symbol on https://pingwhen.app/sec-filings, such as `NVDA` |
| `forms` | `any`, `8-k`, `4`, `13d`, `10`, or `s-1` |
| `notify` | https webhook URL (starts immediately) or an email (confirm link, no id in the response) |

```sh
curl -s -X POST https://pingwhen.app/v1/sec-filings -H 'content-type: application/json' -d '{"ticker":"NVDA","forms":"4","notify":"https://hooks.example.com/sec"}'
```

Response `201`:

```json
{
  "watch_id": "w_...",
  "status": "active",
  "manage_token": "mt_...",
  "ticker": "NVDA",
  "company": "NVIDIA CORP",
  "forms": "4",
  "plan": "free",
  "interval_sec": 3600,
  "poll": "GET /v1/watches/w_...?token=mt_...",
  "cancel": "DELETE /v1/watches/w_...?token=mt_..."
}
```

List that watch (no browser):

```sh
curl -s https://pingwhen.app/v1/watches/w_...?token=mt_...
```

Delete it:

```sh
curl -s -X DELETE https://pingwhen.app/v1/watches/w_...?token=mt_...
```

An email `notify` answers `202` `{"status":"pending_confirm","state":"check_email",...}` with no id. A second ticker on the same webhook answers `201` with `status` `pending_payment` and `checkout_url`.

Each new filing is one signed POST (`User-Agent: PingWhenBot/1.0`, `X-PingWhen-Event: filing`, `X-PingWhen-Signature: sha256=<HMAC-SHA256 of the raw body, keyed with manage_token>`). Form 4 includes the insider name from the filing XML:

```json
{
  "event": "filing",
  "watch_id": "w_...",
  "url": "https://pingwhen.app/sec-filings/nvda?forms=4",
  "checked_at": "2026-10-22T20:15:00.000Z",
  "ticker": "NVDA",
  "company": "NVIDIA CORP",
  "cik": "0001045810",
  "form": "4",
  "filed_at": "2026-10-10T01:04:09.000Z",
  "filed_at_ct": "Oct 9, 2026, 8:04 PM CT",
  "insider": "Coxe Tench",
  "accession": "0001197647-26-000011",
  "document_url": "https://www.sec.gov/Archives/edgar/data/1045810/000119764726000011/wk-form4_1791579847.xml"
}
```

Human form: `POST https://pingwhen.app/sec-filings/watch` with `notify`, `ticker`, and `forms`. 1 ticker free, checked every hour. Starter $9/mo · 10 tickers · every 5 min. Plus $19/mo · 50 tickers. A webhook starts without checkout.

### Drug recalls
Agents create, list, and delete this watch with the API. No browser and no confirmation link. `POST https://pingwhen.app/v1/drug-recalls/watch` with a webhook `notify` on the free watch answers `201` `{"status":"active",...}` and includes `watch_id` and `manage_token`. List it with `GET https://pingwhen.app/v1/watches/w_...?token=mt_...`. Delete it with `DELETE https://pingwhen.app/v1/watches/w_...?token=mt_...` (`204`). A second watch for the same webhook is `pending_payment` and returns `checkout_url`.

```sh
curl -s -X POST https://pingwhen.app/v1/drug-recalls/watch -H "content-type: application/json" -d '{"notify":"https://webhook.site/your-id","name":"levothyroxine","class":"any"}'
```

`201`:

```json
{
  "watch_id": "w_...",
  "status": "active",
  "manage_token": "mt_...",
  "url": "https://pingwhen.app/drug-recalls/levothyroxine",
  "poll": "GET /v1/watches/w_...?token=mt_...",
  "cancel": "DELETE /v1/watches/w_...?token=mt_..."
}
```

Poll `GET https://pingwhen.app/v1/watches/w_...?token=mt_...` returns `{"watch_id":"w_...","status":"active","notify":"https://webhook.site/your-id","events":["recall"],"name":"Levothyroxine","class":"any"}`. `events` is `["recall"]`. `name` is the drug or device. `class` is `any` or `I`. Delete: `DELETE https://pingwhen.app/v1/watches/w_...?token=mt_...`.

The check is every 30 minutes, one shared openFDA fetch for every watch. The first check is a quiet baseline. The next check that finds a new match posts one signed webhook (`X-PingWhen-Event: recall`, `X-PingWhen-Signature: sha256=<HMAC-SHA256 of the raw body, keyed with manage_token>`). A free watch's first alert includes `offer`.

Sample webhook payload:

```json
{
  "event": "recall",
  "watch_id": "w_...",
  "url": "https://pingwhen.app/drug-recalls/levothyroxine",
  "checked_at": "2026-10-23T12:00:00.000Z",
  "disclaimer": "These are possible matches from public FDA data. Confirm the drug, device, firm, and lot on the official notice before you act.",
  "kind": "drug",
  "name": "Levothyroxine",
  "class": "any",
  "recalls": [
    {
      "id": "drug:D-0353-2026",
      "classification": "Class I",
      "title": "Synthroid (levothyroxine sodium) tablets, 100 mcg",
      "firm": "Example Pharma",
      "date": "2026-10-15",
      "url": "https://www.accessdata.fda.gov/scripts/ires/index.cfm?Event=99001"
    }
  ],
  "offer": "One drug or device alert stays free. Starter is $9/mo for 10 watches, enough to cover what a pharmacy or clinic stocks. Plus is $19/mo and imports a stock list from CSV."
}
```

An email `notify` (`{"notify":"you@example.com","name":"levothyroxine","class":"any"}`) stays `pending_confirm` until the link is opened. The human form is `POST https://pingwhen.app/drug-recalls/watch`.

### Campground alerts

One email when a federal campground is listed closed and then listed open. National forest campgrounds use the Forest Service nightly open/closed list. National park campgrounds use National Park Service season dates and closure alerts. Checked once a day on Starter and Plus. This is not a Recreation.gov site calendar and not a cancellation list: a campground can be listed open while every site is reserved. One watch. Starter is $9 a month for 10 watches. Plus is $19 a month for 40 watches. Campground status is checked once a day on either plan. A free account does not include watches.

`POST https://pingwhen.app/campsites/watch`

JSON (`Content-Type: application/json`) is the agent path. Do not send an `Origin` header on that request.

| field | value |
| --- | --- |
| `notify` | email, or an https webhook URL |
| `campground` | a campground name (`Upper Pines`, `Mather Campground`, `Trillium Lake`) |
| `arrive` | `YYYY-MM-DD` |
| `depart` | `YYYY-MM-DD`, after arrive, at most 180 days later, within 18 months |
| `nights` | optional, 1 to 14, and no more nights than the dates cover |

```sh
curl -sS -X POST https://pingwhen.app/campsites/watch -H "content-type: application/json" -d '{"notify":"you@example.com","campground":"Upper Pines","arrive":"2027-07-04","depart":"2027-07-06"}'
```

Email answers `202`:

```json
{
  "status": "pending_confirm",
  "state": "check_email",
  "message": "Check your email to confirm. We sent a link to that address; alerts start once it's opened."
}
```

The watch stays `pending_confirm` until that link is opened. A paying account with a free slot becomes `active`. Anyone else continues to Stripe Checkout. A webhook answers `201` with `status` `pending_payment`, `watch_id`, `manage_token`, `interval_sec` `86400`, `events` `["open"]`, and `checkout_url`. Paying is the confirmation. A signed-in paying account with a free slot is `active` and omits `checkout_url`.

```sh
curl -sS -X POST https://pingwhen.app/campsites/watch -H "content-type: application/json" -d '{"notify":"https://example.com/hooks/camps","campground":"Upper Pines","arrive":"2027-07-04","depart":"2027-07-06"}'
```

Human form: `POST https://pingwhen.app/campsites/watch` as `application/x-www-form-urlencoded`, with `notify`, `campground`, `arrive`, and `depart`. Send `Origin: https://pingwhen.app`. Email answers `HTTP/2 303` to `/check-email/ce_...`. A webhook form answers `303` to Checkout. Pages: https://pingwhen.app/campsites, one page per state, and one page per campground (also in https://pingwhen.app/sitemap.xml).

The first check records the current status and does not email. We email once if a later check moves it from closed to open while `depart` is still today or later. Then the watch stops. We check once a day. National forest campgrounds use the Forest Service nightly open/closed list. National park campgrounds use National Park Service season dates and closure alerts. Out of season is closed. A closure alert that names the campground is closed. In season, with no closure alert, is open. This is the campground's open or closed status, not a Recreation.gov reservation calendar and not a cancellation list. A campground can be listed open while every site is reserved. The first check records the status and stays quiet. We email once if it changes from closed to open while your dates are still ahead, including if it opens before you arrive. After that email, the watch stops.

Webhook event `open`:

```json
{
  "event": "open",
  "watch_id": "w_...",
  "url": "https://pingwhen.app/campsites/california/upper-pines?arrive=2027-07-04&depart=2027-07-06",
  "campground": "Upper Pines Campground",
  "status": "open",
  "arrive": "2027-07-04",
  "depart": "2027-07-06",
  "checked_at": "2027-06-02T15:00:00.000Z",
  "source_url": "https://www.nps.gov/yose/planyourvisit/conditions.htm",
  "note": "Official campground open/closed status, not Recreation.gov site inventory."
}
```

### Device recalls
Agents create, list, and delete this watch with the API. No browser and no confirmation link. `POST https://pingwhen.app/v1/device-recalls/watch` with a webhook `notify` on the free watch answers `201` `{"status":"active",...}` and includes `watch_id` and `manage_token`. List it with `GET https://pingwhen.app/v1/watches/w_...?token=mt_...`. Delete it with `DELETE https://pingwhen.app/v1/watches/w_...?token=mt_...` (`204`). A second watch for the same webhook is `pending_payment` and returns `checkout_url`.

```sh
curl -s -X POST https://pingwhen.app/v1/device-recalls/watch -H "content-type: application/json" -d '{"notify":"https://webhook.site/your-id","name":"CPAP","class":"I"}'
```

`201`:

```json
{
  "watch_id": "w_...",
  "status": "active",
  "manage_token": "mt_...",
  "url": "https://pingwhen.app/device-recalls/cpap?class=1",
  "poll": "GET /v1/watches/w_...?token=mt_...",
  "cancel": "DELETE /v1/watches/w_...?token=mt_..."
}
```

Poll `GET https://pingwhen.app/v1/watches/w_...?token=mt_...` returns `{"watch_id":"w_...","status":"active","notify":"https://webhook.site/your-id","events":["recall"],"name":"CPAP","class":"I"}`. `events` is `["recall"]`. `name` is the drug or device. `class` is `any` or `I`. Delete: `DELETE https://pingwhen.app/v1/watches/w_...?token=mt_...`.

The check is every 30 minutes, one shared openFDA fetch for every watch. The first check is a quiet baseline. The next check that finds a new match posts one signed webhook (`X-PingWhen-Event: recall`, `X-PingWhen-Signature: sha256=<HMAC-SHA256 of the raw body, keyed with manage_token>`). A free watch's first alert includes `offer`.

Sample webhook payload:

```json
{
  "event": "recall",
  "watch_id": "w_...",
  "url": "https://pingwhen.app/device-recalls/cpap?class=1",
  "checked_at": "2026-10-23T12:00:00.000Z",
  "disclaimer": "These are possible matches from public FDA data. Confirm the drug, device, firm, and lot on the official notice before you act.",
  "kind": "device",
  "name": "CPAP",
  "class": "I",
  "recalls": [
    {
      "id": "device:99843",
      "classification": "Class I",
      "title": "CPAP device, product code BZD",
      "firm": "Example Devices",
      "date": "2026-10-10",
      "url": "https://www.accessdata.fda.gov/scripts/cdrh/cfdocs/cfres/res.cfm?id=99843"
    }
  ],
  "offer": "One drug or device alert stays free. Starter is $9/mo for 10 watches, enough to cover what a pharmacy or clinic stocks. Plus is $19/mo and imports a stock list from CSV."
}
```

An email `notify` (`{"notify":"you@example.com","name":"CPAP","class":"any"}`) stays `pending_confirm` until the link is opened. The human form is `POST https://pingwhen.app/device-recalls/watch`.

## Poll a watch

`GET https://pingwhen.app/v1/watches/:id?token=mt_...`

A watch with an email `notify` answers **202** `{"status":"pending_confirm","state":"check_email","message":"..."}` with no id or token: we email that address a confirm link, and the watch starts (or continues to Checkout) once it's opened. Webhook destinations get the token and `checkout_url` right away.

Status: `pending_confirm` | `pending_payment` | `active` | `past_due` | `canceled` | `expired`. `pending_confirm` means we are waiting on the confirmation email. Once active, the JSON includes `portal_url` (Stripe Customer Portal).

## Webhooks

Every alert type posts JSON to your https URL with `User-Agent: PingWhenBot/1.0`, `X-PingWhen-Event: <event>` and `X-PingWhen-Signature: sha256=<hex HMAC-SHA256 of the raw body, keyed with the watch's manage_token>`. Events: `active` (after payment), `change`, `down`, `up` (website and tax-sale watches), `crossing` (river flow), `recall` (recalls), `filing` (SEC filings), `opening` (Global Entry, NEXUS, or SENTRI interview slots), `open` (a campground listed open).

Website and tax-sale alerts:

```json
{
  "event": "change",
  "watch_id": "w_...",
  "url": "https://example.com",
  "checked_at": "2026-09-20T20:04:00Z",
  "http_status": 200,
  "excerpt": "text now includes $19/mo"
}
```

Respond `410` or `{"ok":false,"unsubscribe":true}` to delete the watch. Webhook URLs must be public https; private and metadata addresses are refused.

## Cancel

`DELETE https://pingwhen.app/v1/watches/:id?token=mt_...` removes that watch. Hand the user `portal_url` to cancel the Stripe subscription. Every email has an unsubscribe link.

## Suggest a new alert

`POST https://pingwhen.app/v1/niches/suggestions` with `email` and `prompt`. A page URL is sent to the homepage watch form. Anything else points at an existing PingWhen alert, an outside alert, or a stored request. A stored request does not send anything until that alert exists. This API does not build the monitor and does not charge for it. Outdoors requests are alerts only. Do not ask PingWhen to book a trip, guide, or lodging.

OpenAPI: https://pingwhen.app/v1/openapi.json
