Send a Webhook to Slack: Transform & Forward Any Payload

Send any webhook to Slack. Step-by-step guide to forward an incoming webhook into a Slack channel, transforming the raw payload into Slack's { text } format in flight.

You have a service that fires webhooks — a CI pipeline, a signup form, an uptime monitor, a payment provider, an alerting tool — and you want each event to show up in a Slack channel. The problem: Slack incoming webhooks don't accept arbitrary payloads. They expect a specific JSON body, and the webhook your source sends almost never matches that shape.

Webhook Relay sits in the middle. It receives the incoming webhook at a stable public URL, transforms the payload into the format Slack expects, and delivers it — no glue server, no Lambda, no maintenance.

How it works

The flow is a single hop with a transform in the middle:

Your service  ──▶  Webhook Relay  ──▶  transform to { text }  ──▶  Slack channel
 (raw webhook)      (public URL)        (serverless function)      (message appears)

A Slack incoming webhook expects a JSON body like this:

{ "text": "your message" }

A generic incoming webhook isn't shaped like that, so we add a small transformation function that wraps the raw body into Slack's format before delivery. That one feature is what turns Webhook Relay into a universal "anything → Slack" bridge.

Step 1: Create the Slack incoming webhook URL

In Slack, you create an incoming webhook through a Slack app:

  1. Go to api.slack.com/apps and click Create New App → From scratch. Name it and pick your workspace.
  2. Open Incoming Webhooks and toggle it On.
  3. Click Add New Webhook to Workspace, choose the channel messages should post to, and authorize it.
  4. Copy the generated Webhook URL. It looks like:
https://hooks.slack.com/services/T0000/B0000/XXXXXXXX

Keep this URL handy — it's the destination Webhook Relay will deliver to. Each Slack incoming webhook is bound to one channel, so create one per channel you want to post to.

Step 2: Create a Webhook Relay public endpoint

Open the new public destination page and paste the Slack incoming webhook URL as the destination. Webhook Relay gives you back an input URL — a public endpoint that you'll point your source service at.

That input URL is now a stable address you can hand to any service. Everything sent to it gets forwarded toward Slack.

Step 3: Add a transformation function

Right now, anything you send to the input URL would be forwarded to Slack as-is — and Slack would reject it, because it isn't in { "text": "..." } shape. Add a transformation to fix that.

Click Transform on the input, open the Functions page, create a function "from scratch", and paste this Lua:

local json = require("json")

local payload = {
    text = r.RequestBody,
}

local body, err = json.encode(payload)
if err then error(err) end

r:SetRequestHeader("content-type", "application/json")
r:SetRequestMethod("POST")
r:SetRequestBody(body)

This takes the raw incoming body, wraps it as { "text": "..." }, sets the right headers and method, and lets Webhook Relay deliver it. Slack now accepts the message and posts it to your channel.

Step 4: Point your source at the URL and test

Configure your source service (the CI tool, form, monitor, payment provider — whatever you're integrating) to send its webhook to the Webhook Relay input URL.

To confirm everything works before wiring up the real service, fire a quick test with curl:

curl -X POST https://my.webhookrelay.com/v1/webhooks/your-input-id \
  -d 'Deployment finished: build #482 passed on main'

Within a second or two, Deployment finished: build #482 passed on main appears in your Slack channel. If nothing shows up, open the request log in your dashboard to see exactly what was received and delivered.

Going further

Once the basic bridge works, the transform function is where the real power is.

Build a richer message with Block Kit. If your source sends JSON, parse it and construct a Slack blocks array instead of a flat string — sections, labelled fields, dividers, even buttons:

local json = require("json")

local event = json.decode(r.RequestBody)

local payload = {
    blocks = {
        {
            type = "section",
            text = {
                type = "mrkdwn",
                text = "*Deployment " .. event.status .. "*\n" .. event.message,
            },
        },
    },
}

local body, err = json.encode(payload)
if err then error(err) end

r:SetRequestHeader("content-type", "application/json")
r:SetRequestMethod("POST")
r:SetRequestBody(body)

Filter out noise. Not every event deserves a ping. Use forwarding rules to only deliver events that match a condition, or drop them in the function with an early return.

Fan out to multiple channels. Add more destinations so one incoming webhook lands in several Slack channels — or in Slack and Discord — at the same time. Since each Slack incoming webhook targets one channel, add one destination per channel. See forwarding to multiple destinations.

For a complete real-world walkthrough of this transform pattern, see TradingView alerts to Discord and Slack, where a plain alert message is reshaped into a chat notification.

FAQ

What does Slack need to receive? A POST with a JSON body of { "text": "your message" } (or a blocks array for Block Kit) to a Slack incoming webhook URL. The transform function produces exactly that.

Do I need to run a server? No. Webhook Relay receives, transforms, and delivers everything in the cloud. There's nothing to host or keep online.

How do I see the raw payload my source sends? Send it to a free Webhook Bin first to inspect the exact body and headers, then write your transform around that structure.

Get started

Create a free Webhook Relay account and set up your first "anything → Slack" bridge in a few minutes. Not sure what your source sends? Inspect the payload in a Webhook Bin first, then write the transform to match.