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.
- Go to api.slack.com/apps, click Create New App and choose From scratch. Pick a name and the workspace.
- Open Incoming Webhooks in the sidebar and switch Activate Incoming Webhooks on.
- Click Add New Webhook to Workspace, choose a channel and allow it.
- 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
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.
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_textof up to 150 characters. - A section block takes
mrkdwntext 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.
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.
| Response | What it means | What to do |
|---|---|---|
ok | The message was posted | Nothing |
invalid_payload | Malformed JSON or badly escaped text | Fix the payload; don't retry it unchanged |
action_prohibited | An admin restricted posting this way | Ask a workspace admin; don't keep retrying |
channel_not_found | The channel does not exist or is invalid | Create a webhook for a valid channel |
channel_is_archived | The channel was archived | Unarchive it or use a webhook for an active channel |
user_not_found | The addressed user does not exist or is invalid | Recreate the webhook |
no_service | The webhook is disabled, removed or invalid | Add a new webhook in the app settings |
| HTTP 429 | Too many messages | Wait 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.
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 responseUse 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.
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.
pip install tqdm-liveSources
- Slack: Sending messages using incoming webhooks docs.slack.dev
- Slack: Rate limits docs.slack.dev
- Slack: Formatting message text docs.slack.dev
- Slack: Header block docs.slack.dev
- Slack: Section block docs.slack.dev
- Slack: chat.postMessage docs.slack.dev