---
title: Monitoring 100 client websites: a setup for agencies
description: One client site is a monitor. A hundred is a system: names and tags you can search, the same checks on every site, alerts that reach the right people, maintenance that doesn't page you, a status page per client and a number for the monthly report. Here's a setup that scales, what it costs in monitor slots, and the parts WatchFor doesn't do for agencies yet.
canonical: https://watchfor.io/blog/monitoring-client-websites-for-agencies
---

# Monitoring 100 client websites: a setup for agencies

2026-10-03 · WatchFor Team · Monitoring · monitoring, uptime, status-pages, automation

One client site is a monitor. A hundred is a system: names and tags you can search, the same checks on every site, alerts that reach the right people, maintenance that doesn't page you, a status page per client and a number for the monthly report. Here's a setup that scales, what it costs in monitor slots, and the parts WatchFor doesn't do for agencies yet.

The first client site you monitor takes two minutes. By the fortieth, the
problems are different. Which monitor belongs to which client? Who gets
woken up when the bakery's WordPress falls over at 2 a.m.? Why did the
domain of a client you built a site for three years ago expire on a
Saturday, and why did you hear about it from them?

None of that is solved by a better check. It's solved by setting up every
site the same way, from the start. This post is that setup for an agency
with around a hundred client sites in WatchFor, including the plan limits
that matter at this size and what WatchFor doesn't do for agencies yet.

## First decision: one organization, or one per client?

Everything in WatchFor lives in an organization. You can run
all clients in one, or give each client their own.

| | One agency organization | One organization per client |
| --- | --- | --- |
| Billing | One plan for everything | Each organization needs its own plan |
| Separating clients | Tags | Hard walls |
| Who can see what | Every member sees every client | Invite people per client |
| Report emails | One report for the whole organization | A report per client, as-is |
| Switching | None | Organization picker, with health per organization |

For most agencies the shared organization is the right default: one bill, one
place to look, and tags do the separating. It has one firm rule. **Don't
invite clients into it.** Members see every monitor in the organization, so a
client login would see your other clients. Give clients a status page and
report numbers instead (both below).

An organization per client suits the few large clients who want their own
logins, their own report emails, or to pay for monitoring themselves. You
can be a member of any number of organizations and own up to 10; each extra
one needs its own plan, or the client creates it and invites you as an
admin. The rest of this post assumes the shared organization.

## Names and tags: the part you can't fix later

With a hundred clients, the monitor list is only usable if you can type a
client's name and see their whole setup. Decide the conventions before the
first import.

**Names** start with the client, then the job, and stay short (the limit is
50 characters): `northwind website`, `northwind domain`,
`northwind shop api`. Sorting and searching then group a client's monitors
by themselves.

**Tags** carry everything you'll want to filter on. Each monitor takes up
to 10 tags of up to 32 characters, in lowercase letters, digits, hyphens
and colons, so `key:value` pairs work:

- `client:northwind` on every monitor of that client. Always. This one tag
  drives search, status pages, maintenance and the API.
- `tier:care-plan` (or whatever your retainer levels are called), so you
  can see at a glance which outages are covered by a contract.
- `stack:wordpress`, `stack:shopify`, `stack:nextjs`, because outages and
  maintenance cluster by platform.
- `env:staging` on the odd staging site, so it never gets confused with
  production.

The search box on the monitors list matches tags as well as names and
targets, so typing a tag gives you that slice of the fleet:

![The WatchFor monitors list in rows view, searched for the tag production: six monitors of different types (heartbeat, DNS, domain, two SSL certificates and an API check), each with its tags, status, target, 24-hour latency, uptime bars and interval](/blog/monitoring-client-websites-for-agencies-monitors.webp)

Our demo organization uses environment tags rather than client tags, but
`client:northwind` works the same way. Tags can be added and removed in bulk
(select monitors, then **Add tags**), and the API filters on them with
`GET /v1/monitors?tag=client:northwind`.

## What to monitor on every client site

Most client sites need the same two things watched, and they fit in two
monitors per site.

**1. One HTTP monitor that does three jobs.** An HTTP monitor checks that
the site answers with a good status code, and it can carry more rules than
people expect. On each site's monitor, switch on:

- **HTTP Status Evaluation** (on by default): anything outside `2xx` and
  `3xx` is a problem.
- **Response Body Match**: the page must *contain* something only the real
  site has, like the client's name in the footer. This catches the failures
  that still return `200`: a hosting "account suspended" page, a parked
  domain, a blank page from a broken theme, a site replaced by someone
  else's.
- **SSL Certificate**: alerts when fewer than *N* days remain on the
  certificate. It's off by default on HTTP monitors; turn it on, with
  enough days to fix a failed auto-renewal calmly (14 to 21 is sensible).

![The Alerting tab of an HTTP monitor in WatchFor: HTTP Status Evaluation, Response Time and SSL Certificate switched on, Response Body Match available, and a confirmation threshold of 3 consecutive failures](/blog/monitoring-client-websites-for-agencies-http-rules.webp)

That's uptime, content and certificate expiry for one monitor slot. A
separate SSL monitor adds the certificate chain, hostname and TLS version
details, which is worth it for the clients who care, but it isn't needed to
avoid an expired certificate.

**2. One domain monitor.** Domain registrations are the classic agency
disaster, because the renewal emails go to whoever registered the domain:
often the client, sometimes someone who left years ago. ICANN's
[Expired Registration Recovery Policy](https://www.icann.org/resources/pages/errp-2013-02-28-en)
requires registrars to send at least two reminders, about a month and about
a week before expiry, but only to the registered name holder. A WatchFor
domain monitor reads the expiry date from the registry and opens an incident
when fewer than 30 days remain (the default), so the warning comes to you.

**Worth adding for some clients:**

| Client need | Monitor |
| --- | --- |
| Page speed is in the contract | Core Web Vitals (a Lighthouse audit of one page) |
| A shop or a lead form makes them money | A [Playwright browser check](/blog/monitor-checkout-flow-with-playwright) of the checkout or contact form |
| You run their backups or cron jobs | A heartbeat the job pings |
| Their email keeps landing in spam | Email policy (SPF, DKIM, DMARC) and blacklist checks |

## The arithmetic: what 100 sites cost in monitor slots

WatchFor counts monitors, not sites or seats. Team members are included on
every plan. With the two-monitor baseline, 100 sites is 200 monitors. Here
are the limits that matter for an agency, from the current
[pricing](/pricing):

| | Free | Starter | Pro | Business |
| --- | --- | --- | --- | --- |
| Price per month | €0 | €7 | €25 | €49 |
| Monitors | 15 | 50 | 100 | 250 |
| Domain expiry monitors | none | 10 | 50 | 100 |
| Core Web Vitals pages | none | none | 15 | 30 |
| Status pages | 3 | 25 | 50 | 100 |
| Contact groups | 2 | 10 | 25 | 50 |
| Maintenance windows | 1 | 1 | 3 | 10 |
| Shortest check interval | 3 min | 1 min | 1 min | 30 s |
| Regions per monitor | 1 | 2 | 3 | all |

Yearly billing is cheaper (Business is €468 a year). What that means in
practice:

- **100 sites fit on Business.** 200 baseline monitors, exactly 100 domain
  monitors, and 50 slots left for Core Web Vitals on your top 30 clients and
  a few browser checks or heartbeats.
- **Pro fits 50 sites** with the same baseline: 100 monitors, 50 domains.
- **Past that, or for a custom setup,** the pricing page has an Enterprise
  contact option.

Two limits are easy to miss: contact groups and maintenance windows are far
fewer than your clients. Both have good answers, below.

## Alert routing: who hears about which client

WatchFor routes alerts through **contact groups**. A monitor names one or
more groups; each channel (email, Slack, Teams, PagerDuty, a webhook, and
others) is attached to one or more groups; when an incident opens, every
channel attached to the monitor's groups is notified, subject to the
channel's severity filter.

A layout that works for agencies:

- **An "Agency on-call" group on every monitor.** Email and Slack get
  everything; a pager channel on the same group gets only **critical**
  incidents. Warnings, like a certificate or domain that expires in 30
  days, go to email and Slack and become a ticket. They never wake anyone.
- **A client group only for clients who want alerts themselves.** Add their
  ops inbox as a **contact**: contacts receive notifications but can't log
  in and don't take a seat. Put the client's group on their monitors as a
  second target.
- **When you outgrow one group per client** (more than 25 or 50 clients who
  want direct alerts), group by account manager or by care plan instead.
  The people who need to know are usually the same few anyway.

Two safety nets: the dashboard's **Silent Monitors** card lists monitors
whose groups reach no enabled channel, and **Test notifications** in a
monitor's ⋯ menu shows exactly which channels an incident would reach.
[Contacts & contact groups](/docs/notifications/contact-groups) has the
details.

## Maintenance: one window per platform, not per site

A plugin update that takes a WordPress site down for two minutes shouldn't
page anyone or count as downtime. In a maintenance window the checks keep
running, but alerts are suppressed and the time is excluded from uptime.

With 3 or 10 windows you can't have one per site, and you don't need to.
**Target windows by tag.** A recurring window on `stack:wordpress` every
Tuesday evening covers every WordPress site you monitor, including the ones
you add next month. A one-off window on `client:northwind` covers that
client's migration. Each window can also post a *Scheduled maintenance*
notice on the client's status page in advance and mark it complete when the
window ends. [Maintenance windows done right](/blog/maintenance-windows-done-right)
explains why that beats pausing monitors.

## A status page for every client

A status page per client answers "is it down or is it just me?" before the
client picks up the phone, and doubles as the uptime history you'd otherwise
put in a report.

For each client:

1. Create a page with the client's name, a slug (the page lives at
   `watchfor.io/stats/<slug>` and also answers at `<slug>.watchfor.io`),
   their logo and a light, dark or automatic theme.
2. Make it **public**, or **password-protected** if the work is
   confidential.
3. Add components **by tag**. A component that includes every monitor
   tagged `client:northwind` picks up the monitors you add later, with no
   edit to the page.

![The Edit component dialog of a WatchFor status page: a Website component showing your own monitors, rolled up into one status, incident details set to Status only, and the choice between including monitors by tag or picking specific monitors](/blog/monitoring-client-websites-for-agencies-status-component.webp)

Confirmed incidents on those monitors are published automatically within
about a minute and resolve with them. With **Status only** (the default) the
page says the service is disrupted and nothing about why. You add the human
wording;
[incident status update templates](/blog/incident-status-update-templates)
has copy-paste updates for that. The page shows a 30-day uptime heatmap per
component, the incident history and an RSS feed. The
[complete status page guide](/blog/status-page-complete-guide) covers the
rest.

Before you promise a client anything, read the limits at the end of this
post: no custom domain yet, and the footer says "Powered by WatchFor".

## Monthly reports for clients

Every organization has two scheduled report emails, **weekly** (Mondays, the
previous seven days) and **monthly** (the 1st, the previous calendar month).
Recipients don't need a WatchFor account and can unsubscribe themselves.
Uptime in those reports uses the same rule as everywhere else in WatchFor:
only confirmed critical incidents count as downtime, maintenance is
excluded, and a single failed check from one location doesn't turn a
perfect month into 99.4%.

![The Email reports section of the WatchFor Reports page: a weekly report on Mondays covering the previous 7 days, switched on, and a monthly report on the 1st covering the previous month, switched off, each with its recipient](/blog/monitoring-client-websites-for-agencies-report-emails.webp)

Here's the honest part. In a shared agency organization, that email covers the
**whole organization**, so it's a report for you, not for a client. Reports
scoped to one client's tag are part of *advanced reports*, included on Pro
and Business: pick the client's tag as the scope and the report covers only
their monitors. There are no branded PDF client reports yet. Until there are,
three things work:

- **The client's status page is a live report.** 30 days of uptime per
  component and every incident, always current, at a link you can put in
  your monthly email.
- **The Uptime SLA tab** on each monitor breaks uptime down by day, week or
  month, with maintenance excluded, and prints as a report.
- **A short script against the API** pulls each client's numbers by tag:

```js
// node client-uptime.mjs client:northwind  →  30-day uptime per monitor
const API = process.env.WATCHFOR_API ?? 'https://watchfor.io/api/v1';
const headers = { Authorization: `Bearer ${process.env.WATCHFOR_API_KEY}` };
const tag = process.argv[2];

const get = async (path) => {
  const res = await fetch(`${API}${path}`, { headers });
  if (!res.ok) throw new Error(`${path}: ${res.status}`);
  return res.json();
};

const { data: monitors } = await get(`/monitors?tag=${encodeURIComponent(tag)}&limit=100`);
for (const m of monitors) {
  const u = await get(`/monitors/${m.id}/uptime?period=30d`);
  const uptime = u.uptime_percent == null ? '—' : `${u.uptime_percent.toFixed(3)}%`;
  const down = Math.round((u.downtime_ms ?? 0) / 60000);
  console.log(`${m.name.padEnd(30)} ${uptime.padStart(9)}  ${String(down).padStart(4)} min down  ${u.incident_count ?? 0} incidents`);
}
```

Run against our demo organization with its `production` tag, it prints:

```text
Nightly backup                   98.980%    15 min down  1 incidents
wikipedia.org DNS               100.000%     0 min down  0 incidents
wikipedia.org domain            100.000%     0 min down  0 incidents
github.com certificate          100.000%     0 min down  0 incidents
wikipedia.org certificate       100.000%     0 min down  0 incidents
Public API                      100.000%     0 min down  0 incidents
```

It needs only a **read** API key and works on every plan. Two caveats:
`30d` is a rolling 30 days, not a calendar month, and a monitor's uptime
starts counting when it was created, which is why a backup job added a few
days ago shows 98.98% for one 15-minute incident. For what a credible report
should and shouldn't claim, see
[how to read (and write) an SLA report that isn't lying](/blog/honest-sla-reports).

## Onboarding 100 sites without 100 forms

**Coming from UptimeRobot?** **Monitors → Import** connects with a
read-only UptimeRobot API key and shows a preview before anything is
created. You pick which monitors to bring over and which contact group they
alert. HTTP, keyword, ping and port monitors are imported (keyword monitors
keep their keyword rule), intervals faster than your plan allows are raised
to its minimum, every imported monitor is tagged `uptimerobot`, and
heartbeat monitors are skipped, because your jobs would need the new ping
URL anyway. StatusCake, Pingdom, Checkly, Site24x7 and Instatus work the
same way.

![The Import Monitors dialog in WatchFor with seven sources: CSV file, UptimeRobot, StatusCake, Pingdom (beta), Checkly, Site24x7 and Instatus](/blog/monitoring-client-websites-for-agencies-import.webp)

**From a spreadsheet,** the CSV import takes five columns:

```csv
name,type,target,interval,locations
northwind website,http,https://www.northwind.example,180,europe
northwind domain,rdap,northwind.example,86400,europe
```

The CSV has no tags column, so tag the imported monitors in bulk afterwards
from the list.

**From a script,** the cleanest way is to set up one site by hand exactly
how you want every site (rules, thresholds, contact groups, locations), and
then **clone** it for each client through the API. A clone copies the
template's alert rules and config, and you send only what differs:

```js
// node create-client-monitors.mjs clients.csv
// clients.csv:  client,site
//               northwind,https://www.northwind.example

const API = process.env.WATCHFOR_API ?? 'https://watchfor.io/api/v1';
const KEY = process.env.WATCHFOR_API_KEY;              // a write key
const WEB_TEMPLATE = process.env.WEB_TEMPLATE_ID;       // tuned HTTP monitor
const DOMAIN_TEMPLATE = process.env.DOMAIN_TEMPLATE_ID; // tuned domain monitor

const rows = readFileSync(process.argv[2], 'utf8')
  .trim().split('\n').slice(1)
  .map((line) => line.split(',').map((cell) => cell.trim()));

async function create(body) {
  const res = await fetch(`${API}/monitors`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${KEY}`,
      'Content-Type': 'application/json',
      // Re-running the script within 24 h never creates duplicates.
      'Idempotency-Key': `${body.tags[0]}:${body.name}`,
    },
    body: JSON.stringify(body),
  });
  const json = await res.json();
  if (!res.ok) throw new Error(`${body.name}: ${res.status} ${json.error?.message}`);
  console.log(`${res.status} ${json.name} → ${json.id}`);
}

for (const [client, site] of rows) {
  const tags = [`client:${client}`];
  const domain = new URL(site).hostname.replace(/^www\./, '');
  await create({ name: `${client} website`, cloneFromMonitorId: WEB_TEMPLATE, target: site, tags });
  await create({ name: `${client} domain`, cloneFromMonitorId: DOMAIN_TEMPLATE, target: domain, tags });
  await new Promise((r) => setTimeout(r, 2500)); // two calls per client stays under 60/min
}
```

We tested it against our demo organization: the clone of an HTTP monitor
arrived with the template's status, latency and SSL Certificate rules, and
the domain clone with its expiry rule, both tagged `client:…`. The API
allows 60 requests a minute on every plan; with the pause, 100 clients take
about five minutes and stay well under it. The
`Idempotency-Key` makes a re-run within 24 hours replay the first response
instead of creating duplicates. The Response Body Match keyword differs per
site, so set it on each site's Alerting tab, or give each client's template
its own.

An AI agent connected to the [WatchFor MCP server](/docs/api/mcp) can do
the same from a list of sites in a chat.

## What WatchFor doesn't do for agencies (yet)

Better to know now than after you've told a client:

- **No custom domains on status pages,** and the "Powered by WatchFor"
  footer stays. Pages live on `watchfor.io`.
- **No per-client report emails or branded PDF reports** in a shared
  organization. Use the status page, the Uptime SLA tab or the API, or give the
  client their own organization.
- **No client logins limited to their own monitors.** Every member of a
  organization sees all of it.
- **No per-client billing.** You buy one plan per organization.
- **No email subscriptions on status pages.** RSS, the badge and
  `status.json` are the ways to follow one.

## The setup, in order

1. Decide: shared organization, plus separate ones only for clients who need
   them.
2. Write down the naming pattern and tags (`client:`, `tier:`, `stack:`).
3. Build one template HTTP monitor (status, body match, SSL Certificate)
   and one template domain monitor, routed to your agency group.
4. Import from UptimeRobot or clone the templates for every client.
5. Set each site's body-match keyword.
6. Create channels: email and Slack for everything, a pager for critical.
7. Add recurring maintenance windows by `stack:` tag.
8. Create a status page per client with components by `client:` tag.
9. Switch on the monthly report for your team, and put the clients' status
   page links (or the script's numbers) in their monthly email.

The [tags docs](/docs/monitors/tags), [maintenance windows](/docs/monitors/maintenance)
and [Monitors API](/docs/api/monitors#clone-a-monitor) cover each step in
more detail.

---

Canonical page: https://watchfor.io/blog/monitoring-client-websites-for-agencies · All articles: https://watchfor.io/blog/all.md · Site guide: https://watchfor.io/llms.txt
