How to Create a Slack Webhook URL in 4 Steps
Create a Slack app at api.slack.com/apps, turn on Incoming Webhooks, click Add New Webhook to Workspace and pick a channel. Slack gives you a hooks.slack.com URL that posts to that channel. Private channels, DMs, several channels, the Workflow Builder alternative, and the errors you may see.
On this page
To create a Slack webhook, make a Slack app and turn on incoming webhooks. Go to api.slack.com/apps, click Create New App, choose From scratch and pick your workspace. Open Incoming Webhooks, switch Activate Incoming Webhooks on, click Add New Webhook, choose the channel and click Allow. Slack shows a URL like https://hooks.slack.com/services/T000/B000/XXXX. Anything you POST to it as JSON appears in that channel.
Each URL posts to one channel. You cannot change the channel, name or icon per message: in our legacy vs app webhook test the app webhook ignored all three fields and still answered ok. For a private channel you must be a member first.
We created a webhook from scratch in a test workspace on 4 October, posted eight test payloads to it, then deleted the app and kept posting. The screenshots and responses below are from that run.
Create the webhook URL
- Create an app. Open api.slack.com/apps, click Create New App, then From scratch. Name it after what will post, such as "Deploy bot", and pick the workspace.
- Turn on incoming webhooks. In the app settings, click Incoming Webhooks in the left menu and switch Activate Incoming Webhooks to On. If you create the app from a manifest that asks for the
incoming-webhookscope, the switch is already On. - Add a webhook. Click Add New Webhook at the bottom. Slack opens a permission page with a Channel for webhook picker. Choose the channel and click Allow. If your workspace requires admin approval for apps, the request goes to an admin first.
- Copy the URL from Webhook URLs for Your Workspace. The table also shows the channel and who added it, and the trash icon removes the URL.
Adding the webhook also posts a notice in that channel: "added an integration to this channel", followed by your app's name. Everyone in the channel sees it.
Test it from a terminal:
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"Hello from my webhook"}' \
https://hooks.slack.com/services/T000/B000/XXXX
Slack answers ok with HTTP 200 and the message appears in the channel, posted by your app. In our run each POST took 300 to 460 ms.
Treat the URL like a password. Slack's docs warn that it contains a secret and that Slack searches for leaked webhook URLs and revokes them. Keep it in an environment variable or a secrets store, not in a public repository.
Private channels, DMs and several channels
If you need to pick the channel per message, or edit and delete messages later, use a bot token and chat.postMessage instead. Webhooks cannot delete a message once it is posted.
App webhook or Workflow Builder webhook?
Slack has a second kind of webhook that needs no app. In Workflow Builder, start a workflow From a webhook. Slack gives you a URL starting with https://hooks.slack.com/triggers/, and the workflow's steps decide what to post and where.
| App incoming webhook | Workflow Builder webhook | |
|---|---|---|
| URL | hooks.slack.com/services/... | hooks.slack.com/triggers/... |
| Plans | All plans | Paid plans |
| Payload | Full message JSON, including Block Kit | Up to 20 flat variables, no nested JSON |
| Who can make it | Anyone allowed to install apps | Members, unless admins restrict webhook workflows |
| Rate limit | About 1 message per second | 1 request per second |
Pick Workflow Builder when a non-developer should own the message text. Pick an app webhook when your code builds the message. Ideas for workflows are in Slack Workflow Builder examples.
Never appear "away" on Slack again
Cloud-based. No downloads. Works 24/7 even when your laptop is off.
Send a formatted message
A webhook accepts the same JSON as other Slack messages: text for the notification and fallback, and blocks for the layout.
{
"text": "Deploy finished",
"blocks": [
{
"type": "section",
"text": { "type": "mrkdwn", "text": "*Deploy finished* for `web`\n<https://example.com/runs/42|Open the run>" }
}
]
}
Tested curl, Python and Node.js senders, with retries for 429, are in Slack webhook curl example. On Windows, the PowerShell version uses Invoke-RestMethod and needs no module. Links use <url|text> in webhook messages, not [text](url). The syntax for bold, lists, mentions and the markdown block is in Slack Block Kit markdown, and a ready GitHub setup is in how to send a Slack message from GitHub Actions. To put a new line in the text, send \n; more in how to add a new line in Slack.
To design the blocks visually before you send them, see how to use Slack Block Kit Builder.
What a webhook accepts: our test payloads
We sent each of these to the new webhook URL and read the result in the channel:
| Payload | Response | What showed up |
|---|---|---|
{"text":"..."} | 200 ok | The text, posted by the app |
text plus a blocks section with mrkdwn | 200 ok | Bold title, inline code and a working link |
Form-encoded payload={"text":"..."} instead of JSON | 200 ok | The text |
{"foo":"bar"}, no text | 400 no_text | Nothing |
{"text":""} | 400 no_text | Nothing |
Cut-off JSON ({"text": "broken) | 400 invalid_payload | Nothing |
[docs](https://example.com) in the text | 200 ok | The brackets stay as typed; only the bare URL becomes a link |
"channel":"#social" added | 200 ok | Posted in the webhook's own channel; the override was ignored |
Deleting the app does not stop the URL at once. We deleted the app through Slack's manifest API and posted again: the first request, about a second later, still returned ok and posted. Five seconds later the same URL returned 404 no_service, and so did every request after. The channel got a "removed an integration from this channel" notice.
Webhook errors
| Response | Meaning |
|---|---|
400 invalid_payload | The JSON is malformed, or text is not escaped |
400 no_text | The payload has no text |
403 action_prohibited | An admin restricted posting this way |
403 posting_to_general_channel_denied | Only some people may post in #general |
404 no_service | The webhook was disabled, removed or mistyped |
404 channel_not_found | The channel is gone or the app lost access |
410 channel_is_archived | The channel was archived |
We saw no_text, invalid_payload and no_service ourselves; the other rows come from Slack's docs. In a later test an archived channel's webhook returned 404 no_active_hooks, not 410, and stayed broken after we unarchived it; that test and every other error we reproduced are in why a Slack webhook is not working. Slack's docs say an invalid_payload should not be retried without a fix. For no_service, create a new webhook; the old URL will not come back.
FAQ
How do I get a Slack webhook URL? Create an app at api.slack.com/apps, activate Incoming Webhooks, click Add New Webhook and choose a channel.
Can one webhook post to several channels? No. Create one webhook per channel under the same app.
Can a Slack webhook send a DM? Yes. Pick a person, such as yourself, in the channel picker when you add the webhook.
Is a Slack webhook free? App incoming webhooks work on every plan. Workflow Builder webhooks need a paid plan.
Stop Jiggling Your Mouse.
Join hundreds of remote workers who never worry about their Slack status. Set it up once, stay green forever.
Related Articles
How to See All Your DMs in Slack, Including the Hidden Ones
The DMs tab lists your direct messages with people. App DMs, Slackbot and DMs with deleted accounts sit elsewhere. We compared the tab with the full list the Slack API returned.
Slack Daily Standup Without a Paid Bot: /remind, Workflow Builder and a Script
We set up a weekday 9:30 standup in a free Slack workspace three ways: a /remind reminder, Workflow Builder, and a short script that posts the questions and collects the thread replies.
Leave Multiple Slack Channels at Once: 4 Ways Timed, Plus a Script
Slack has no bulk leave button. We made 12 test channels and left them with the sidebar menu, the channel browser, /leave and a 30-line script, and timed each way.