---
title: WatchFor API authentication: API keys and OAuth 2.1
description: API keys (read or write scope) and OAuth 2.1 — how to create, scope, rotate and revoke credentials, and which surfaces accept which.
canonical: https://watchfor.io/docs/api/authentication
---

# WatchFor API authentication: API keys and OAuth 2.1

API keys (read or write scope) and OAuth 2.1 — how to create, scope, rotate and revoke credentials, and which surfaces accept which.

Every request to the REST API, the MCP server and the A2A endpoint
authenticates with a Bearer credential in the `Authorization` header.
Two kinds are accepted, and they are interchangeable across all three
surfaces:

| Credential | Looks like | Best for |
| --- | --- | --- |
| **API key** | `wf_live_…` | Scripts, CI, SDKs, the CLI, MCP clients configured by hand, any server-to-server integration |
| **OAuth 2.1 access token** | opaque token issued by WatchFor's authorization server | MCP clients and AI agents that support the MCP authorization flow — no human-issued key needed |

```bash
curl https://watchfor.io/api/v1/monitors \
  -H "Authorization: Bearer wf_live_YOUR_KEY"
```

Anything that isn't a `wf_` key is validated as an OAuth token. The
scheme is case-insensitive (`bearer` works). Any other scheme — `Basic`,
or a bare key without `Bearer` — returns `401` with "Unsupported
authorization scheme … — use: Authorization: Bearer wf_live_…", and an
empty credential returns `401` "Empty Bearer credentials".

## API keys

Create keys in the dashboard under **Settings → API keys** (organization
owners and admins only). Each key:

- belongs to exactly one **organization** — it can only ever see and change
  that organization's data;
- has a **scope**, fixed at creation: `read` (every GET endpoint and
  read-only tools) or `write` (`read` plus create, update, delete,
  acknowledge, resolve);
- can carry an optional **expiry** — a number of days, or an exact date
  and time, chosen when you create it — an expired key fails with `401`
  and the message "This API key has expired";
- can be **renamed** after creation (the rename is recorded in the
  activity log with the old and new name);
- is stored **hashed (SHA-256)** — the full `wf_live_…` value is shown
  exactly once, at creation. If you lose it, revoke it and create a new one;
- shows its **last used** time in the dashboard so stale keys are easy to spot;
- can be **revoked instantly** — requests fail with `401` from that moment.

Key creation and revocation are recorded in the organization
[activity log](/docs/organization/activity-log), and every action a key
performs is attributed to it there (`API key: <name>`). Read it back over
the API with [`GET /v1/activity`](/docs/api/activity).

### Scopes

| Scope | Grants |
| --- | --- |
| `read` | All GET endpoints; read-only MCP tools (`get_*`, `list_*`) and A2A skills |
| `write` | Everything in `read`, plus creating, updating and deleting monitors, alert rules, contacts, contact groups, maintenance windows, status pages and announcements, incident actions, on-demand checks and report-email settings |

A `read` key against a write endpoint returns `403 forbidden` ("This
endpoint requires a key with the write scope"). Check what a key can do
with `GET /v1/me`, which returns its `scope` and organization.

### Handling keys safely

- Keep keys in environment variables or a secrets manager — never in
  source control or client-side code.
- Create **one key per integration** ("CI", "Grafana", "Claude") so a leak
  is revocable without breaking everything else.
- Prefer `read` unless the integration genuinely mutates resources. A
  read-only key is also a safe way to let an AI agent explore your account.
- Rotate by creating a new key, switching the integration, then revoking
  the old one — both stay valid during the switch.

## OAuth 2.1

WatchFor runs an OAuth 2.1 authorization server built for the MCP
authorization specification. An MCP client (or any OAuth-capable agent)
registers itself, sends you to a WatchFor consent screen in your browser,
and receives a token — no API key is ever copied by hand.

| | |
|---|---|
| Discovery (authorization server metadata) | [`/.well-known/oauth-authorization-server`](https://watchfor.io/.well-known/oauth-authorization-server) and [`/.well-known/openid-configuration`](https://watchfor.io/.well-known/openid-configuration) |
| Protected-resource metadata | [`/.well-known/oauth-protected-resource`](https://watchfor.io/.well-known/oauth-protected-resource) |
| Authorization endpoint | `https://watchfor.io/api/auth/oauth2/authorize` |
| Token endpoint | `https://watchfor.io/api/auth/oauth2/token` (form-encoded) |
| Client registration | Dynamic (RFC 7591) at the `registration_endpoint` in the metadata — no manual app setup |
| PKCE | Required; `S256` only (the `plain` challenge method is rejected) |
| Consent | Every authorization shows an explicit consent screen; `prompt=consent` is enforced server-side |
| Scopes | `read`, `write` (plus `openid`, `profile`, `email`, `offline_access`) |
| Access token lifetime | 1 hour |
| Refresh token lifetime | 14 days (issued with `offline_access`); rotated on every use |
| Token storage | Refresh tokens, opaque access tokens and client secrets are stored hashed; public clients use PKCE without a secret |

When a token expires the API answers `401 unauthorized`; use the refresh
token at the token endpoint (`grant_type=refresh_token`) or re-run the
flow. MCP clients that follow the authorization spec do both
automatically. A `401` for a missing or unrecognised credential also
carries a
`WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource"`
header, so a client learns how to authenticate from its first failed
request.

OAuth access also ends with the account's own security actions: **resetting
your password** or **signing out of all sessions** revokes every OAuth
token you have granted, so a connector holding a stolen refresh token loses
it too. The app stays under *Account settings → Security → Connected apps*;
reconnecting goes through the consent screen again. Refresh tokens of
WatchFor's own apps are single-use (rotated on every refresh), and presenting
an already-used one revokes that app's sessions.

### How OAuth maps to your data

An OAuth token acts **as the user who approved it**, so it can reach every
organization that user belongs to. Pick one per request with the
`X-WatchFor-Organization` header:

```bash
curl https://watchfor.io/api/v1/monitors \
  -H "Authorization: Bearer YOUR_OAUTH_ACCESS_TOKEN" \
  -H "X-WatchFor-Organization: 0b6c2f1e-6d2a-4c47-9a51-3f0f2a9c8e11"
```

- [`GET /v1/organizations`](/docs/api/meta#list-organizations) lists the
  organizations the token can act on, with the user's role in each and
  which one is the default — it works without the header and isn't
  plan-gated, so it is the place to look up the ids.
- The user must be a member of that organization, otherwise the request
  fails with `403 forbidden`.
- Without the header, requests resolve to the user's oldest **owned**
  organization, or their oldest membership if they own none. A user with
  no organization gets `403` ("finish onboarding in the dashboard first").
- The header works the same on `/api/v1/batch` (every sub-operation uses
  it), the MCP server and A2A.
- With an API key the header is optional and may only repeat the key's
  own organization id — a key never switches organization, so any other
  id is `403`.
- The [SDKs](/docs/api#organizations-in-the-sdks) send it for you: pass
  `organization` when creating the client (CLI: `--organization <id>` or
  `WATCHFOR_ORGANIZATION`).

**Your role in the organization applies, exactly as in the dashboard.**
A token needs the `write` scope to change anything, and on top of that:

| Role | Can do through OAuth |
| --- | --- |
| Owner, admin | Everything the `write` scope allows |
| Member | Read everything, acknowledge incidents, run diagnostics. Creating, changing, pausing, resolving or deleting returns `403` with a message such as "Only owners and admins can resolve incidents" |

Activity-log entries made through OAuth are attributed as "Name (via
OAuth)". The selected organization's per-plan rate limit applies, as for
keys.

The authorization-server metadata also carries an `agent_auth` block
(registration URI, supported identity types) and points at
[`/auth.md`](https://watchfor.io/auth.md), a short machine-readable
description of the flow for AI agents working without a human.

## Which surface accepts what

| Surface | API key | OAuth token | Without credentials |
| --- | --- | --- | --- |
| REST API `/api/v1/*` | yes | yes | only `GET /openapi.json`, `GET /meta/monitor-types` and the [sandbox](/docs/api/sandbox) |
| [MCP server](/docs/api/mcp) `/api/mcp` | yes (header in the client config) | yes (client-driven flow) | `initialize`, `ping`, `tools/list`, `resources/*`, `prompts/*` |
| [Documentation MCP server](/docs/api/mcp#documentation-mcp-server) `/api/docs-mcp` | not needed | not needed | everything (read-only docs) |
| [A2A](/docs/api/a2a) `/api/a2a` | yes | yes | `agent/getAuthenticatedExtendedCard` and the Agent Card |

Machine-readable connection details for MCP clients:
[`/.well-known/mcp.json`](https://watchfor.io/.well-known/mcp.json).

## Failure modes

| Status | Meaning |
| --- | --- |
| `401 unauthorized` | Missing or malformed header, unknown, revoked or expired key, invalid or expired OAuth token |
| `403 forbidden` | Valid credential but insufficient scope, the plan has no API access, (OAuth) the user has no organization yet, isn't a member of the organization `X-WatchFor-Organization` names, or is a member without owner/admin rights for that change, or the account is suspended |
| `429 rate_limited` | Request budget exhausted — see [rate limits](/docs/api/errors#rate-limits) |

Unauthenticated requests to the API root (`GET /api/v1`) return a `401`
JSON body with links to the spec, the docs and the MCP/A2A endpoints —
useful as a starting point for tooling that discovers the API.

---

Canonical page: https://watchfor.io/docs/api/authentication · All docs: https://watchfor.io/docs · Site guide: https://watchfor.io/llms.txt
