- Setting up an incoming webhook in four steps
- Sending your first message
- Keep the webhook URL secret
- What an incoming webhook can't do
- Replying in a thread with a webhook
- Slack webhook errors and what they mean
- Generating webhook URLs for an app you distribute
- My verdict on Slack webhooks
- Frequently asked questions
Slack webhooks, or incoming webhooks, give you a unique URL that posts messages from another app into a Slack channel when you send it a JSON payload. You get one by creating a Slack app, switching on Incoming Webhooks and authorising it for the channel you want it to post to.
Here's the catch. Each webhook URL is specific to a single user and a single channel, and you can't override the channel, username or icon from your payload. Those always come from the Slack app's configuration, set by whoever installed it. So if you want alerts in three channels, you're setting up three webhooks.
Setting up an incoming webhook in four steps
- Create a Slack app: pick a name and choose the workspace it belongs to, then click Create. An app you already have works too. Slack suggests a sandbox channel, because you'll post plenty of test messages.
- In the app's settings, select Incoming Webhooks and toggle Activate Incoming Webhooks to on.
- Click Add New Webhook to Workspace, pick the channel the app will post to, then select Authorize. For a private channel, you have to be in it first.
- Copy your new URL from the Webhook URLs for Your Workspace section and send it a POST request with your message.
That Add New Webhook to Workspace button runs a shortcut version of the Slack app installation flow, so you don't need to write any code to get a URL. The address starts with hooks.slack.com/services/ and ends in three IDs. If you're building a GovSlack app for public sector customers, your API calls go to the slack-gov.com domain instead of slack.com.
Sending your first message
Make an HTTP POST request to the webhook URL with the header Content-type: application/json and a body like {"text": "Hello, World"}. Check the channel and you'll see the message, posted by your app. Slack's example uses cURL, but any HTTP request library works, and the requests are structured in exactly the same way.
Webhooks follow the same rules as Slack's other messaging APIs, so you can send one line of text or use text formatting, Block Kit layout blocks and interactive components. The only difference is that your JSON carries other fields alongside text. Slack's own advanced example turns a hotel review alert into a formatted message with a section, an image with alt text and an average rating field.
Keep the webhook URL secret
Slack is blunt about this: your webhook URL contains a secret. Don't share it online, and that includes public version control repositories. Slack actively searches out leaked secrets and revokes them, so a URL pushed to a public repo can stop working. Keep it out of shared code and screenshots, and treat it like a password.
What an incoming webhook can't do
| Limit | What Slack's docs say |
|---|---|
| Pick the channel per message | No. It's fixed to the channel chosen when the app was installed |
| Change the username or icon | No. Both inherit from the Slack app's configuration |
| Delete a message it posted | No. For a chat flow that needs deletion, call chat.postMessage |
| Attachments | A maximum of 100 per message |
| Return the message ts | No, so threaded replies need an extra lookup |
Replying in a thread with a webhook
You can make a webhook message appear as a reply in a thread by sending the parent message's ts value as the thread_ts field. The awkward part is that the webhook doesn't return ts when you post, so you have to fetch it another way. Slack lists three: enable the Events API and subscribe to a callback when your webhook posts, search for the message by timestamp with assistant.search.context, or find it in a set of messages with conversations.history.
Slack webhook errors and what they mean
Most of the time you'll get an HTTP 200 response with a plain text ok. When a post fails, incoming webhooks return more expressive errors than Slack's Web API, with status codes such as 400 Bad Request, 403 Forbidden and 404 Not Found, plus an error string.
| Error | Meaning |
|---|---|
| action_prohibited | An admin has restricted this way of posting. Don't retry for now |
| channel_is_archived | The channel's archived and won't accept new messages |
| invalid_payload | Malformed JSON or badly escaped text. Fix it before retrying |
| invalid_token | The token's expired, invalid or missing |
| no_active_hooks | The webhook is disabled |
| no_service | The webhook is disabled, removed or invalid |
| no_service_id | The service ID, the B part of the URL, is invalid or missing |
| no_team / team_disabled | The workspace is missing, invalid or no longer active |
| no_text | Your payload has no text field |
| posting_to_general_channel_denied | Posting to #general is restricted and the webhook's creator isn't allowed to post there |
| too_many_attachments | The message has more than 100 attachments |
| channel_not_found / user_not_found | The channel or user doesn't exist or is invalid |
The ones to watch are invalid_payload, channel_not_found and user_not_found: Slack says not to retry those without changing the request first.
Generating webhook URLs for an app you distribute
The settings page route is fine for your own workspace. If other people will install your app, it needs to create URLs on the fly during the standard OAuth install flow. Add the incoming-webhook permission to your scope list, and users see an extra permission on the Authorize screen where they pick the channel. After the OAuth code exchange, the JSON response includes an incoming_webhook object with four fields: channel, channel_id, configuration_url and url, which is the new webhook URL.
Want AI doing the heavy lifting in your marketing?
I build the systems that handle the boring 80 percent, so you get your week back. Done properly, with the human kept in.
Webhooks can also start a workflow from outside Slack. That's set up in Slack Workflow Builder, not in your app's settings.
My verdict on Slack webhooks
I'd use an incoming webhook whenever you want another tool to drop updates into one channel, because you can have it posting in minutes without building a full app. Give each channel its own webhook, test in a sandbox channel first, and guard the URL like a password. If you need to delete messages, switch channels on the fly or build richer chat flows, move to chat.postMessage. And if a ready-made integration already does the job, check the Slack apps directory before you build anything.
Webhooks are often the first bit of automation a team builds, and the time savings snowball from there. I run AI training and workshops for teams, and help businesses with AI enablement and workflow automation, so the busywork runs itself and people get time back. Most of that work is with SaaS marketing teams and small businesses. Here's how we can work together.
Frequently asked questions
What is a Slack incoming webhook?
A unique URL that lets an app post messages into Slack. You send it a JSON payload with the message text and some options, and the message appears in the channel the webhook was set up for.
Can one Slack webhook post to several channels?
No. Each webhook URL is specific to a single user and a single channel, and you can't override the channel, username or icon in the payload.
Can I delete a message sent with a Slack webhook?
No. Incoming webhooks don't let you delete a message after it's been posted. If you need that, call chat.postMessage instead.
What does the no_text error mean in Slack?
The text attribute was missing from your JSON payload. Add a text field and send the request again.
Can I add a Slack webhook to a private channel?
Yes, but you must be in that private channel first. Then pick it when you authorise the webhook.

