# What a contact is

A **contact** is a place HostTracker can deliver an alert or a scheduled report to: an email address, a phone
number, a chat app account, a webhook, an on-call integration key, and so on. Creating a contact does not alert
anyone by itself - a monitor has to be [subscribed](/alerts/subscriptions/) to it first, and even then an
**unconfirmed** contact receives nothing at all (see below).

## Contact types

The API's contact-type catalogue (`GET /contact/type`) publishes every type it knows about. Not all of them can
be created today - a few are registered for reads only (a stray/legacy row must still display correctly) but
hidden from creation:

| Type (API token) | Addressed by | Creatable via a plain address | Confirmable | Can receive reports |
|---|---|---|---|---|
| `email` | Email address | Yes | Yes (code) | Yes - the only type that can |
| `sms` | Phone number (international format) | Yes | Yes (code) | No |
| `voiceCall` | Phone number | Yes | Yes (code) | No |
| `http` | A URL you control, or a gateway-recognized webhook (Slack, Teams, PagerDuty, Opsgenie, Pushover, Pushbullet, Mattermost - see below) | Yes | No (born confirmed - there's no channel to verify) | No |
| `webPush` | A browser push subscription | Yes, but only from the browser's own subscription handshake, not a typed address | No (the browser step already proves it) | No |
| `telegram`, `viber`, `discord` | A messenger chat, linked by registering with the HostTracker bot | No - minted only by the bot's registration handshake | No (linking proves it) | No |
| `facebook`, `googleChat` | Same as above | No, and hidden from the New Contact picker too | No | No |
| `skype` | Retired channel | No - reads only, for accounts that still hold one | No | No |
| `whatsApp` | Phone number, via the Meta Cloud API | **Not yet** - parked pending Meta onboarding, hidden from the picker for every account | Yes (once it ships) | No |

:::note[Slack, Teams, PagerDuty, Opsgenie, Pushover, Pushbullet and Mattermost aren't separate contact types]
Under the hood every one of these is an `http` contact with a **gateway** value that tells HostTracker which
formatting and delivery rules to use (Slack is instead recognized from its `hooks.slack.com` webhook address).
The New Contact picker shows each as its own option so the setup flow matches the service you're connecting to
- see that channel's own page for the exact steps. **Opsgenie is currently hidden from the picker** for every
account (its region/custom-fields setup needs a wire member the contact API doesn't have yet) even though the
type exists and is registered; existing Opsgenie rows keep working.
:::

So in the app's **Add contact** picker you see fourteen live options: Email, SMS, Call, HTTP (webhook), Slack,
Microsoft Teams, PagerDuty, Pushover, Pushbullet, Mattermost, Telegram, Viber, Discord, and Web push. WhatsApp,
Opsgenie, Skype, Facebook and Google Chat are excluded from that picker today (WhatsApp and Opsgenie because
they aren't fully wired up yet; Skype because it's retired; Facebook and Google Chat by product decision even
though the underlying bot channels exist). See [all channels](/alerts/channels/) for a setup guide per type.

## Confirmation - why an unconfirmed contact gets nothing

Email, SMS and voice contacts must be **confirmed** before they receive anything real. This exists so a mistyped
address can't silently swallow alerts (or run up an SMS bill) forever.

- On create, HostTracker sends a **5-digit numeric code** to the address, valid for **30 minutes** with **3
  attempts** to enter it correctly. A still-valid, still-unused code is returned again on a resend request
  rather than replaced - so resending never invalidates a code you're already looking at.
- Chat-app and key-addressed contacts (Telegram, Viber, Discord, PagerDuty, Opsgenie, Pushover, Pushbullet,
  webhooks, web push) are confirmed **implicitly**, by the setup step itself - there's nothing separate to
  verify, because either the bot's own registration handshake or the browser's push-subscription exchange
  already proved the endpoint is real.
- If the account already holds a **confirmed** contact with the same type, address and delivery settings, a new
  one you create with that same address is born **already confirmed** - so adding a second alert-delay tier for
  a phone number you've already verified doesn't ask you to re-verify it. Changing a contact's **address**
  always resets it back to unconfirmed.
- **The gate is silent and it's enforced twice**: first when HostTracker decides who should hear about an event
  at all (an unconfirmed or over-package-limit contact is dropped from that list before anything is rendered),
  and again right before sending. Nothing surfaces this as an error anywhere - a subscription can exist, look
  correctly configured, and simply never deliver. Always confirm a contact right after creating it, and
  [send it a test alert](/alerts/test-a-contact/) to be sure.

## Settings reference

These are the fields on every contact, from the **Main Settings** group of the editor. A few (`mimeType`,
`httpHeaders`, `templates`) only apply to `http` contacts; `plainText` only to `email`.

| Setting (UI label) | API field (v2) | Type / allowed values | Default | Plan limits | What it does for you |
|---|---|---|---|---|---|
| Contact Type | `type` | One of the tokens in the table above | - (required on create) | - | Selects the channel; cannot be changed after creation (`type_immutable`) |
| Address | `address` | String - shape depends on `type` (email, E.164-ish phone, URL) | - (required for address-bearing types) | - | Where the alert is delivered |
| Contact name | `name` | String, up to 100 characters | Empty | - | A label to recognize it by later |
| Notification language | `language` | 2-letter code, or empty for "from profile" | From profile | - | Renders alert content in this language when a translation exists, else falls back to the profile language, then English. Voice scripts are English only regardless. |
| Alert delay | `alertDelay` | Minutes - one of a **fixed ladder**: `0` (instant), `3`, `5`, `15`, `30`, `60`, `180`, `360`, `720`, `1440` | `0` (instant) | - | How long a confirmed Down must persist before *this contact* hears about it - see [escalation](/alerts/escalation/) |
| Grouped alerts | `groupedAlerts` | boolean | Email: `true` (also when `POST /contact` omits it); HTTP-family: `false` | - | Combine alerts that fire close together into one message - see [grouped alerts](/alerts/grouped-alerts/) |
| Active hours | `activePeriod` | `{start, end, days[], timezone}` or `null` | `null` (always active) | - | Restrict delivery to chosen hours/days/timezone - see [Active hours](/alerts/active-hours/) |
| Billing notifications | `billingNotifications` | boolean (email only) | `false` | - | Also receive billing/payment emails at this address |
| News and updates | `sendNews` | boolean (email only) | `false` | - | Receive occasional product news |
| Plain-text emails | `plainText` | boolean (email only) | `false` (HTML) | - | Send plain text instead of the HTML design |
| Gateway | `gateway` | String - a recognized value (e.g. `Slack`, `Teams`, `PagerDuty`, `Opsgenie`, `Pushover`, `Pushbullet`, `Mattermost`), or absent | Server-routed (absent) | - | Pins an `http` contact to a specific channel's formatting/delivery rules instead of a plain generic webhook |
| MIME type | `mimeType` | String, e.g. `application/json` (http only) | `application/json` | - | The outgoing webhook body's content type |
| HTTP headers | `httpHeaders` | Array of `{header, value}` (http only) | Empty | - | Custom headers sent with every webhook request (auth tokens, routing headers) |
| Custom templates | `templates` | Array of `{event, content}`, event = `up`/`down`/`repeatedlyDown` (http only) | Empty (uses the built-in payload) | - | Author the exact JSON/text HostTracker posts for each event - see [Webhook](/alerts/channels/webhook/) for the `[[token]]` variables available |

Two members exist only on the wire, not as an editor field: `defaultSubscriptions: {alerts, reports}` (on
create only - seed this contact onto **every** existing monitor for Up+Down alerts and/or scheduled reports in
the same call) and inline `alertSubscriptions[]` / `reportSubscriptions[]` (subscribe specific monitors at
create time). Both are additive conveniences over calling the subscription endpoints separately afterward.

:::caution[The active-hours timezone member is spelled differently on two doors]
Inside `activePeriod`, the v2 API spells the zone member **`timezone`** (lowercase), while the app's own
internal contact model (and the legacy v1 door) spells it **`timeZone`**. Both accept an IANA id (e.g.
`Europe/Berlin`) on input and read it back the same way; storage keeps a Windows zone id internally and the read
is that Windows zone's IANA *representative*, so a contact saved as `Europe/Rome` can read back as
`Europe/Berlin` if the two share one underlying Windows zone - a documented rounding, not a bug. A `null` zone
means the stored hours are plain UTC, not "use my profile timezone".
:::

## Set it up in the app

1. Open **Alerts & Contacts** in the sidebar and click **+ Add contact**.
2. Choose the **Contact Type**. The fields below it change to match - a typed address box for Email/SMS/Call,
   a bot-connect flow for Telegram/Viber/Discord, a browser-permission prompt for Web push, or a URL +
   Request Template group for HTTP/Slack/Teams/etc.
3. Enter the address (or complete the connect flow) and give the contact a **name**.
4. Expand **Main Settings** to set language, alert delay, active hours, and the type-specific switches from the
   table above.
5. Click **Save**. For Email/SMS/Call, HostTracker sends the confirmation code immediately.
6. Confirm it (link, code entry, or resend from the contact's row), then
   [send a test alert](/alerts/test-a-contact/).

## Do it with the API or MCP

Create an email contact:

```
POST /contact
{ "type": "email", "address": "ops@example.com", "name": "Ops inbox", "alertDelay": 0 }
```

The response is the new `ContactView`, `confirmed: false`, plus a `confirmation` block (`{sent, channel,
expiresAt, triesAllowed}`). Confirm it:

```
POST /contact/{id}/confirmation/verify
{ "code": "48213" }
```

Resend the code if it expired or wasn't sent:

```
POST /contact/{id}/confirmation
```

Update settings (partial - only the fields you send change):

```
PATCH /contact/{id}
{ "alertDelay": 15, "groupedAlerts": true }
```

List, read, delete: `GET /contact`, `GET /contact/{id}`, `DELETE /contact/{id}`. MCP tools:
`create_contact` (email, sms, voiceCall or webPush only - messenger contacts are registered from the messenger
itself, and `http`/webhook is deliberately excluded from this tool: use `create_webhook` under
[Integrations](/integrations/webhooks/) for signed HTTP delivery instead), `update_contact`, `delete_contact`,
`send_contact_confirmation`, `confirm_contact`, `list_contacts`, `get_contact`, `test_contact`.

## What happens next

A newly created contact is unconfirmed (except the implicitly-confirmed types) and has no subscriptions unless
you set `defaultSubscriptions`/inline subscriptions on create. It receives nothing until it's both confirmed
**and** subscribed to at least one monitor for at least one event. Once both are true, it's evaluated against
its own alert delay, active hours and grouping settings at send time - not at subscribe time.

## Limits and gotchas

- **`422 validation_failed`** (reason `unknown_member`) - the create/update body has a closed vocabulary; an
  unrecognized top-level member, or a misspelled nested one (like `activePeriod.timeZone` instead of
  `timezone`), is refused with an `allowed[]` list rather than silently dropped.
- **`422 type_immutable`** - you can't change `type` after creation. Create a new contact instead.
- **`422 validation_failed`** (reason `undeliverable_domain`) - a new email address is checked for a working MX record
  at create/update time and refused if the domain can't receive mail at all (this check fails open on DNS
  trouble, so it never blocks a real domain having a bad day).
- **`429 rate_limited`** on a confirmation resend - capped at roughly 10 per contact per day and 50 per account
  per day, plus a short per-request cooldown; the response carries `Retry-After`.
- **`422 invalid_confirmation_code`** - wrong, expired, or attempts exhausted; carries `attemptsLeft` and
  `expiresAt` so a client can tell a user exactly what's wrong. **`409`** if the contact is already confirmed.
- A contact can be deleted with everything it's subscribed to - `DELETE /contact/{id}` also removes its
  subscriptions. There is no undo.
- Plan limits cap the total number of contacts and, per type, how many of each; an over-limit create either
  refuses (`403 package_limit` on v2) or, on the app's create-anyway path, lands the contact **paused**
  (unconfirmable, no send) until you free up room.

## Related

- [Subscriptions: bind a monitor to a contact](/alerts/subscriptions/)
- [Contact groups](/alerts/contact-groups/)
- [Set up each channel](/alerts/channels/)
- [Alert delays & escalation](/alerts/escalation/)
- [Active hours](/alerts/active-hours/)
- [Test a contact](/alerts/test-a-contact/)
