# Watchfox — Public REST API v1

**API docs version:** `v1`
**Last updated:** `2026-06-14`

**Base URL:** `https://app.watchfox.io`

This reference separates Watchfox HTTP endpoints by integration surface. Do not treat every `/api/...` route as the same kind of API.

**Main rule:**

- Use **Public API** endpoints with `X-Api-Key` for customer integrations.
- Use **CSV/report exports** for reporting, BI, and data download workflows.
- Use **utility validators** for lightweight one-off checks and diagnostics.
- Treat **UI-managed/admin endpoints** as signed-in app flows using `Authorization: Bearer <session JWT>`, not as the stable public integration surface.

**Docs & Tools**

- Docs hub: `/docs/`
- OpenAPI: `/docs/openapi.yaml`
- Postman: `/docs/watchfox.postman_collection.json`

**Maintenance rule:** When public API behavior changes, update this file, `openapi.yaml`, and the Postman collection in the same PR.

---

## Authentication

### Public API with `X-Api-Key`

Project API keys are sent with:

```http
X-Api-Key: wk_…
```

A project API key automatically scopes the request to its project. This is the recommended authentication mechanism for external customer integrations.

### Authenticated app / Bearer endpoints

Signed-in app endpoints use:

```http
Authorization: Bearer <supabase_access_token>
```

Bearer endpoints are scoped by the signed-in user's workspace membership and often require `project_id` or `org_id`.

These endpoints are primarily for the Watchfox app UI and owner/admin workflows. They may change more often than the Public API.

### Unauthenticated utility endpoints

Some utility endpoints are intentionally unauthenticated but rate-limited per IP:

- `GET /api/validate/sitemap`
- `GET /api/validate/links`
- `GET /api/heartbeat/{token}`

They are safe public tools, but they are not intended for bulk crawling or high-volume automation.

---

## Rate limiting

Watchfox uses token-bucket rate limits. Responses may include:

- `RateLimit-Limit`
- `RateLimit-Remaining`
- `RateLimit-Reset` as UNIX seconds
- `Retry-After` on `429`

Rate limits differ by surface:

- Public API: generally per API key.
- CSV exports: per API key or signed-in user/project context.
- Report exports: stricter, because PDF/report generation is more expensive.
- Utility validators: per IP.
- UI-managed/admin endpoints: per signed-in user/org/project context where applicable.

Clients should back off on `429` and respect `Retry-After`.

---

## Errors

JSON errors look like:

```json
{ "error": "unauthorized" }
```

Some endpoints also return `code`, `detail`, `feature`, `limit`, `current`, or `missing`.

---

## Endpoint overview by integration surface

| Surface                    | Endpoints                                                                                                                                                                         | Auth                                                                               | Scope                                                           | Stability                                                          | Rate limit expectations                                                         | Recommended for customer integrations?                                                     |
| :------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------- | :-------------------------------------------------------------- | :----------------------------------------------------------------- | :------------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------- |
| Public API                 | `GET/POST/PATCH/DELETE /api/v1/monitors`, `POST /api/v1/monitors/bulk`, `GET /api/v1/monitor_checks`, `GET /api/v1/incidents`, `POST /api/v1/incident`, `POST /api/v1/test-alert` | `X-Api-Key`; some endpoints also support Bearer for app/UI flows with `project_id` | Project-scoped                                                  | Stable v1 customer integration surface                             | Per API key; mutating/test endpoints are stricter                               | Yes for monitor/check/incident integrations; `test-alert` is limited to setup verification |
| CSV exports                | `GET /api/export/monitor_checks.csv`, `GET /api/export/incidents.csv`, `GET /api/export/report_summary.csv`, `GET /api/export/link_errors.csv`                                    | `X-Api-Key`; Bearer + `project_id` where documented                                | Project-scoped, except link checker export is target-URL scoped | Stable export surface; CSV columns documented per endpoint         | Per API key or Bearer project context; streaming exports still have safe limits | Yes, for BI/reporting/export workflows                                                     |
| Report exports             | `GET /api/reports/monthly.pdf`, `GET /api/reports/summary.json`                                                                                                                   | `X-Api-Key` or Bearer + `project_id`                                               | Project-scoped; plan-gated                                      | Stable report export surface, but report layout/content may evolve | Stricter than lightweight JSON endpoints; cache and avoid polling               | Yes, for report downloads and dashboard/report integrations                                |
| Utility validators         | `GET /api/validate/sitemap`, `GET /api/validate/links`                                                                                                                            | No auth                                                                            | Target URL from query param                                     | Public utility tools, not core data API                            | Per IP; small diagnostic limits                                                 | Limited — useful for one-off validation, not bulk crawling                                 |
| Heartbeat ping             | `GET /api/heartbeat/{token}`                                                                                                                                                      | Token in URL                                                                       | One heartbeat monitor                                           | Stable monitor ingestion endpoint                                  | Protected by token and operational limits                                       | Yes, for cron/job pings                                                                    |
| Status redirect helper     | `GET/HEAD /api/v1/status/{slug}`                                                                                                                                                  | No auth                                                                            | Public status slug                                              | Utility redirect compatibility route                               | Lightweight                                                                     | Limited                                                                                    |
| UI-managed/admin endpoints | `/api/notifications`, `/api/notifications/secret`, `/api/keys`, `/api/report_branding`, `/api/maintenance_windows`                                                                | Bearer session                                                                     | Signed-in user + workspace/project/org role                     | App/admin surface, not the main Public API                         | Per signed-in user/org/project context where applicable                         | No, unless explicitly documented for signed-in admin automation                            |

**Not public API:** internal service routes, ops routes, billing webhooks, report-mailer/internal routes, DB RPC helpers, and service-role-only flows are intentionally excluded from this reference.

---

## Conventions

### Time & formats

- All timestamps are ISO-8601 in **UTC**.
- Date-only strings (`YYYY-MM-DD`) are accepted for `from`/`to` and treated as midnight UTC.

### CSV formatting

- `text/csv; charset=utf-8` with `,` as delimiter.
- Fields with commas, quotes, or newlines are quoted.
- Inner `"` are escaped as `""`.

### Ordering & incremental sync

- `order=asc|desc` where documented.
- `since=` lets you do **incremental** downloads. Two forms:
  - `since=<ISO>` — rows with timestamp strictly greater than `<ISO>`.
  - `since=<ISO>,<id>` — stable cursor: rows with timestamp greater than `<ISO>` or timestamp equal to `<ISO>` and primary key greater than `<id>`.
- When `since=` is used, `order=asc` is enforced where needed to make cursors stable.

### Limits

Endpoints enforce sensible `limit` ranges. See each endpoint for defaults and maxima.

### Project scoping

Your `X-Api-Key` automatically scopes all queries to **its project**.

---

## Public API endpoints — recommended for customer integrations

### GET `/api/v1/monitors`

List project monitors visible to your API key. Includes a convenience `heartbeat_url` for heartbeat monitors.

**Auth:** `X-Api-Key`  
**Scope:** project  
**Stability:** Public API v1  
**Rate limit expectation:** standard read endpoint  
**Recommended for customer integrations:** yes

**Response:** `200` JSON array.

**Example PowerShell**

```powershell
$H = @{ "X-Api-Key" = "wk_…" }
Invoke-RestMethod "https://app.watchfox.io/api/v1/monitors" -Headers $H
```

---

### POST `/api/v1/monitors`

Create a new monitor. Automatically checks plan limits.

**Auth:** `X-Api-Key`; Bearer may be used by app/UI flows where supported  
**Scope:** project  
**Stability:** Public API v1  
**Rate limit expectation:** mutating endpoint, stricter than reads  
**Recommended for customer integrations:** yes

**Responses**

- `201` created
- `402` plan limit or feature gate

**Supported monitor types**

- `uptime` — HTTP/HTTPS check
- `keyword` — check for presence/absence of text in the page body
- `tls` — SSL/TLS certificate expiry
- `domain` — domain expiry via RDAP
- `heartbeat` — cron job / scheduled job monitoring
- `sitemap` — sitemap and broken-link checks within sitemap scope

**Fixed-cadence note**

- `tls` monitors use a fixed 12-hour cadence.
- `domain` monitors use a fixed 24-hour cadence.
- `sitemap` monitors use a fixed 24-hour cadence.

These are scheduled checks, not 1-minute uptime probes.

**Body parameters**

- `type` — required, one of the supported monitor types.
- `url` — required except for `heartbeat`; full URL such as `https://example.com`.
- `label` — optional friendly name; defaults to hostname.
- `interval_s` — optional check interval for interval-driven monitors. Ignored for `tls`, `domain`, and `sitemap` because they use fixed cadences.
- `keyword` — required for `type=keyword`.
- `keyword_should_exist` — optional for `type=keyword`; `true` means the page must contain the keyword, `false` means the page must not contain it. Default is `true`.
- `locations` — backward-compatible/internal compatibility field. Normal scheduled Uptime and Keyword checks are normalized to one primary scheduled probe group; sending multiple values does not enable permanent multi-probe scheduled checking.

**Example keyword monitor**

```json
{
  "type": "keyword",
  "url": "https://eshop.com/product",
  "label": "Stock Check",
  "keyword": "Out of stock",
  "keyword_should_exist": false
}
```

**Example domain monitor**

```json
{
  "type": "domain",
  "url": "https://my-agency.com"
}
```

---

### PATCH `/api/v1/monitors`

Update an existing monitor.

**Auth:** `X-Api-Key`; Bearer may be used by app/UI flows where supported  
**Scope:** project  
**Stability:** Public API v1  
**Rate limit expectation:** mutating endpoint, stricter than reads  
**Recommended for customer integrations:** yes

**Typical body**

```json
{
  "id": "monitor_uuid",
  "label": "Updated name",
  "enabled": true
}
```

---

### DELETE `/api/v1/monitors`

Delete a monitor.

**Auth:** `X-Api-Key`; Bearer may be used by app/UI flows where supported  
**Scope:** project  
**Stability:** Public API v1  
**Rate limit expectation:** mutating endpoint, stricter than reads  
**Recommended for customer integrations:** yes

**Query**

- `id` — monitor id, required
- `project_id` — optional/relevant for Bearer app/UI flows

---

### POST `/api/v1/monitors/bulk`

Bulk-create monitors, primarily for import/migration workflows.

**Auth:** `X-Api-Key`; Bearer may be used by app/UI flows where supported  
**Scope:** project  
**Stability:** Public API v1  
**Rate limit expectation:** stricter import/bulk limit  
**Recommended for customer integrations:** yes, for controlled imports/migrations

This endpoint applies the same server-side validation and plan-limit enforcement as single monitor creation.

**Probe coverage in bulk imports**

- Probe coverage is automatic for normal scheduled Uptime and Keyword checks.
- New customer-facing CSV templates should not include `probe_groups`, `probe_group`, or `locations` as configuration fields.
- `probe_groups`, `probe_group`, and legacy `locations` may still be accepted as backward-compatible/internal compatibility fields for older imports.
- Multiple submitted probe-group values are normalized to one primary scheduled probe group. They do not enable permanent multi-probe scheduled checking.
- Additional probe coverage is automatic for smart outage verification where supported by the plan.

**Example bulk body**

```json
{
  "dry_run": true,
  "items": [
    {
      "type": "uptime",
      "url": "https://example.com",
      "label": "Homepage",
      "interval_s": 300
    }
  ]
}
```

---

### GET `/api/v1/monitor_checks`

List recent raw checks for a single monitor.

**Auth:** `X-Api-Key`; Bearer with project context where supported  
**Scope:** project + monitor  
**Stability:** Public API v1  
**Rate limit expectation:** standard read endpoint  
**Recommended for customer integrations:** yes, for recent diagnostics. Use CSV exports for bulk/export use cases.

**Retention:** raw monitor checks are available for the recent raw-history window only.

**Query**

- `monitor_id` — required
- `limit` — default `50`, max `1000`
- `offset` — optional offset paging
- `before` — optional cursor for older results, either `<ISO>` or `<ISO>,<id>`
- `type` — optional monitor type filter
- `region` — optional internal probe group code for historical check evidence

`region` is an internal probe group compatibility code. It describes historical check evidence only; it is not a supported customer configuration knob for scheduled probe selection and is not a guaranteed geographic execution location.

---

### GET `/api/v1/incidents`

List incidents. Supports filtering, incremental cursors, and optional monitor info.

**Auth:** `X-Api-Key`  
**Scope:** project  
**Stability:** Public API v1  
**Rate limit expectation:** standard read endpoint  
**Recommended for customer integrations:** yes

**Response:** `200` JSON array with `cache-control: no-store`.

**Query**

- `from`, `to` — ISO or `YYYY-MM-DD`
- `monitor_id` — filter by monitor
- `status` — `open`, `resolved`, or `all`; default `all`
- `include` — comma-list: `monitor`, `monitor_label`, `monitor_url`
- `order` — `asc` or `desc`; default `desc`; forced `asc` when `since` is used
- `limit` — default `20000`, range `1..50000`
- `since` — incremental cursor: `<ISO>` or `<ISO>,<id>` over `(started_at,id)`; forces `order=asc`

---

### POST `/api/v1/incident`

Open or resolve a custom incident.

**Auth:** `X-Api-Key`  
**Scope:** project + monitor  
**Stability:** Public API v1  
**Rate limit expectation:** mutating endpoint, stricter than reads  
**Recommended for customer integrations:** yes, for controlled custom incident automation

Open:

```json
{
  "monitor_id": "monitor_uuid",
  "summary": "Something broke",
  "details": {
    "hint": "Optional context"
  }
}
```

Resolve:

```json
{
  "monitor_id": "monitor_uuid",
  "resolved_at": "2026-05-01T12:34:00Z"
}
```

---

### POST `/api/v1/test-alert`

Send a test notification through configured channels. No incident is created.

**Auth:** `X-Api-Key`  
**Scope:** project + optional monitor  
**Stability:** Public API v1  
**Rate limit expectation:** strict test-alert limit  
**Recommended for customer integrations:** limited; useful for setup verification, not recurring automation

**Response:** `200` with `{ "ok": true }`.

Body with optional overrides:

```json
{
  "monitor_id": "monitor_uuid",
  "project_id": "project_uuid",
  "channel": "email|slack|webhook",
  "link": "https://example.com"
}
```

---

### GET/HEAD `/api/v1/status/{slug}`

Redirect helper to the public status route.

**Auth:** none  
**Scope:** public status slug  
**Stability:** compatibility helper  
**Rate limit expectation:** lightweight  
**Recommended for customer integrations:** limited

**Response:** `308` with `Location: /status/{slug}`.

---

## CSV exports — reporting and BI surface

CSV endpoints are optimized for BI, support analysis, and report/export workflows. They stream compact CSV with stable columns and documented cursors where available.

**Auth:** use `X-Api-Key` for project-scoped external integrations. Some exports also support signed-in Bearer requests with `project_id` for app/UI download flows.

**Scope:** project-scoped unless the endpoint explicitly documents a target URL.

**Stability:** stable export surface. Columns are documented per endpoint; new columns may be added only when safe.

**Recommendation:** use these endpoints for exports, BI ingestion, reporting workflows, and support/debug workflows. Do not embed API keys in URLs.

### GET `/api/export/monitor_checks.csv`

Export recent raw monitor checks as CSV.

**Auth:** `X-Api-Key`; Bearer with `project_id` where supported  
**Scope:** project, optionally monitor  
**Stability:** stable CSV export surface  
**Rate limit expectation:** export limit, higher cost than lightweight JSON reads  
**Recommended for customer integrations:** yes, for exports/BI. For small recent diagnostics, `GET /api/v1/monitor_checks` may be simpler.

**Default columns:** `monitor_id,checked_at,probe_group,status_code,latency_ms,ok`

`probe_group` is the customer-facing neutral probe group label for historical check evidence. It does not imply that customers can manually choose scheduled probe groups.

**Filters**

- `from`
- `to`
- `monitor_id`
- `ok`
- `order`
- `since`
- `limit`

**Select columns**

`fields=...` is optional.

Allowed fields:

- `monitor_id`
- `checked_at`
- `probe_group`
- `status_code`
- `latency_ms`
- `ok`
- `id`
- `muted_by_mw`

The CSV header respects the order you pass.

**Examples**

Default customer-facing export:

```bash
curl -H "X-Api-Key: wk_…" \
  "https://app.watchfox.io/api/export/monitor_checks.csv?monitor_id=00000000-0000-0000-0000-000000000000&limit=100"
```

Explicit customer-facing field selection:

```bash
curl -H "X-Api-Key: wk_…" \
  "https://app.watchfox.io/api/export/monitor_checks.csv?monitor_id=00000000-0000-0000-0000-000000000000&fields=monitor_id,checked_at,probe_group,ok&order=asc&since=2026-04-01T00:00:00Z,00000000-0000-0000-0000-000000000000"
```

---

### GET `/api/export/incidents.csv`

Export incidents as CSV.

**Auth:** `X-Api-Key`; Bearer with `project_id` where supported  
**Scope:** project  
**Stability:** stable CSV export surface  
**Rate limit expectation:** export limit  
**Recommended for customer integrations:** yes, for exports/BI/reporting

**Columns:** `id,monitor_id,started_at,resolved_at,summary,status`

Optional columns with `include=`:

- `monitor_label`
- `monitor_url`

**Filters**

- `from`
- `to`
- `monitor_id`
- `status=open|resolved|all`
- `order`
- `limit`

**Cursor**

`since=<ISO>` or `since=started_at,id`; this forces `order=asc`.

**Example**

```bash
curl -H "X-Api-Key: wk_…" \
  "https://app.watchfox.io/api/export/incidents.csv?include=monitor_label,monitor_url&since=2026-04-01T00:00:00Z,00000000-0000-0000-0000-000000000000"
```

---

### GET `/api/export/report_summary.csv`

Export report summary rows and included web-monitor metrics for the same report window used by `summary.json` and the PDF report.

**Auth:** `X-Api-Key` or `Authorization: Bearer …` with `project_id=<uuid>`  
**Scope:** project  
**Stability:** stable CSV export surface  
**Rate limit expectation:** report/export limit  
**Recommended for customer integrations:** yes, for reporting, BI, support analysis, and customer portals

**Columns:** `row_type,monitor_id,monitor_label,monitor_type,metric,value,unit,window_start,window_end,generated_at`

**Filters**

- `month=YYYY-MM`
- or `from=YYYY-MM-DD&to=YYYY-MM-DD`
- `project_id=<uuid>` when using Bearer auth

The export intentionally does not include monitor URLs, tokens, webhook secrets, internal service credentials, SQL/RPC errors, stack traces, or raw debug payloads.

**Example API key**

```bash
curl -H "X-Api-Key: wk_…" \
  "https://app.watchfox.io/api/export/report_summary.csv?month=2026-04"
```

**Example signed-in/Bearer**

```bash
curl -H "Authorization: Bearer eyJ…" \
  "https://app.watchfox.io/api/export/report_summary.csv?month=2026-04&project_id=<uuid>"
```

---

### GET `/api/export/link_errors.csv`

Export problematic links discovered on a given HTML page. Useful for catching `404`, `5xx`, and network errors.

**Auth:** `X-Api-Key`  
**Scope:** target `url` query parameter  
**Stability:** stable utility/export surface  
**Rate limit expectation:** strict checker/export limit  
**Recommended for customer integrations:** limited; useful for diagnostics, not bulk crawling

**Columns:** `source_url,target_url,status,ms,error`

Errors include:

- `client` for 4xx
- `server` for 5xx
- `network` for no response

Redirects are not exported as errors.

**Query**

- `url` — target HTML page to scan
- `sample` — max links to verify; default `200`, max `200`
- `timeout_ms` — per-request timeout; default `8000`, max `30000`

**Example**

```bash
curl -H "X-Api-Key: wk_…" \
  "https://app.watchfox.io/api/export/link_errors.csv?url=https://app.watchfox.io/test/links-mix.html&sample=100"
```

---

## Utility validators

Utility validators are public, unauthenticated, per-IP rate-limited diagnostic tools. They are useful for one-off checks and support/debugging, but they are not a bulk crawler API.

### GET `/api/validate/sitemap`

Lightweight sitemap/sitemap-index validator. Fetches and validates XML, samples up to `sample` URLs, and returns a status breakdown.

**Auth:** none  
**Scope:** target `url` query parameter  
**Stability:** public utility tool  
**Rate limit expectation:** low per-IP diagnostic limit  
**Recommended for integrations:** limited; useful for one-off validation and setup diagnostics, not bulk crawling

**Response:** `200` JSON summary.  
**HEAD:** returns `204` for health checks.

**Query**

- `url` — sitemap URL to validate, required
- `sample` — default `50`, max `200`
- `timeout_ms` — optional timeout

**Behavior**

If the target sitemap is unreachable or returns a non-2xx status, the endpoint returns a JSON error response with diagnostic fields such as status/error details.

**Quick test**

```bash
curl "https://app.watchfox.io/api/validate/sitemap?url=https://example.com/sitemap.xml&sample=5"
```

---

### GET `/api/validate/links`

Lightweight links checker for a single HTML page. Fetches the page, extracts links from anchors, images, styles, and scripts, and verifies a sample with `HEAD` and fallback `GET`.

**Auth:** none  
**Scope:** target `url` query parameter  
**Stability:** public utility tool  
**Rate limit expectation:** low per-IP diagnostic limit  
**Recommended for integrations:** limited; useful for diagnostics, not bulk crawling

**Response:** `200` JSON summary.  
**HEAD:** returns `204` for health checks.

**Query**

- `url` — page to analyze, required
- `sample` — discovered links to check, `1..200`, default `50`
- `timeout_ms` — per-link timeout, `1000..30000`, default `8000`

**Response shape**

```json
{
  "ok": true,
  "page_url": "https://example.com",
  "discovered": 42,
  "sampled": 42,
  "ok_2xx": 35,
  "redir_3xx": 2,
  "client_4xx": 3,
  "notfound_404": 2,
  "server_5xx": 1,
  "other": 1,
  "examples": {
    "redir": [
      {
        "url": "https://example.com/old",
        "status": 301,
        "ms": 120,
        "via": "HEAD"
      }
    ],
    "client_4xx": [
      {
        "url": "https://example.com/about",
        "status": 404,
        "ms": 88,
        "via": "GET"
      }
    ]
  }
}
```

**Quick test**

```bash
curl "https://app.watchfox.io/api/validate/links?url=https://example.com/&sample=20"
```

---

## Heartbeats

### GET `/api/heartbeat/{token}`

Records a heartbeat ping for the monitor owning `{token}`.

**Auth:** token in URL  
**Scope:** one heartbeat monitor  
**Stability:** stable monitor ingestion endpoint  
**Rate limit expectation:** protected by token and operational limits  
**Recommended for integrations:** yes, for cron/job pings

**Responses**

- `204` success
- `404` unknown or disabled monitor
- `429` rate-limited

A ready-to-use `heartbeat_url` is included in `GET /api/v1/monitors` for heartbeat monitors.

---

## Report exports

Report endpoints are the report/export surface. They are project-scoped, plan-gated where applicable, and more expensive than lightweight JSON/CSV endpoints.

They are recommended for report download workflows, customer portals, and controlled integrations. They are not meant for high-frequency polling.

### GET `/api/reports/monthly.pdf`

Generates a monthly PDF report.

**Auth:** `X-Api-Key` or `Authorization: Bearer …` with `project_id=<uuid>`  
**Scope:** project  
**Stability:** stable report export endpoint; PDF design/content may evolve as the product improves  
**Rate limit expectation:** stricter report-download limit; cache downloaded PDFs and respect `429 Retry-After`  
**Recommended for customer integrations:** yes, for report download workflows; no for high-frequency polling  
**Plan:** requires `pdf_reports`

If the organization does not have access, returns `402`:

```json
{
  "error": "plan_required",
  "code": "PLAN_REQUIRED",
  "feature": "pdf_reports"
}
```

**Query**

- `month=YYYY-MM` — defaults to previous month, UTC
- `from=YYYY-MM-DD&to=YYYY-MM-DD` — custom UTC date range, `to` exclusive
- `project_id=<uuid>` — required for Bearer auth; ignored for API-key auth

**Response:** `application/pdf` with `Cache-Control: no-store`

**Examples**

```bash
# API key, project-scoped
curl -H "X-Api-Key: wk_…" \
  -o report.pdf \
  "https://app.watchfox.io/api/reports/monthly.pdf?month=2026-04"

# Bearer session, explicit project
curl -H "Authorization: Bearer eyJ…" \
  -o report.pdf \
  "https://app.watchfox.io/api/reports/monthly.pdf?month=2026-04&project_id=<uuid>"
```

---

### GET `/api/reports/summary.json`

Returns a JSON executive summary for the selected month or custom range.

**Auth:** `X-Api-Key` or `Authorization: Bearer …` with `project_id=<uuid>`  
**Scope:** project  
**Stability:** stable report summary surface; additional fields may be added over time  
**Rate limit expectation:** report-summary limit; cache where possible  
**Recommended for customer integrations:** yes, for dashboards/report portals; avoid high-frequency polling

**Range**

- `month=YYYY-MM`, UTC
- or `from=YYYY-MM-DD&to=YYYY-MM-DD`, where `to` is exclusive, max 120 days

**Response fields include**

- `avg_uptime_pct`
- `total_incidents`
- `total_downtime_sec`
- `longest_incident`
- `worst_monitors`
- `trends`
- `report_target`

---

## UI-managed/admin endpoints

UI-managed/admin endpoints are signed-in app flows. They are documented here to avoid confusion, but they are not the main Public API contract.

### Notifications

#### GET `/api/notifications?project_id=…`

List notification channels for a project.

#### POST `/api/notifications`

Create a notification channel.

#### DELETE `/api/notifications?id=…`

Delete a notification channel.

**Auth:** `Authorization: Bearer <JWT>`  
**Scope:** signed-in user + project/workspace role  
**Stability:** UI-managed/admin surface  
**Rate limit expectation:** per signed-in user/project context where applicable  
**Recommended for customer integrations:** no. Configure notification channels in the app UI unless Watchfox explicitly documents an admin automation workflow.

Payload includes:

- `channel`: `email`, `slack`, `webhook`, or another supported channel
- `target`
- optional `rules`

---

### POST `/api/notifications/secret`

Set or rotate the HMAC secret for a webhook notification channel.

**Auth:** `Authorization: Bearer <JWT>`  
**Scope:** signed-in owner/admin + notification channel  
**Stability:** UI-managed/admin helper  
**Rate limit expectation:** strict mutating endpoint  
**Recommended for customer integrations:** no

**Body**

```json
{
  "id": "notification_uuid",
  "secret": "changeme-strong"
}
```

**Response**

```json
{
  "ok": true
}
```

This helper updates the channel's `rules.secret` server-side.

---

### API keys

#### POST `/api/keys`

Create a project API key. Returns the plaintext `wk_…` key once.

#### DELETE `/api/keys?id=…`

Revoke an API key.

**Auth:** `Authorization: Bearer <JWT>`  
**Scope:** signed-in owner/admin + project  
**Stability:** UI-managed/admin surface  
**Rate limit expectation:** strict mutating endpoint  
**Recommended for customer integrations:** no. Use these to create/revoke API keys from the app/admin context; use the resulting `wk_…` key for external integrations.

---

### Report branding

#### GET `/api/report_branding?org_id=…`

Read report branding settings for an organization.

#### PATCH `/api/report_branding`

Update report branding settings.

**Auth:** `Authorization: Bearer <JWT>`  
**Scope:** signed-in workspace member for read; owner/admin for writes  
**Stability:** UI-managed/admin surface  
**Rate limit expectation:** per signed-in user/org context; write endpoint is stricter  
**Recommended for customer integrations:** no. This is app configuration for monthly PDF/report branding, not a Public API endpoint.

**Plan behavior**

- Report branding requires the report/PDF entitlement.
- White-label behavior requires the white-label entitlement where applicable.

---

### Maintenance windows

Maintenance windows are app/admin configuration endpoints. Launch maintenance windows are one-time only.

#### GET `/api/maintenance_windows?org_id=...&project_id=...&active=1&at=2026-05-01T20:00:00Z`

List maintenance windows. When `project_id` is provided, the response includes organization-wide windows plus windows that affect the selected project.

#### POST `/api/maintenance_windows`

Create a one-time maintenance window.

#### DELETE `/api/maintenance_windows?id=...`

Delete a maintenance window.

**Auth:** `Authorization: Bearer <JWT>`  
**Scope:** signed-in owner/admin + org/project/monitor target  
**Stability:** UI-managed/admin surface  
**Rate limit expectation:** per signed-in user/org context; mutating endpoints are stricter  
**Recommended for customer integrations:** no, unless explicitly documented for signed-in admin automation.

**Supported scopes**

- `org`: organization-wide maintenance. `project_id`, `monitor_id`, and `tag` must be empty.
- `project`: current-project maintenance. `project_id` is required.
- `monitor`: one monitor. `monitor_id` is required; Watchfox validates and stores the monitor's project context.
- `tag`: tag within one project. `project_id` and `tag` are required. The same tag in another project is not affected.

**Create body — project scope**

```json
{
  "org_id": "org_uuid",
  "project_id": "project_uuid",
  "name": "Client deploy window",
  "applies_to": "project",
  "monitor_id": null,
  "tag": null,
  "starts_at": "2026-05-01T20:00:00Z",
  "ends_at": "2026-05-01T21:00:00Z"
}
```

**Create body — tag within project**

```json
{
  "org_id": "org_uuid",
  "project_id": "project_uuid",
  "name": "Production deploy",
  "applies_to": "tag",
  "monitor_id": null,
  "tag": "production",
  "starts_at": "2026-05-01T20:00:00Z",
  "ends_at": "2026-05-01T21:00:00Z"
}
```

**Behavior**

Recurring maintenance windows are not launch-supported. If a non-empty `rrule` field is sent, the API returns `400 recurring_maintenance_not_supported`. Create a one-time maintenance window for each planned work period instead.
While a window is active, uptime workers still write `monitor_checks` but set `muted_by_mw=true` and suppress notifications. Incidents are still opened/resolved as usual for history and reporting. Only alerts are muted. Project and tag scopes affect only monitors/components in the intended project.

---

## Webhook signatures

Each webhook delivery includes a signature header:

```http
Watchfox-Signature: t=<unix_seconds>, v1=<hex(hmac_sha256(t + "." + raw_body, secret))>
```

- `t` is the UNIX timestamp when Watchfox signed the payload.
- `v1` is lowercase hex HMAC-SHA256 of the exact bytes of `"<t>.<raw_body>"` using your per-channel secret.
- Reject requests where `|now - t| > 5 minutes` to reduce replay risk.
- Retries occur for `429` and `5xx` with exponential backoff.

**Node.js verification example**

```js
import crypto from "node:crypto";

export function verifyWatchfoxSignature(rawBody, sigHeader, getSecret) {
  const parts = Object.fromEntries(
    String(sigHeader || "")
      .split(",")
      .map((s) =>
        s
          .trim()
          .split("=")
          .map((v) => v.trim()),
      ),
  );

  const t = Number(parts.t);
  const v1 = parts.v1;
  if (!t || !v1) return false;

  if (Math.abs(Math.floor(Date.now() / 1000) - t) > 300) return false;

  const secret = getSecret();
  const signed = `${t}.${rawBody}`;
  const mac = crypto.createHmac("sha256", secret).update(signed).digest("hex");

  return (
    mac.length === v1.length &&
    crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(v1))
  );
}
```

Historical note: older integrations may have verified `HMAC(secret, raw_body)` without the timestamp. If you need backward compatibility, verify the new format first and optionally accept the legacy format explicitly until you rotate secrets.

---

## Quick smoke tests

### PowerShell

```powershell
$Base = "https://app.watchfox.io"
$H = @{
  "X-Api-Key" = "wk_…"
  "User-Agent" = "watchfox-cli"
}

# First 10 checks as objects
$from = (Get-Date).AddDays(-2).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
Invoke-WebRequest "$Base/api/export/monitor_checks.csv?from=$([uri]::EscapeDataString($from))&limit=1000" -Headers $H |
  ForEach-Object Content |
  ConvertFrom-Csv |
  Select-Object -First 10

# Open incidents formatted as a table
Invoke-WebRequest "$Base/api/export/incidents.csv?status=open&from=$([uri]::EscapeDataString($from))" -Headers $H |
  ForEach-Object Content |
  ConvertFrom-Csv |
  Format-Table -AutoSize

# Sitemap validator
Invoke-WebRequest "$Base/api/validate/sitemap?url=$([uri]::EscapeDataString('https://example.com/sitemap.xml'))&sample=25" |
  ForEach-Object Content
```

### curl

```bash
curl -H "X-Api-Key: wk_…" "https://app.watchfox.io/api/v1/incidents?limit=5"
curl "https://app.watchfox.io/api/validate/sitemap?url=https://example.com/sitemap.xml&sample=50"
curl -H "X-Api-Key: wk_…" -o report.pdf "https://app.watchfox.io/api/reports/monthly.pdf?month=2026-04"
```

---

## Version & status

**API docs updated: 2026-05-02.** Public API and export endpoints are intended to be stable customer-facing surfaces. UI-managed/admin endpoints are documented for clarity, but they are not the primary external integration contract. Breaking changes to Public API v1 will be announced before GA where practical.