Slack webhooks

Post to Slack from Python with an incoming webhook

A Slack incoming webhook is a URL tied to one channel: POST {"text": "..."} and Slack answers ok. You get it from a Slack app with Incoming Webhooks switched on, no bot code needed. Plan for about 1 message per second per channel, up to 50 blocks per message and text that is cut off after 40,000 characters.

4 min readNotifications and alerts

At a glance

  • One webhook posts to one channel; an app's webhook can't override the channel, username or icon.
  • Success is HTTP 200 with the body ok; failures return an HTTP error such as 400, 403, 404 or 410 with an error string in the body.
  • About 1 message per second, short bursts allowed; a 429 carries Retry-After.
  • Text over 40,000 characters is truncated, and a message holds up to 50 blocks.

Create an incoming webhook

An incoming webhook is the fastest way to get a Python script, CI job or cron task posting into Slack without building a bot.

  1. Go to api.slack.com/apps, click Create New App and choose From scratch. Pick a name and the workspace.
  2. Open Incoming Webhooks in the sidebar and switch Activate Incoming Webhooks on.
  3. Click Add New Webhook to Workspace, choose a channel and allow it.
  4. Copy the URL, which looks like https://hooks.slack.com/services/T.../B.../XXXX.

Each webhook is tied to the channel you picked. An app's incoming webhook can't override that channel, and it can't change the username or icon either.

Treat the URL as a secret. Anyone who has it can post to your channel.

Send a message with requests

POST a JSON body of {"text": "..."} to the webhook URL. When it works, Slack returns HTTP 200 and the plain text body ok.

The text field supports Slack's mrkdwn:

  • *bold* for bold
  • _italic_ for italics
  • ` code ` for inline code
  • <https://example.com|label> for a named link
Python
import os

import requests

WEBHOOK_URL = os.environ["SLACK_WEBHOOK_URL"]

response = requests.post(
    WEBHOOK_URL,
    json={"text": "*Nightly ETL* finished in 42m. <https://example.com/runs/812|Open the report>"},
    timeout=10,
)
if response.status_code != 200 or response.text != "ok":
    raise RuntimeError(f"Slack said {response.status_code}: {response.text}")

To test the webhook from a terminal first, send the same payload with curl.

Bash
curl -X POST -H "Content-Type: application/json" \
  --data '{"text": "Deploy finished"}' \
  "$SLACK_WEBHOOK_URL"

Format messages with Block Kit

When one line of text is not enough, Block Kit gives the message structure. A message can hold up to 50 blocks, and two of them cover most alerts:

  • A header block takes plain_text of up to 150 characters.
  • A section block takes mrkdwn text of 1 to 3,000 characters.

Keep a top-level text field even when you send blocks. Slack uses it as the fallback for notifications.

Python
payload = {
    "text": "Nightly ETL finished in 42m",
    "blocks": [
        {"type": "header", "text": {"type": "plain_text", "text": "Nightly ETL finished"}},
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": "*Rows:* 1,284,019\n*Duration:* 42m 10s\n<https://example.com/runs/812|Open the report>",
            },
        },
    ],
}
requests.post(WEBHOOK_URL, json=payload, timeout=10).raise_for_status()

Errors and rate limits

Incoming webhooks return real HTTP status codes, such as 400 Bad Request, 403 Forbidden, 404 Not Found and 410 Gone for an archived channel, with an error string in the body instead of ok.

ResponseWhat it meansWhat to do
okThe message was postedNothing
invalid_payloadMalformed JSON or badly escaped textFix the payload; don't retry it unchanged
action_prohibitedAn admin restricted posting this wayAsk a workspace admin; don't keep retrying
channel_not_foundThe channel does not exist or is invalidCreate a webhook for a valid channel
channel_is_archivedThe channel was archivedUnarchive it or use a webhook for an active channel
user_not_foundThe addressed user does not exist or is invalidRecreate the webhook
no_serviceThe webhook is disabled, removed or invalidAdd a new webhook in the app settings
HTTP 429Too many messagesWait the Retry-After seconds, then retry

Legacy attachments have one more: more than 100 of them returns too_many_attachments.

The limit for incoming webhooks is 1 message per second, with short bursts above that allowed. Slack's general rule is no more than one message per second per channel, however you post. Over the limit, Slack returns HTTP 429 Too Many Requests with a Retry-After header in seconds. Text longer than 40,000 characters is truncated.

Python
import time

import requests


def post(url, payload):
    response = requests.post(url, json=payload, timeout=10)
    if response.status_code == 429:
        time.sleep(int(response.headers.get("Retry-After", 1)))
        response = requests.post(url, json=payload, timeout=10)
    if response.status_code != 200:
        raise RuntimeError(f"Slack webhook failed: {response.status_code} {response.text}")
    return response

Use it for job alerts

The usual pattern wraps the job in try/except: post when it finishes, post the error when it raises, then re-raise so the exit code stays non-zero.

Python
try:
    run_etl()
except Exception as exc:
    post(WEBHOOK_URL, {"text": f"ETL failed: `{exc!r}`"})
    raise
post(WEBHOOK_URL, {"text": "ETL finished"})

That covers clean finishes and exceptions, but not a job that hangs. The Python script notification guide covers SIGTERM and the other exits.

FAQ

Can a Slack incoming webhook post to a different channel?

No. An app's incoming webhook is locked to the channel chosen when it was added, and the payload can't override the channel, username or icon. To post to a second channel, add another webhook for that channel.

How many messages per second can I send to a Slack webhook?

About 1 per second, with short bursts allowed. Over the limit, Slack responds with HTTP 429 Too Many Requests and a Retry-After header that gives the seconds to wait.

Why does Slack return invalid_payload?

The JSON is malformed or the message text is not properly escaped. Slack's docs say not to retry the request until you fix it.

What is the maximum length of a Slack webhook message?

Text over 40,000 characters is truncated. With Block Kit you can send up to 50 blocks, with up to 150 characters in a header block and up to 3,000 characters in each section block's text.

Should I use an incoming webhook or chat.postMessage?

Use an incoming webhook when a script, cron task or CI job only posts into one known channel. Use the Web API's chat.postMessage with a bot token when you need to post to any channel the bot is in.

Check on long runs from your phone

Change one import and every tqdm bar prints a link you can open on any device. Get a Discord or Slack message when the run finishes, fails or stalls.

Get a live linkpip install tqdm-live

Sources