Slack incoming webhook URL

A Slack incoming webhook URL lets an app post JSON into one Slack channel. Here is how to get one, what goes in the text field, and its limits.

Updated October 1, 2026

A Slack incoming webhook URL is a secret address that posts a message into one Slack channel. Your code sends an HTTP POST with a JSON body to the URL, and the message appears in the channel as your Slack app. Incoming webhooks only send messages into Slack. A Stripe webhook is a different thing. It carries events out of Stripe to your server.

How to get a webhook URL in Slack

Incoming webhooks belong to a Slack app, so you create the app first. The steps in Slack's docs are:

  1. Go to api.slack.com/apps and create a new app in your workspace.
  2. Open Incoming Webhooks in the app settings and switch on Activate Incoming Webhooks.
  3. Click Add New Webhook to Workspace.
  4. Pick the channel the webhook will post to, then approve the install.
  5. Copy the new URL from the Incoming Webhooks page.

The URL looks like this example:

  • https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX

The last part of the URL is a secret token. You don't send a separate token or an Authorization header. The URL is the credential.

Apps installed by other workspaces through OAuth request the incoming-webhook scope instead. The OAuth response then includes an incoming_webhook object with the url, the channel, the channel_id and a configuration_url.

Sending a Slack webhook to a channel

Each URL posts to the channel picked during install. Slack's docs say each webhook is specific to a single user and a single channel. To post to #payments and #disputes, you need two webhook URLs. The payload can't move a message to another channel, and it can't change the app's name or icon either.

A minimal request is a JSON body with the header Content-type: application/json:

  • curl -X POST -H 'Content-type: application/json' --data '{"text":"Payment failed for $96.00"}' YOUR_WEBHOOK_URL

Slack answers ok when the message posts.

The JSON payload text field

The text field holds the message. It is the smallest valid payload, and Slack returns no_text if you send nothing to post. It accepts Slack's mrkdwn formatting, such as *bold*, _italic_, and links written as <https://example.com|label>.

You can also send a blocks array for a Block Kit layout. Keep text in the payload anyway. When a message has blocks, Slack uses text as the fallback for notifications and for clients that can't show blocks.

Mentions go inside text in Slack's own syntax. Use <@U012AB3CD> for a person, and <!here> or <!channel> for a channel-wide mention. Typing "@channel" as plain text notifies nobody.

Limits of Slack incoming webhooks

  • One channel per URL. Every extra channel needs its own webhook.
  • No edits or deletes. Slack states that incoming webhooks can't delete a message after it posts. To manage messages later, use the Web API method chat.postMessage.
  • Rate limit. Slack allows one message per second, with short bursts above that. Past the limit, Slack returns HTTP 429 Too Many Requests with a Retry-After header that gives the seconds to wait.
  • Mentions reach channel members only. Slack notifies a mentioned person only if they already belong to the channel.

When Slack webhook messages stop arriving

A failed post returns an HTTP error status and a short error string. Each one points to a different fix:

  • invalid_payload or no_text means the JSON is malformed or empty. Engineering fixes the code that builds it.
  • channel_is_archived means someone archived the channel. Create a webhook for an active channel.
  • action_prohibited means a workspace admin restricted posting. A Slack admin decides.
  • no_service or invalid_token means the webhook is disabled or the URL is wrong. Generate a new one in the app settings.

Treat the URL like a password. Anyone who has it can post into your channel, and Slack scans for leaked secrets and revokes them. Keep it in a secrets manager, never in a public repo or browser code. Log every non-200 response, because a quiet channel can mean a broken webhook.

Why ChargeBell uses a Slack app instead

A homemade Stripe alert chains two webhooks. Stripe sends an event to your webhook endpoint. Your code checks the webhook signature, converts the amount from cents, looks up the customer, and posts the text to a Slack incoming webhook URL. You then own both halves. On the Stripe side that means signatures, retries, duplicate events and ordering. On the Slack side it means one URL per channel, rate limits and error handling. The usual failure is silent. Someone archives the alerts channel, Slack starts returning channel_is_archived, and failed payments stop reaching anyone.

ChargeBell installs as a Slack app through OAuth and posts with chat.postMessage, so one install can post to many channels. It asks for permission to list channels, post alerts as the ChargeBell app, join public channels and run its /chargebell slash command. It doesn't read message history. Each alert type can go to its own channel, for example dispute alerts to #founders and failed payment alerts to #billing. You can send a test alert for any alert type, so you see a message land before real money moves. The Stripe Slack integration page covers the full setup.

Get alerted in Slack

ChargeBell posts Stripe events to the Slack channel you pick, written in plain English.

Related terms