# Restrict a contact to active hours

**Active hours** let a contact only receive alerts during a window you choose, instead of around the clock. It's
set per contact, so different contacts can have completely different schedules.

## When to use it

- A personal phone (SMS or voice) that should only ring during work hours, or your own waking hours.
- A weekday-only contact for someone who isn't on call at weekends.
- Splitting coverage across timezones, so each person's contact is only active during their own local hours.

Keep at least one contact **always-on** (typically email, Slack, or an on-call channel) so outages outside every
active-hours window still reach someone.

## Settings reference

| Setting (UI label) | API field (v2) | Type / allowed values | Default | Plan limits | What it does for you |
|---|---|---|---|---|---|
| Time of activity | `activePeriod` (whole object, or `null`) | `null`, or `{start, end, days[], timezone}` | `null` ("Always active") | - | Switches between always-on and a restricted window |
| Start / End | `activePeriod.start` / `activePeriod.end` | Clock time, `"HH:mm"` or `"HH:mm:ss"` | `00:00:00` / `23:59:59.999` when the window is created without them | - | The daily window's open and close time |
| Days | `activePeriod.days` | Array of day names (`Monday`...`Sunday`) | Every day, when the member is omitted | - | Which days of the week the window applies on |
| Timezone | `activePeriod.timezone` | IANA id, e.g. `Europe/Berlin`, or `null` | `null` (times are plain UTC, **not** your profile timezone) | - | The zone the start/end clock times are read in |

An **empty `days` array is not "every day" - it's "no day"**, i.e. the contact is paused entirely. Leaving
`days` out of the request is what means "every day"; sending it explicitly empty is how the app itself
represents a paused contact.

:::caution[Two different spellings of the timezone field]
The v2 API spells it **`timezone`** (all lowercase, nested under `activePeriod`). The contact editor's own
internal model, and the legacy v1 door, spell the same thing **`timeZone`** (capital Z). If you're scripting
against the API directly, use the exact casing for the door you're calling - a misspelled nested field name
here is silently dropped rather than refused, so a wrong-cased `timeZone` on v2 looks like a successful save
that quietly kept the contact's old zone.
:::

## Set it up in the app

1. Open the contact and expand **Main Settings**, then **Time of activity**.
2. Switch from **Always active** to **Active during hours**.
3. Choose the **days of the week** it should be active.
4. Set the **start and end time**.
5. Choose the **timezone** the hours are evaluated in.
6. Save.

## Do it with the API or MCP

```
PATCH /contact/{id}
{ "activePeriod": {
    "start": "09:00:00", "end": "18:00:00",
    "days": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
    "timezone": "Europe/Berlin"
} }
```

Clear it back to always-on:

```
PATCH /contact/{id}
{ "activePeriod": null }
```

There's no dedicated MCP tool for active hours today - use `update_contact` for the fields it exposes
(name, address, language, `alertDelay`, `groupedAlerts`), or `api_request` with the `PATCH /contact/{id}` body
above for `activePeriod` specifically.

## What happens next

Outside the configured window, alerts to that contact are simply **not delivered** - they aren't queued or
delayed until the window opens, and a Down that happens entirely outside the window (and recovers before it
opens) never reaches that contact at all. This is evaluated fresh at the moment of delivery, independently of
[alert delay](/alerts/escalation/) - a contact can be both delayed and windowed at once.

## Limits and gotchas

- **`422` on a malformed time** - `start`/`end` must parse as `HH:mm` or `HH:mm:ss`; anything else is refused
  with a `malformed` reason rather than silently rounded.
- **`422 unknown_enum_value`** for a `days` entry that isn't a real day name.
- Active hours are evaluated **at send time**, not when the outage started - so if the window opens partway
  through an ongoing outage, this contact can still receive the (already-late) Down alert once it does.
- A contact with no zone set (`timezone: null`) is evaluated in plain UTC, which is easy to mistake for "my
  profile's timezone" - it isn't. Always set a zone explicitly for a window you expect to line up with local
  time.

## Related

- [What a contact is](/alerts/contacts/)
- [Alert delays & escalation](/alerts/escalation/)
- [Subscriptions: alert vs report](/alerts/subscriptions/)
