Why two of them cannot be a personal preference
An incoming webhook posts to one fixed channel in one workspace. It is a single destination for the whole team — and posting the same message there once per team member is the same message five times. So Slack and Discord are connected by an admin and used by rules; in-app and email are per person, per account.A webhook URL never comes back
The API returns a masked hint —…/T01ABCDEF/B02GHIJKL/xyzS•••••••• — enough to recognise which webhook it is, useless to anyone who reads the response. The same rule API keys follow, for the same reason: an endpoint that hands a credential back turns every read of that page into a way to take it.
The host is checked
The stored URL is fetched server-side, so accepting an arbitrary one would make the API a request forwarder pointed wherever the caller likes — at an internal address, or at a cloud metadata endpoint. Pinning the host to the provider’s own is the cheap half of not building an SSRF. It also catches the ordinary mistake: pasting the app page instead of the webhook, which would otherwise be a connection that silently never posts.A rule may name a channel nobody has connected
The evaluator finds no connection and delivers nothing there. It does not error, and it does not invent a destination. Hiding the option until the account wires it up would mean setting the alert up twice — once before connecting and once after.Only in-app actually sends
Email, Slack and Discord record deliveries asqueued, and nothing drains them yet.
Every channel carries a delivers flag through the API, and the page shows “Not sending yet” where it is false. A toggle that silently does nothing is the worst control on any page, and the honest version costs one boolean.
The inbox is the delivery record
There is no separate notifications table. A notification is a delivery — two records for it could disagree about whether you were told. Deliveries are fanned out when a rule fires, not resolved when you open the inbox: the audience is who was on the team at that moment, and working it out on read would quietly rewrite history every time somebody joined or left.GET /v1/account/inbox reads it; POST /v1/account/inbox/read marks items read, by id or { "all": true }.
Reading the catalogue
GET /v1/account/channels returns the catalogue as well as the stored preferences — every destination’s label, its sentence, and whether it delivers yet. So the MCP server can answer “what am I being told through” without a second copy that drifts.
PUT /v1/account/channels merges: only the switches you send are changed.