Contacts and on-call
Contacts are the people (and devices) Northplane notifies. Contact groups bundle them, on-call schedules say who is on duty when, and escalation policies pick one of the three per step. This page covers the people side; how steps fire is on Escalation policies, how a channel type becomes a real delivery is on Notification channels.
| Resource | API | Permissions | UI |
|---|---|---|---|
Contact (contact, bundle Contact) |
/api/v1/contacts |
oncall:read / oncall:write |
Admin → Contacts (Kontakte) |
Contact group (contact-group, bundle ContactGroup) |
/api/v1/contact-groups |
oncall:read / oncall:write |
Admin → Contact groups (Kontaktgruppen) |
Time period (time-period, bundle TimePeriod) |
/api/v1/time-periods |
objects:read / config:write |
Templates → Time periods (Zeiträume) |
Schedule (schedule, bundle Schedule) |
/api/v1/schedules |
oncall:read / oncall:write |
On-Call (Bereitschaft) page |
Override (override, not in bundles) |
/api/v1/schedules/{name}/overrides |
oncall:read / oncall:write |
On-Call page → Overrides |
All of them follow the generic resource API (GET/POST list, GET/PUT (with If-Match)/DELETE by name; names or ids resolve everywhere). Generated reference: post_contacts, post_contact_groups, post_schedules, get_oncall_now.
Contacts
Section titled “Contacts”| Field | Notes |
|---|---|
name |
unique per tenant; used as the actor name when this person acknowledges by ack link, SMS or phone |
email |
target for email channels |
phone |
E.164 (+4366412345678); target for sms and voice; also used to recognise inbound callers and SMS senders (only digits and + are compared, so formatting differences do not matter) |
userId |
the user id of a browser login or the API-token id of a device — target for push (Mobile push) |
timeZone |
IANA name (Europe/Vienna); the zone in which the preference periods are evaluated; default UTC |
preferences[] |
ordered channel preferences, see below |
kind: Contactmetadata: { name: bereitschaft }spec: phone: "+4366412345678" userId: 0191c0de-… # API-token id of the phone, for push timeZone: Europe/Vienna preferences: - { profile: worktime, period: worktime, channels: [push, email] } - { profile: night, period: night, channels: [voice, sms, push], severity: warning } - { profile: default, channels: [email] }A contact’s full data (the contact, the notification events that mention it and its audit entries) can be exported with GET /api/v1/contacts/{name}:data-export (permission admin:audit, format northplane-gdpr-export/1).
Channel preferences
Section titled “Channel preferences”A preference is {profile, period, channels[], severity}:
| Field | Notes |
|---|---|
profile |
a free label (default, worktime, night, anything) — not evaluated, shown in the UI only |
period |
what is matched: the name of a stored TimePeriod (wins), or one of the built-ins worktime / arbeitszeit (Monday–Friday 08:00–17:59 in the contact’s timeZone) and night / nacht (everything else). Empty = the fallback preference |
channels |
ordered list of channel types (email, sms, voice, push, ntfy, slack, teams, webhook, mqtt, servicenow, zendesk, jira, ticket; the UI picker offers the first eight) — every listed type gets its own delivery |
severity |
minimum severity (critical > warning > info > ok); the preference is skipped for less severe alerts. Empty = all |
Resolution (PreferredChannels) for an alert of severity S at time now:
- Walk the preferences in list order, skipping every preference whose
severityis higher than S. - A preference with a
periodwhose TimePeriod (stored first, then built-in) contains now in the contact’s time zone wins immediately — itschannelsare used. A stored period that does not contain now is skipped; a period name that is neither stored nor built-in never matches. - If no period matched, the first period-less preference (that passed the severity filter) is the fallback.
- If nothing matched at all, Northplane falls back to
[email].
In the example above a critical alert at 03:00 Vienna time goes out as voice, SMS and push; an info alert at 03:00 skips the night preference (minimum warning) and falls back to e-mail.
Preferences are also used by object-level routing (below) — there the severity is the problem’s severity, and for recoveries the severity of the state recovered from, so whoever was paged for the outage also gets the recovery.
Contact groups
Section titled “Contact groups”{name, members[], idpGroup} — members are contact ids or names. Groups are referenced by escalation steps (notify: {contactGroup: leitung}) and by objects (contactGroups). idpGroup (an Entra/Keycloak group id) is stored for reference only — there is no automatic membership sync from the directory.
kind: ContactGroupmetadata: { name: leitung }spec: members: [bereitschaft, ops-alice]Time periods
Section titled “Time periods”TimePeriods are shared by monitoring (check and notification periods) and alarming (contact preferences, schedule restrictions). The format — days with HH:MM-HH:MM ranges per weekday, exceptions by date, wrapping windows like 19:00-07:00, 00:00-00:00 = closed, the well-known 24x7 — is documented on Maintenance. Two things matter here:
- A TimePeriod has no time zone. Each consumer decides: contact preferences evaluate it in the contact’s
timeZone; schedule layer restrictions in the schedule’stimeZone; an object’snotificationPeriodin UTC. - Stored periods win over the built-in
worktime/nightnames — define a TimePeriod calledworktimeand it replaces the built-in definition for preferences.
kind: TimePeriodmetadata: { name: buerozeiten }spec: alias: Bürozeiten days: monday: ["08:00-18:00"] tuesday: ["08:00-18:00"] wednesday: ["08:00-18:00"] thursday: ["08:00-18:00"] friday: ["08:00-18:00"] exceptions: "2026-12-24": ["00:00-00:00"]On-call schedules
Section titled “On-call schedules”A schedule is a set of layers (rotations); each layer cycles its participants in order from an anchor time, optionally restricted to duty windows. Overrides replace whoever is on duty for a window. Resolution always yields one person (the first matching override, else the first layer whose restriction covers the moment).
| Field | Notes |
|---|---|
name |
referenced by policies: notify: {schedule: bereitschaft} |
timeZone |
IANA; handoffs of daily/weekly rotations happen at the anchor’s wall-clock time in this zone (DST-safe); restrictions are evaluated here |
layers[] |
at least one participant per layer (validation 422) |
Layer (Rotation):
| Field | Notes |
|---|---|
name |
optional label (shown in oncall/now shifts) |
participants |
contact ids (names resolve too), in wheel order |
unit |
daily (24 h), weekly (7 days) — calendar arithmetic in timeZone; custom — plain arithmetic with length |
length |
custom only; default 24h |
anchor |
RFC 3339 time: start of participants[0]’s first shift; the wheel extends backwards too |
restriction |
TimePeriod-style days map; the layer is only on duty inside these windows (e.g. nights + weekend) |
kind: Schedulemetadata: { name: bereitschaft }spec: timeZone: Europe/Vienna layers: - name: primary participants: [alice, bob, carol] unit: weekly anchor: "2026-01-05T08:00:00+01:00" # Monday 08:00 handover - name: nights participants: [dave] unit: custom length: 12h anchor: "2026-01-05T19:00:00+01:00" restriction: monday: ["19:00-07:00"] tuesday: ["19:00-07:00"] wednesday: ["19:00-07:00"] thursday: ["19:00-07:00"] friday: ["19:00-07:00"] saturday: ["00:00-24:00"] sunday: ["00:00-24:00"]Resolution at time t (ResolveOnCall):
- With offset 0, the first override covering t wins (
{contactId, start, end, override: true}). - Otherwise layers in order: the first layer whose
restrictioncontains t (or has none) yieldsparticipants[(index + offset) mod n], where index is the shift number sinceanchor. - Backup (
escalateTo: backupin a policy step) = offset 1: the next person on the winning layer’s wheel. Overrides are not applied for the backup — if Alice is overridden by Erin this week, the backup is still Bob (the person after Alice), not the person after Erin.
In the example, step 1 of a policy (notify: {schedule: bereitschaft}) at 03:00 on a Tuesday resolves the primary layer first — it has no restriction, so Dave’s nights layer is never reached at that position. Put restricted layers first when they should take precedence during their windows.
Overrides
Section titled “Overrides”| Method & path | Permission | Notes |
|---|---|---|
POST /api/v1/schedules/{name}/overrides |
oncall:write |
body {"contactId", "start", "end", "reason"}; end must be after start; audited override.create |
GET /api/v1/schedules/{name}/overrides |
oncall:read |
sorted by start |
DELETE /api/v1/schedules/{name}/overrides/{id} |
oncall:write |
audited override.delete |
curl -X POST https://monitoring.example.net/api/v1/schedules/bereitschaft/overrides \ -H "Authorization: Bearer np_…" -H "Content-Type: application/json" \ -d '{"contactId":"erin","start":"2026-08-24T06:00:00Z","end":"2026-08-31T06:00:00Z","reason":"Alice on vacation"}'Overrides are stored as resources of kind override whose scheduleId is the schedule name; they are not part of bundles. In the UI, the Overrides button on a schedule card opens a dialog (contact, reason, from/to) and lists the active ones.
Who is on call — endpoints
Section titled “Who is on call — endpoints”| Method & path | Permission | Returns |
|---|---|---|
GET /api/v1/oncall/now?schedule=<name> |
oncall:read |
[{schedule, shifts: [{contactId, layer, start, end, override}], contacts: [Contact…]}] for all (or one) schedules — the data behind the Now cards and the Overview widget |
GET /api/v1/schedules/{name}/timeline?from=&to=&days= |
oncall:read |
resolved, merged shifts (default: last 14 days); overrides split the underlying shifts |
GET /api/v1/schedules/{name}/stats?from=&to=&days= |
oncall:read |
[{contactId, contact, hours, weekendHours, overrides}] over the timeline (default 30 days) |
GET /api/v1/schedules/{name}/ics |
oncall:read |
iCalendar feed, now − 7 days … now + 60 days; SUMMARY: On-Call: <name> ( (Override) appended), UID: <scheduleId>-<i>@northplane |
Generated reference: get_schedules_name_timeline, get_schedules_name_stats, get_schedules_name_ics, post_schedules_name_overrides.
The On-Call page
Section titled “The On-Call page”On-Call (Bereitschaft) in the sidebar shows, per schedule, a Now card (who is on duty with e-mail and phone, ICS download), the Schedules (Dienstpläne) manager — a dialog with name (locked when editing), time zone (default Europe/Berlin), and layers (optional name, rhythm daily/weekly/custom, participants with contact suggestions, shift length for custom, anchor, restriction as key/value rows like mon=08:00-17:00) — plus a 14-day timeline card per schedule (override = amber ring), the hours per person (30 days) table and the Overrides dialog. Details of the page: Alerts, incidents, events and on-call.
Per-object contact routing (no rule needed)
Section titled “Per-object contact routing (no rule needed)”Nagios-style routing is built into the object model: a host or service whose effective spec (after templates) carries contacts and/or contactGroups notifies them directly on every hard state change, without any alert rule or escalation policy.
| ObjectSpec field | Notes |
|---|---|
contacts, contactGroups |
contact / group names (or ids) |
enableNotifications |
false switches routing off (default on) |
notifyOn |
which hard transitions notify: warning, critical, unknown (services), down, unreachable (hosts), recovery; empty = all problem states + recovery |
notificationPeriod |
TimePeriod name; outside the window nothing is sent (evaluated in UTC) |
kind: Hostmetadata: { name: db-01, folder: /prod }spec: address: 10.0.0.5 checkCommand: builtin:icmp contactGroups: [leitung] notifyOn: [down, unreachable, recovery] notificationPeriod: buerozeitenGates, in order: hard state change only; enableNotifications not false; notifyOn filter (a recovery counts only after a non-OK state); notificationPeriod contains now; the object is not in a downtime; an acknowledged problem stays quiet (recoveries still go out — the sticky ack is cleared on hard recovery). Each contact’s channels come from its preferences (severity = the problem’s, or the recovered-from state for recoveries), fallback email; there is no step channel override here. Deliveries are outbox items of kind object-notification — same channels, retries and dead letters as alerts — with the title <host> / <service> is <STATE>: <output> or … recovered (<from> → <to>), .Policy = object in templates, a link to /objects/<id> instead of an alert page and no ack link (there is no alert to acknowledge). The resulting notification events carry an empty alertId. Object fields and templates: Object model, Hosts and services; suppression details: Checks and states.
Putting it together
Section titled “Putting it together”For an alert, an escalation step resolves people in this order: notify.schedule (current on-call, or backup) → else notify.contactGroup members → else notify.contact; for each contact the channel types are the step’s channels, else the contact’s matching preference, else [email]; for each type the tenant’s first enabled channel of that type delivers to the contact’s email / phone / userId or the channel’s url. Every (contact, type) pair is one delivery with its own retries and its own notification event. Verify a configuration without paging anyone with POST /api/v1/escalation-policies/{name}:simulate or the Simulate button (Escalation policies).