A Webhook URL Is a Password
A Slack incoming webhook has no signature, no key and no account behind it. Whoever holds the URL can post to that channel, forever, as you — which makes it a credential wearing a link's clothing, and it usually gets treated like a link.
There is a category of secret that does not look like one. An API key looks like a secret: it is a meaningless string in a field marked key, and people treat it accordingly. A webhook URL looks like a link. It starts with https, it has a domain you recognise, and it goes in a field that often is not even a password field. It gets pasted into tickets, screenshots, shared docs and chat messages by people who would never do that with a key, and it does exactly the same job.
What the URL actually authorises
A Slack incoming webhook is a long unguessable address bound to one channel in one workspace. Post JSON to it and a message appears there. That is the whole protocol.
There is no signature on the request. No shared secret to compute a digest with. No account, no token, no expiry, and no way for Slack to tell one sender from another, because there is nothing in the request that identifies a sender at all. Possession is the entire authorisation.
Which is not an oversight — it is a reasonable design for the thing it does. But it has consequences that follow directly, and they are worth writing down rather than deriving under pressure.
| If the URL leaks | What the holder can do | What they cannot |
|---|---|---|
| Anyone with it can post | Put any message in that channel, indefinitely | Read the channel, or anything else in the workspace |
| Messages appear as your integration | Look exactly like your genuine alerts | Reach a different channel |
| There is no rate limit you set | Flood the channel until it is muted | Delete or edit existing messages |
| Nothing expires | Keep doing it after they have left the company | Do anything once the webhook is revoked |
The realistic risk is not somebody reading your data. It is somebody posting a convincing approval request into a channel your team trusts.
That last one is the part worth taking seriously, and it is a social risk rather than a technical one. A channel that has received genuine approval alerts for eight months is a channel where a ninth one is believed. Nothing about a forged message would look wrong, because there is nothing in a real one that a forgery lacks.
How it is handled here
Given that, the URL is treated as what it is rather than as what it looks like.
It is stored encrypted against your workspace, on the row for that connector. It is never written into a log line — when a send fails, what gets logged is the provider's error message, and the address is not part of the context. It is never included in an error shown to a user, and it is not echoed back into the page in full once saved.
Configuring it is a permissioned action rather than something any signed-in member can do, which matters in both directions: it stops a webhook being replaced by somebody who should not be redirecting your alerts, and it keeps the number of people who have ever seen the value small.
None of that is exotic. It is the ordinary handling of a credential, applied to something that does not present as one — which is the entire trick, because the failure mode here is not a weak control, it is a control nobody thought to apply.
Four connectors with this shape, and two without
Of the six chat destinations, four are webhook-shaped and identical in this respect: paste a URL, and the URL is the credential. Slack, Teams, Discord and Google Chat all work this way, and each is bound to exactly one destination — a channel, a space — chosen when the webhook was created.
That binding is a genuine security property and it is easy to undervalue. A leaked webhook reaches one channel. It cannot be pointed at a different one, cannot be used to enumerate anything, and cannot read a single message. Compared with a token that grants access to a workspace, the blast radius is small and fixed.
The two exceptions in the family carry real credentials instead, and the difference shows up in how they have to be handled.
-
Telegram carries a bot token in the address
The token is part of the request path rather than a header. It is a credential in a position where things are conventionally logged, which is a reason to treat proxy and access logging as part of the threat model rather than as plumbing.
-
A Telegram bot token is broader than a webhook
It is the bot, not a channel. Anyone holding it can act as the bot anywhere the bot is a member, which is a wider grant than any of the four webhook connectors offer.
-
WhatsApp uses a bearer token
Sent as a header against Meta's API, in the conventional place, alongside an identifier for the number that sends. A leak reaches whatever that token was scoped to at Meta.
-
Both refuse before they call
An incomplete configuration returns a reason rather than attempting a request with a missing credential, so a half-set-up connector reads as unconfigured rather than as failing.
The one operational habit worth having
Because a webhook cannot be scoped, cannot be audited and never expires, the only real control you have is replacement. It is quick — revoke the old one in the chat tool, create a new one, paste it in — and it is the correct response to a long list of ordinary events that nobody thinks of as security events.
Somebody who set it up leaves. The URL appears in a screenshot in a ticket. A contractor's access is wound down. The value was pasted into a shared document during setup and the document still exists. In every one of those, the webhook is more widely known than it was, and nothing anywhere will tell you so.
Rotating is cheap and there is no state to migrate — the new URL simply starts being used. That asymmetry between the cost of rotating and the cost of not knowing is the argument for doing it on a schedule rather than on suspicion.
What is in place, layer by layer
How chat credentials are handled
Encrypted at rest
Webhook URLs and bot tokens alike are stored through an encrypted cast on the row for a single workspace, never in configuration or environment.
Kept out of logs
A failed send logs the provider's error message. The destination address is not part of the logged context, so a credential does not end up in a file with a wider audience.
Kept out of error messages
What a user sees when a send fails is the reason, not the address, so a screenshot of a failure is not a leak.
A permissioned setting
Configuring a chat destination is its own right rather than something every signed-in member can change, which limits both who can redirect alerts and who has seen the value.
Refusal before the request
An incomplete configuration returns a reason instead of sending a call with a missing credential, so the failure names the cause.
One destination per credential
Each of the four webhook connectors is bound to a single channel or space by the provider, so the reach of a leaked value is fixed and small.
Replaceable in seconds
Rotation is paste-and-save with nothing to migrate, which is what makes it reasonable to do on a schedule rather than only after a scare.
The provider's rejection surfaced
A revoked webhook and a malformed payload produce different provider responses, and both are shown as given rather than collapsed into one message.
Signed inbound events are a different connector and a different model — there, a shared secret and a computed digest decide whether a request is genuine. The asymmetry is the provider's, not ours.
Three positions held on purpose
- A webhook URL is handled as a credential from the moment it is pasted. It does not look like one, which is precisely the argument for encrypting it, keeping it out of logs and gating who can set it.
- The destination is never included in a log line or an error message. A failure that a user can screenshot, or an operator can grep, should not be a way to obtain the thing that authorises posting.
- Rotation is presented as routine rather than as an incident response. A credential that cannot expire, cannot be scoped and cannot be audited is one whose only real control is replacement, and treating that as ordinary maintenance is the honest framing.
Five questions about how a product holds your webhook
Is it encrypted at rest?
A good answer sounds like
Yes, per workspace.
What ours actually is
Stored through an encrypted cast on the row for one workspace. Not in configuration, not in environment.
Does it appear in your logs?
A good answer sounds like
No.
What ours actually is
A failed send logs the provider's message. The address is not in the context.
Who can change it?
A good answer sounds like
A named permission.
What ours actually is
Configuring a chat destination is its own right, not open to every member.
What does a leak actually get someone?
A good answer sounds like
A specific, bounded answer.
What ours actually is
The ability to post into one channel. No read access, no other channel, nothing else in the workspace.
How do I rotate it?
A good answer sounds like
Quickly, with no migration.
What ours actually is
Revoke at the provider, create a new one, paste and save. There is no state that carries over.
Our take
Security reviews are good at asking about the things that look like secrets and poor at asking about the things that are. A webhook URL passes through a procurement questionnaire without a comment because there is no question shaped like it — it is not a key, not a password, not an OAuth grant, and the field it lives in is usually labelled URL. Meanwhile it is a permanent, unrevocable-by-you, unauditable permission to put words in front of your team in a place they trust. The good news is that the handling required is entirely ordinary. The only hard part is noticing that it applies.
Treat the link like the key it is
Encrypted, out of the logs, behind a permission, and rotated when the people around it change. None of it is difficult; all of it depends on calling the thing by its real name.
Talk through connector securityFrequently asked questions
Can someone with our webhook URL read our Slack messages?
No. An incoming webhook is write-only and bound to one channel. It grants the ability to post there and nothing else — no reading, no other channels, no workspace access. That is the reason a leak is a nuisance rather than a breach, and it is worth knowing precisely so the response is proportionate.
How would we know if a webhook had leaked?
Realistically, by seeing a message in the channel that your systems did not send. There is no audit trail on the provider side attributing posts to a sender, because there is no sender identity in the protocol. This is the strongest argument for scheduled rotation: you cannot detect the condition, so you reduce how long it can persist.
Should we use a private channel for alerts?
It helps for a different reason — it limits who reads the operational detail in the messages, which is often the more sensitive half. It does nothing about the webhook itself, since the URL works regardless of the channel's privacy.
Is Telegram less safe because the token is in the URL?
It is a broader credential and it sits in a position where things get logged by intermediaries, so it deserves more care than a channel-bound webhook. Whether that matters depends on your infrastructure. The honest summary is that the four webhook connectors have the smallest blast radius of the six, and that is a legitimate reason to prefer one of them.
What happens to queued messages when we rotate the webhook?
Anything already dispatched against the old URL will be refused by the provider and logged with its reason. Nothing is lost from your workspace — chat is a notification channel, and the record it was telling you about is unaffected.