Skip to content

Notification channels

Each owner can register their own outbound notification channels — one or more URLs that every booking event (new booking, cancellation, reschedule, approval request, reminder, and the rest) is sent to. Channels are independent of the owner-email setting: email goes out separately, and channels deliver whether or not Send me (the owner) email notifications for bookings is checked. That setting only governs your own email; a registered channel always fires.

From /me/settings, under Notification channels:

  1. Paste a channel URL into the empty row (see Getting a channel URL below for each provider).
  2. Give it a Label — a short name for your own reference. Leave it blank and calit fills in the channel’s display name (e.g. “Telegram”).
  3. Press Send test on that row to confirm delivery. This works before you save, so you can correct a wrong URL without storing it first.
  4. Leave Default ticked to notify this channel for every meeting type, or untick it to keep the channel registered but silent until a meeting type names it explicitly.
  5. Press Save channels.

Default is the enable switch for the inherit set. Unticking it does not delete the channel — the URL stays registered and testable, and any meeting type that names the channel explicitly still delivers to it. Deleting the row is how you remove a channel entirely.

A new channel is created with Default on. The checkbox needs a saved row to carry, so untick it on the next save if you want the channel silent by default.

Every channel is an Apprise-style URL — the same shape Apprise popularised, implemented here by notify4j, whose Channel URLs reference is the authority on the exact syntax calit accepts. Here is how to obtain one per provider:

  1. Message @BotFather on Telegram, send /newbot, and follow the prompts. BotFather gives you a bot token (looks like 123456:AAbbCCddEE...).

  2. Get the chat id to send to — message your new bot (or add it to a group), then call https://api.telegram.org/bot<token>/getUpdates and read the chat.id field from the response.

  3. The channel URL carries the Bot API host, then the token, then the chat id:

    telegram://api.telegram.org/<bot-token>/<chat-id>

    The api.telegram.org part is required — it is the host the message is posted to, not a placeholder. A URL without it has nowhere to put the chat id and can never deliver.

  1. Create an incoming webhook for the channel you want notifications posted to.
  2. Slack gives you a webhook URL; the channel URL uses the same path components after the slack:// scheme; the exact token layout is in Slack’s incoming-webhook docs.
  1. In the target Discord channel’s settings, add a webhook (Server Settings → Integrations → Webhooks → New Webhook).
  2. Discord gives you a webhook URL; use it in discord:// form with the webhook id and token.
  1. Pick a topic name (a private, hard-to-guess string works as the access control) on ntfy.sh or your own self-hosted ntfy server.

  2. The channel URL names the server first, then the topic:

    ntfy://ntfy.sh/<topic>

    For a self-hosted server, swap in your own host — and use the +http transport suffix if it is not behind TLS:

    ntfy+http://<host>:<port>/<topic>
  1. In your Gotify server’s web UI, create an application — Gotify gives you an app token.

  2. The channel URL points at your Gotify server with that token:

    gotify://<host>/<app-token>

Two references, for two different questions. For the URL syntax calit accepts, use notify4j’s own Channel URLs reference — calit implements notify4j’s catalog, which follows Apprise conventions without being identical to it, so Apprise’s reference can describe field layouts calit rejects. For how to obtain the token or webhook in the first place, use the provider’s own docs below. The ? icon beside the Notification channels heading on /me/settings opens this page.

SchemeChannelWhere the URL format is documented
telegram://TelegramTelegram bots
slack://SlackSlack incoming webhooks
discord://DiscordDiscord webhooks
ntfy://, ntfy+http://ntfyntfy docs
gotify://GotifyGotify push
teams://Microsoft TeamsTeams incoming webhooks
googlechat://Google ChatGoogle Chat webhooks
mattermost://MattermostMattermost incoming webhooks
rocketchat://Rocket.ChatRocket.Chat integrations
matrix://MatrixMatrix docs
mastodon://MastodonMastodon tokens
bluesky://BlueskyAT Protocol
signal://Signalsignal-cli-rest-api
pushover://PushoverPushover API
pushbullet://PushbulletPushbullet API
zulip://ZulipZulip send-message
pagerduty://PagerDutyEvents API v2
opsgenie://OpsgenieOpsgenie API integration
twilio://Twilio SMSTwilio SMS
whatsapp://WhatsAppWhatsApp Cloud API
webhook://Generic webhookPosts JSON to any URL you control — see the caveat below

An operator can restrict which of these are accepted with NOTIFY_ALLOWED_SCHEMES.

A URL that is missing a part its channel needs — a Telegram URL with no chat id, an ntfy URL with no topic — is refused when you save it, with a message naming the channel type. It is never stored, because a stored channel that cannot deliver would fail silently on every booking.

Any HTTP endpoint that accepts a JSON POST can be used directly as a webhook:// or webhook+http:// URL, with no provider-specific setup. This is the fallback for anything not natively supported.

By default a meeting type notifies every channel the host has marked Default. On a meeting type’s page, under Notifications, a host can instead pick a custom subset for that specific type — and that subset may include channels that are not Default, which is how a channel is scoped to one meeting type without touching any other. Non-default channels are shown there with a not by default badge.

Naming a channel on a meeting type always wins over its Default setting: the explicit pick is the opt-in.

This override is per host: on a shared (multi-host) meeting type, each co-host sets their own routing independently from their own view of the type. A co-host narrowing their own notifications down to one channel has no effect on what the creator, or any other co-host, receives.

What the UI can and can’t tell you about a failed delivery

Section titled “What the UI can and can’t tell you about a failed delivery”

Each channel row shows the timestamp of its last successful and last failed delivery, so you can see that a delivery failed and when. It cannot show why: notify4j reports only a pass/fail count for a delivery attempt, with no HTTP status code or exception detail, so there is nothing more specific calit could honestly display. If a channel is failing, double check the URL with Send test, and confirm the destination (bot, webhook, topic) still exists and accepts the token.

Channel URLs are never shown again in full

Section titled “Channel URLs are never shown again in full”

Once saved, a channel’s URL is never rendered back to you in the clear — the settings page shows a redacted form instead, e.g. telegram://… or ntfy+http://host:port/…, with the secret portion replaced. Leaving that redacted value in place and pressing Save channels keeps the stored URL unchanged; pasting a redacted value into a different (empty) row is rejected rather than silently stored as a dead channel.

One consequence: two channels of the same scheme render an identical mask. If you have two Telegram channels, both show as telegram://… in the list — the Label is the only thing that tells them apart, so label your channels meaningfully.

Operator note: NOTIFY_MAX_ATTEMPTS and worker pressure

Section titled “Operator note: NOTIFY_MAX_ATTEMPTS and worker pressure”

Delivery retries run synchronously on a background thread and block that worker until they either succeed or exhaust their attempts. At the default of 3 attempts, an unreachable channel occupies a worker for up to roughly 33 seconds (three 10-second request timeouts, plus 1 s and 2 s of backoff between attempts). On an instance with many registered channels and a large reminder burst, lowering NOTIFY_MAX_ATTEMPTS reduces how long a single bad channel can hold a worker thread.

See Configuration for the full list of NOTIFY_* environment variables.