Rich messages

A channel post webhook renders its event as a message in the target channel. When the payload is a blocks message — an object with a blocks array — it renders as a rich, structured message (headings, sections, field grids, dividers, context lines). Anything without a blocks array falls back to a formatted payload preview.

The sender does the formatting; each block maps to the chat renderer. The format is compatible with common block-based message builders, so existing block JSON generally works unchanged.

Payload shape

Send a JSON body with a blocks array (up to 50 blocks) and an optional text fallback:

{
  "text": "Release deploy failure",
  "blocks": [
    {
      "type": "header",
      "text": { "type": "plain_text", "text": "Release deploy failure" }
    },
    { "type": "divider" }
  ]
}

text becomes the message's plain-text body (used for search and notifications). Sign and send it like any other webhook request — see Setup.

Block types

Every supported block, with a payload example.

A bold heading. text is a plain_text object (max 150 characters).

{
  "type": "header",
  "text": { "type": "plain_text", "text": "New time-off request" }
}

section (text)

A block of markdown text. text is a text object — use mrkdwn for the compact syntax (see Text formatting).

{
  "type": "section",
  "text": {
    "type": "mrkdwn",
    "text": "The *production* deploy failed — <https://ci.example.com/runs/123|view logs>."
  }
}

section (fields)

A two-column label/value grid (up to 10 fields). Each field is a text object; the common *Label*\nvalue idiom is split into a label and value.

{
  "type": "section",
  "fields": [
    { "type": "mrkdwn", "text": "*Environment*\nproduction" },
    { "type": "mrkdwn", "text": "*Branch*\nmain" }
  ]
}

divider

A horizontal rule.

{ "type": "divider" }

context

Small, muted supporting text — timestamps, captions, provenance. elements is a list of text objects and image elements (up to 10); images render as links.

{
  "type": "context",
  "elements": [
    {
      "type": "image",
      "image_url": "https://example.com/ci.png",
      "alt_text": "CI"
    },
    { "type": "mrkdwn", "text": "CI/CD • just now" }
  ]
}

image

An image reference. Rendered as an alt-text link (see Images).

{
  "type": "image",
  "image_url": "https://cdn.example.com/charts/wau.png",
  "alt_text": "Weekly active users",
  "title": { "type": "plain_text", "text": "WAU" }
}

markdown

Standard/CommonMark markdown, passed through — bold **x**, tables, fenced code, task lists. Use this for AI/LLM-generated markdown.

{
  "type": "markdown",
  "text": "**Highlights**\n\n- Rich webhook messages\n- [Changelog](https://example.com/changelog)"
}

actions

A row of buttons. Only buttons with a url render (as links); interactive buttons (with an action_id and no url) are dropped.

{
  "type": "actions",
  "elements": [
    {
      "type": "button",
      "text": { "type": "plain_text", "text": "View logs" },
      "url": "https://ci.example.com/runs/123"
    }
  ]
}

rich_text

Structured formatted text — sections, lists, quotes, and preformatted code. Flattened to markdown on render.

{
  "type": "rich_text",
  "elements": [
    {
      "type": "rich_text_section",
      "elements": [
        {
          "type": "text",
          "text": "Release checklist:",
          "style": { "bold": true }
        }
      ]
    },
    {
      "type": "rich_text_list",
      "style": "bullet",
      "elements": [
        {
          "type": "rich_text_section",
          "elements": [{ "type": "text", "text": "Cut tag" }]
        },
        {
          "type": "rich_text_section",
          "elements": [{ "type": "text", "text": "Run migrations" }]
        }
      ]
    }
  ]
}

Text formatting

There are two text conventions:

  • mrkdwn text objects (in section, context, …) use a compact syntax: *bold*, _italic_, ~strike~, `code`, <https://url|label> links, and <@U…> / <#C…> mentions. These are translated to standard markdown on render.
  • The markdown block uses standard/CommonMark markdown (**bold**, tables, fenced code, task lists) and is passed through as-is.

Mentions render as inert text — a webhook cannot ping members or @channel.

Images

Image references are arbitrary remote URLs. To avoid leaking channel viewers' IPs to the sender's host (and tracking pixels), remote images from webhooks are not auto-loaded. image blocks, section image accessories, and context images render as their alt text linked to the image URL instead.

Not a blocks payload?

Any payload without a blocks array renders as the event type plus a formatted payload preview (pretty-printed JSON, or raw text). Rich messages are opt-in by sending a blocks array.

Security

Block content is untrusted input and is sanitized before rendering:

  • Only http(s) links are kept; other URL schemes are dropped.
  • Mentions and control characters are neutralized.
  • Text lengths, field counts, and block counts are bounded.
  • Interactive elements are dropped; remote images are never auto-loaded.
  • The message author is always the webhook endpoint — a block can't spoof a different author.

Connecting tools that can't sign

Tools like PostHog (HTTP Webhook destination), Datadog (Webhooks integration), and Grafana (webhook contact point) can't HMAC-sign the request, but they can template a custom JSON body and send a static header. To connect them:

  1. Create a channel-post webhook and set its verifier to Shared secret (header token) (Configuration).
  2. In the tool, set the request URL to the endpoint URL, add the header Authorization: Bearer <endpoint-secret>, and set the body to a blocks template. For example (PostHog interpolation shown):
{
  "text": "New signup: {person.properties.email}",
  "blocks": [
    {
      "type": "header",
      "text": { "type": "plain_text", "text": "👋 New signup" }
    },
    {
      "type": "section",
      "fields": [
        { "type": "mrkdwn", "text": "*Email*\n{person.properties.email}" },
        { "type": "mrkdwn", "text": "*Event*\n{event}" }
      ]
    },
    { "type": "context", "elements": [{ "type": "mrkdwn", "text": "PostHog" }] }
  ]
}

Swap the interpolation tokens for the tool's own (Datadog $…, Grafana {{ .Vars.… }}). Because the body is a blocks array, it renders as a rich message — no per-tool support needed on our side.