Skip to content

Webhook payload format for chat services (Discord, Slack, Google Chat) #565

Description

@nickmeinhold

✨ Feature Request

Is your feature request related to a problem? Please describe.

Pasting a chat service's incoming-webhook URL into Kan doesn't deliver anything, and it's come up a few times now across four different services:

They look like four separate integration bugs, but I think they're one thing. Kan posts its own envelope ({event, timestamp, data}), and those services validate the body against their own schema and reject anything that doesn't match. Discord's docs are explicit about it:

Note that when sending a message, you must provide a value for at least one of content, embeds, components, file, or poll.

Kan's payload has none of those, so Discord returns 400 before the message is ever considered. Slack, Google Chat and Rocket.Chat each want their own shape for the same reason.

Describe the solution you'd like

A per-webhook payload format, defaulting to the current envelope, so the body is rendered into whatever the target accepts.

Existing webhooks keep sending byte-identical payloads, so anything already built against the current format is unaffected.

Describe alternatives you've considered

  • Detect the service from the URL. No config needed, but Discord and Slack both allow proxied and custom URLs, so detection is guessably wrong, and it fails silently when it guesses wrong.
  • A user-supplied body template. More flexible, but a much larger surface with an injection story attached, and four fixed adapters cover every service reported so far.
  • Document it and leave it. Reasonable if you'd rather Kan's webhooks stay a single clean contract and chat delivery is someone else's adapter. The one thing that gives me pause: card titles are user-editable, and Slack treats &, < and > as control characters, so an adapter that forwards a title unescaped lets anyone who can edit a card fire a channel-wide mention or a disguised link. That's easy to miss when writing your own, and someone already has written their own.

Additional context

Rocket.Chat and Mattermost accept Slack-shaped payloads, so the body itself carries over. Their mention syntax does not: Mattermost uses bare @channel / @ALL / @here, where Slack uses <!channel>. That matters because card titles are user-editable, so escaping that makes a title safe for Slack does nothing for Mattermost. I'd lean toward treating them as separate formats rather than folding all three under one, but that's a design call rather than a fixed constraint.

Separately, #456 (card.moved using two different ID systems) and #454 (http URLs rejected without surfacing why) look like genuinely separate bugs rather than part of this.

Implementation ideas (optional)

A format column on workspace_webhooks (pg enum, default generic), and a render step at delivery in sendWebhookToUrl. The test-send path uses the same format, since pressing Test is where people hit this first.

I've prototyped it to check the approach is tractable. Two things that surprised me and would bite any implementation: a long card title overflows Discord's 256-char embed title, which is its own 400, and truncating it needs to walk code points rather than UTF-16 units or an emoji title produces a lone surrogate and a 400 as well.

Happy to bin it and go a different way if the shape is wrong, or if this isn't a direction you want for Kan.

One thing worth deciding early: the HMAC has to sign the bytes actually posted, or a verifier can't check them. For the chat formats that means the signature no longer covers a Kan timestamp, since the rendered body doesn't carry one, so replay-freshness stops being inside the MAC. The chat services ignore the signature header anyway, so it may not matter, but it's a semantic change to what X-Webhook-Signature attests to and I'd rather flag it than quietly land it.

Would you like to work on this feature?

  • Yes, I'd like to help implement this feature
  • No, I'm just suggesting the feature

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions