Plain-English explainer

What is a Slack incoming webhook?

A one-way URL that posts a JSON body to a Slack channel. That's it. Below: how they differ from the Web API, what they can and can't do, the JSON shape Slack expects, the errors you'll hit, and when they're the right choice for form notifications.

The one-liner

A Slack incoming webhook is a write-only URL

You POST a JSON body to https://hooks.slack.com/services/T.../B.../<secret>. Slack posts the body, rendered as a message, to the channel that URL was bound to at creation. That's the whole interface — no auth header, no API token, no SDK required.

That simplicity is the reason it's the standard transport for form notifications, monitoring alerts, CI status messages, deploy notifications, and anything else that just needs to say something in a channel.

What they can do

The supported operations

  • Post a plain text message. Body: { "text": "hello" }. Slack's mrkdwn renders inside the text field.
  • Post a Block Kit message. Body: { "text": "fallback", "blocks": [...] }. Use the Block Kit Payload Generator to build the blocks array interactively.
  • Mention users, groups, and channels. <@U123> for a user, <!subteam^S123> for a group, <!here> / <!channel> for broadcast pings.
  • Use markdown formatting. *bold*, _italic_, `code`, line breaks. Renders inside any section block with type: “mrkdwn”.
  • Include action buttons. Block Kit's actions block. Each button has a label and a URL (or a Slack interaction payload for full Slack-app flows).
What they can't do

When you've outgrown incoming webhooks

  • Post to a different channel than the URL is bound to. The channel field exists for backwards compat but new-style Slack-app webhooks ignore it. One webhook = one channel.
  • Edit, delete, or reply in a thread. Use the Web API (chat.update, chat.delete, chat.postMessage with thread_ts) and a bot token.
  • Send ephemeral or scheduled messages. Web API only — chat.postEphemeral and chat.scheduleMessage.
  • Read anything from Slack. Webhooks are write-only. Reading channels, users, or message history needs the Web API and the right scopes.
  • Authenticate beyond the URL secret. The secret in the URL is the entire auth — no per-request HMAC, no signed payloads. Treat the URL like a password.

For form notifications you almost never need any of these. If you do, you've outgrown incoming webhooks — create a Slack app with bot scopes and use the Web API.

The JSON shape

What Slack expects in the body

The minimum valid body:

{
  "text": "hello"
}

A realistic form-notification body:

{
  "text": "New form response from Priya Mehta",
  "blocks": [
    { "type": "header", "text": { "type": "plain_text", "text": "New form response" } },
    { "type": "section", "text": { "type": "mrkdwn",
      "text": "*From:* Priya Mehta\n*Email:* priya@acme.com\n*Budget:* $50,000" } },
    { "type": "context", "elements": [
      { "type": "mrkdwn", "text": "Submitted just now · #leads" }
    ]}
  ]
}

Always include text as a fallback even when you use blocks — Slack uses it for push notifications and accessibility surfaces.

The errors

What Slack says when something's wrong

  • 200 / “ok” — accepted. The message will appear in the channel.
  • 400 / invalid_payload — your JSON is malformed or missing a required field. Most often: missing Content-Type: application/json header.
  • 400 / invalid_blocks— JSON parsed but Slack couldn't render the blocks. A block type Slack doesn't recognise, a field exceeding the cap (text limits, fields-per-section caps), or wrong nesting.
  • 403 / invalid_token— the URL's secret no longer matches. Regenerate.
  • 404 / no_service / no_team — the URL was deleted or never existed. Regenerate.
  • 429 — you're sending too fast. Respect the Retry-After header.
  • 5xx — Slack-side issue. Retry with exponential backoff.

The Slack Webhook Tester decodes these into one-line plain-English explanations so you don't have to remember the vocabulary.

From form to Slack

How form submissions trigger incoming webhooks

A Google Form doesn't natively call webhooks. The standard pipeline is:

  • onFormSubmit Apps Script trigger fires on every submission, hands your code an event object with the response.
  • Your code builds a JSON body (text + blocks) from the response fields.
  • UrlFetchApp.fetch POSTs the body to the Slack incoming webhook URL.
  • Slack renders the message in the bound channel.

Skip writing this code: the Google Forms to Slack Generator emits the entire Apps Script, paste-ready. The On Submit Trigger Generator covers the broader trigger-installation flow if you want to pick the output yourself.

FAQ

Frequently asked questions

What's the difference between an incoming webhook and the Slack Web API?

Incoming webhooks are one-way and dead simple: you POST JSON to a URL, Slack posts the message to a channel, done. The Web API (chat.postMessage and friends) needs a bearer token, supports more shapes (ephemeral messages, scheduled messages, edits), and can do two-way operations. For form notifications you almost always want incoming webhooks — less moving parts, no token rotation, no rate limits to chase.

What can an incoming webhook do?

Post a message to one specific channel — that's it. Text, Block Kit blocks, attachments (legacy), markdown, mentions, emoji. The destination is fixed at URL creation; the identity is fixed to the app the webhook belongs to.

What can an incoming webhook NOT do?

Several things. (1) Post to a different channel than the one bound at creation (new-style webhooks ignore the channel field). (2) Edit, delete, or thread-reply to existing messages — that's the Web API. (3) Send ephemeral or scheduled messages. (4) Read anything from Slack — they're write-only. If you need any of these, you've outgrown incoming webhooks; create a Slack app with the relevant bot scopes and use chat.postMessage.

What JSON shape does an incoming webhook expect?

At minimum a single 'text' string. Realistic payloads add a 'blocks' array (Slack's Block Kit format) for rich layouts. POST to the URL with Content-Type: application/json. The simplest valid body is { "text": "hello" }. The most common form-notification body is { "text": "<fallback>", "blocks": [...] }.

What HTTP status codes mean what?

200 with body 'ok' = success. 400 invalid_payload = your JSON is malformed or the wrong shape. 400 invalid_blocks = blocks JSON parsed but Slack couldn't render. 403 invalid_token = the webhook secret was revoked. 404 no_service / no_team = the URL was deleted. 429 = rate-limited (back off). 5xx = Slack having an issue; retry with backoff.

How fast does a message appear in Slack?

Typically under a second. The POST returns 200 once Slack has accepted the message; rendering in the channel is essentially immediate after that. If you see delays of tens of seconds, look at network-path latency from your sender (not Slack itself).

Can I rate-limit myself? How many messages per second can I send?

Slack publishes guidance of around 1 message per second per webhook for sustained traffic, with burst tolerance. Hit the limit and Slack returns 429 with a Retry-After header — respect it. For form-notification workloads (one message per submission), you almost never hit the limit; for monitoring/alerting workloads, you can.

When should I use an incoming webhook for form notifications?

When you have one Google Form and one Slack channel and you just want every submission to land. The Apps Script that POSTs to the webhook is under 50 lines. When you outgrow that — different responses to different channels, retries, a delivery log a non-engineer can read — the managed option (RouteForms) is built around incoming webhooks under the hood, so the contract stays the same but the operational layer is solved.

Send Google Form responses to Slack

Use the free generator to emit the Apps Script — or use RouteForms for routing rules, retries, and a delivery log on top. Free for 30 responses a month.