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.
Adding a channel
Section titled “Adding a channel”From /me/settings, under Notification channels:
- Paste a channel URL into the empty row (see Getting a channel URL below for each provider).
- 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”).
- 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.
- 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.
- 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.
Getting a channel URL
Section titled “Getting a channel URL”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:
Telegram
Section titled “Telegram”-
Message @BotFather on Telegram, send
/newbot, and follow the prompts. BotFather gives you a bot token (looks like123456:AAbbCCddEE...). -
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>/getUpdatesand read thechat.idfield from the response. -
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.orgpart 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.
- Create an incoming webhook for the channel you want notifications posted to.
- 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.
Discord
Section titled “Discord”- In the target Discord channel’s settings, add a webhook (Server Settings → Integrations → Webhooks → New Webhook).
- Discord gives you a webhook URL; use it in
discord://form with the webhook id and token.
-
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.
-
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
+httptransport suffix if it is not behind TLS:ntfy+http://<host>:<port>/<topic>
Gotify
Section titled “Gotify”-
In your Gotify server’s web UI, create an application — Gotify gives you an app token.
-
The channel URL points at your Gotify server with that token:
gotify://<host>/<app-token>
Every supported channel
Section titled “Every supported channel”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.
| Scheme | Channel | Where the URL format is documented |
|---|---|---|
telegram:// | Telegram | Telegram bots |
slack:// | Slack | Slack incoming webhooks |
discord:// | Discord | Discord webhooks |
ntfy://, ntfy+http:// | ntfy | ntfy docs |
gotify:// | Gotify | Gotify push |
teams:// | Microsoft Teams | Teams incoming webhooks |
googlechat:// | Google Chat | Google Chat webhooks |
mattermost:// | Mattermost | Mattermost incoming webhooks |
rocketchat:// | Rocket.Chat | Rocket.Chat integrations |
matrix:// | Matrix | Matrix docs |
mastodon:// | Mastodon | Mastodon tokens |
bluesky:// | Bluesky | AT Protocol |
signal:// | Signal | signal-cli-rest-api |
pushover:// | Pushover | Pushover API |
pushbullet:// | Pushbullet | Pushbullet API |
zulip:// | Zulip | Zulip send-message |
pagerduty:// | PagerDuty | Events API v2 |
opsgenie:// | Opsgenie | Opsgenie API integration |
twilio:// | Twilio SMS | Twilio SMS |
whatsapp:// | WhatsApp Cloud API | |
webhook:// | Generic webhook | Posts 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.
Generic webhook
Section titled “Generic webhook”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.
Per-meeting-type routing
Section titled “Per-meeting-type routing”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.