Back to Blog
Guide

Slack Webhook curl Example, Plus Python and Node.js

Send a Slack message with one curl command: POST a JSON body with a text field to your incoming webhook URL, and Slack answers ok. Tested examples for curl, jq, Block Kit, Python with retries and Node.js, how to escape quotes and new lines, and what 400 invalid_payload and 429 mean in code.

Slack Green Team
September 28, 2026
September 28, 2026
5 min read
Share:
slack
webhooks
api

To send a Slack message with curl, POST a JSON body with a text field to your incoming webhook URL:

curl -X POST "$SLACK_WEBHOOK_URL" \
  -H "Content-Type: application/json" \
  -d '{"text": "Deploy finished: *web* is live"}'

Slack replies with the two letters ok and posts the message in the webhook's channel. If the JSON is broken, you get HTTP 400 with invalid_payload. Keep the URL in an environment variable such as SLACK_WEBHOOK_URL: anyone who has it can post to your channel. If you do not have a URL yet, how to create a Slack webhook covers the four steps.

We ran every example on this page on 28 September against a local stand-in server that checks the JSON and answers the way Slack's docs describe (ok, or invalid_payload for bad JSON), and logged the exact bodies each one sent. The limits and error codes come from Slack's developer docs on incoming webhooks and rate limits.

We build Slack Green, which keeps a Slack dot green from our servers during the hours you set.

Slack webhook curl example: POST JSON with a text field, Slack answers ok

How a Slack webhook call works

What happens when you call a Slack webhook: your script, HTTP POST with JSON, hooks.slack.com, the channel

  • • One URL, one channel. The webhook posts to the channel picked when the app was installed. Slack's docs say you cannot override the channel, username or icon per message; they come from the app.
  • • The body is JSON. Send Content-Type: application/json and at least text, or blocks plus text as the notification fallback.
  • • The answer is plain text. Success is ok, not a JSON object, and Slack does not return the message ts, so you cannot thread replies to it without the Events API.
  • • About one message per second. Slack's rate limit page lists incoming webhooks at 1 per second, with short bursts allowed. Over that you get HTTP 429 with a Retry-After header.
  • curl examples: text, variables and Block Kit

    Send a message that contains quotes or new lines. Do not paste shell variables into a JSON string by hand. Let jq build the JSON, so quotes and line breaks are escaped:

    MESSAGE='Build "42" failed on main
    see the log'
    jq -n --arg text "$MESSAGE" '{text: $text}' |
      curl -sS -X POST "$SLACK_WEBHOOK_URL" -H "Content-Type: application/json" --data-binary @-

    The body that arrived was {"text": "Build \"42\" failed on main\nsee the log"}: the quotes became \" and the line break \n, which Slack shows as a new line.

    Send a formatted message with Block Kit. Put the JSON in a heredoc and send it with --data-binary, which keeps the body exactly as written:

    curl -X POST "$SLACK_WEBHOOK_URL" \
      -H "Content-Type: application/json" \
      --data-binary @- <<'EOF'
    {
      "text": "Deploy finished for web",
      "blocks": [
        {"type": "section", "text": {"type": "mrkdwn", "text": "*Deploy finished* for `web`\n<https://example.com/runs/42|Open the run>"}}
      ]
    }
    EOF

    Links use <url|text> in webhook messages. The [text](url) form works only in the Slack message composer's markup mode, not in mrkdwn sent by apps and webhooks. To turn normal Markdown into Slack's format first, paste it into our free Markdown to Slack converter, and preview the result in the Slack message formatter. All the syntax is in Slack Block Kit markdown.

    Slack webhook in Python

Never appear "away" on Slack again

Cloud-based. No downloads. Works 24/7 even when your laptop is off.

This uses only the standard library. It retries when Slack rate-limits you and raises an error with Slack's message for anything else:

import json
import os
import time
import urllib.error
import urllib.request

URL = os.environ["SLACK_WEBHOOK_URL"]


def send(text, retries=3):
    body = json.dumps({"text": text}).encode()
    req = urllib.request.Request(URL, data=body, headers={"Content-Type": "application/json"})
    for attempt in range(retries):
        try:
            with urllib.request.urlopen(req, timeout=10) as resp:
                return resp.read().decode()  # "ok"
        except urllib.error.HTTPError as e:
            if e.code == 429:  # rate limited: wait as long as Slack asks
                time.sleep(int(e.headers.get("Retry-After", "1")))
                continue
            raise RuntimeError(f"{e.code} {e.read().decode()}")  # e.g. 400 invalid_payload
    raise RuntimeError("still rate limited")


print(send('Nightly job done: 1,204 rows, "0" errors'))

It printed ok. json.dumps escapes the quotes in the text, so the same function works for any message.

Slack webhook in Node.js

Node 18 and newer have fetch built in, so no package is needed. Save it as send.mjs and run node send.mjs:

const url = process.env.SLACK_WEBHOOK_URL;

const res = await fetch(url, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ text: "Backup finished in 42 s" }),
});
const body = await res.text(); // "ok", or an error such as "invalid_payload"
if (!res.ok) throw new Error(`${res.status} ${body}`);
console.log(body);

It printed ok on Node 26. JSON.stringify does the escaping.

Slack webhook error 400: invalid_payload and bad request

Quotes and new lines in webhook JSON: what breaks and what works

A 400 from a webhook almost always means the JSON is not valid. The classic mistake is an unescaped quote:

curl -i -X POST "$SLACK_WEBHOOK_URL" -H "Content-Type: application/json" \
  -d '{"text": "He said "hi""}'

The inner quotes end the string early, so the body {"text": "He said "hi""} is not JSON, and the answer is HTTP 400 with invalid_payload. Slack's docs say such a request "should not be retried without correction". The fixes:

  • • Build the JSON with jq, json.dumps or JSON.stringify instead of string pasting.
  • • Write a new line as \n inside the string, never a raw line break.
  • • Send files with --data-binary @file; plain -d @file drops the file's line breaks.
  • • Include text. Without text or blocks, Slack answers no_text.
  • Webhook responses to handle in code

    Never appear "away" on Slack again

    Cloud-based. No downloads. Works 24/7 even when your laptop is off.

    Webhook responses your code should handle: 200 ok, 400 invalid_payload, 400 no_text, 404 no_service, 410 channel_is_archived, 429
    ResponseWhat your code should do
    200 okDone. The body is plain text
    400 invalid_payloadFix the JSON; do not retry as is
    400 no_textAdd a text field
    403 action_prohibitedAn admin blocked posting; ask them
    404 no_serviceThe webhook was removed or disabled; create a new URL
    410 channel_is_archivedUnarchive the channel or make a new webhook
    429 Too Many RequestsWait Retry-After seconds, then send again

    The full list, with what each one means for setup, is in how to create a Slack webhook. For CI, how to send a Slack message from GitHub Actions has a ready workflow, and how to add a new line in Slack covers line breaks in API messages.

    FAQ

    How do I send a Slack message with curl?

    curl -X POST "$SLACK_WEBHOOK_URL" -H "Content-Type: application/json" -d '{"text": "Hello"}'. Slack answers ok.

    Why does my Slack webhook return 400 invalid_payload?

    The body is not valid JSON, usually an unescaped quote or a raw line break. Build it with jq or a JSON library.

    What does a Slack webhook return on success?

    HTTP 200 with the plain-text body ok. It does not return the message ts.

    Can a webhook post to a different channel?

    No. Each webhook posts to the channel chosen when it was created. Make one webhook per channel, or use a bot token with chat.postMessage.

    How many messages can I send to a Slack webhook?

    About one per second. Short bursts are allowed; after that Slack answers 429 with Retry-After.

    How do I mention someone in a webhook message?

    Use their user ID: <@U0123456789>. A plain @name stays text.

    Always Active

    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

    Guide

    Slack Emoji for Approved, Perfect and Sounds Good

    The Slack emoji for approved is ✅ :white_check_mark:. For perfect, use 💯 :100: or 🤌 :pinched_fingers:; for sounds good or sure, 👍 :+1: or 👌 :ok_hand:. Every code checked in Slack, what each one means to readers, custom LGTM and SGTM emoji, and a pinned legend for approval channels.

    Slack Green Team•5 min read
    Guide

    Tips for Working Across Time Zones: 10 Habits That Work

    Working across time zones works when the team writes things down, keeps a short overlap window for live talk, says the zone with every time, and lets tools deliver messages in each person's working hours. Ten habits, a follow-the-sun handoff note, the Slack settings that help, and a script that shows your team's local times.

    Slack Green Team•5 min read
    Templates

    Slack Emoji for OOO, PTO and Days Off

    The usual Slack emoji for OOO is 🌴 :palm_tree:, with ✈️ :airplane: for travel and 🗓️ :spiral_calendar_pad: for planned PTO. Enterprise's built-in Out of office status uses ⛔. Codes for vacation, upcoming PTO, sick days, holidays and outages, custom :ooo: and :pto: emoji, and statuses to copy.

    Slack Green Team•5 min read