---
title: "WordPress Form to Slack: Post Every Entry to a Channel"
description: "Send WordPress form entries to Slack: completion hooks for CF7, Gravity Forms and WPForms, Block Kit payloads, the 1 msg/s limit and why to queue it."
url: "https://wpwebhooks.org/blog/wordpress-form-to-slack/"
date: "2026-08-15"
---

# WordPress Form to Slack: Post Every Entry to a Channel

**TL;DR:**

-   Three form plugins, three completion hooks — `wpcf7_mail_sent`, `gform_after_submission`, `wpforms_process_complete` — all feeding one payload builder.
-   A Slack incoming webhook is a plain `POST` of JSON. Minimum body is `{ "text": "..." }`; success is HTTP 200 with the body `ok`.
-   The webhook URL **is** the credential. Slack actively hunts leaked ones and revokes them.
-   Rate limit is roughly **one message per second** with short bursts tolerated; beyond that you get 429 and a `Retry-After`.
-   Incoming webhooks cannot edit or delete a message once posted.

/ Overview

## How do you post WordPress form entries to Slack?

Catch the form plugin's completion hook, build a JSON message, and POST it to the incoming webhook URL Slack gave you. There is no authentication step beyond the URL itself and no SDK worth installing — the whole integration is one HTTP request with a `Content-type: application/json` header.¹

What varies between form plugins is only where the data comes from. Everything after that is shared, which is why the payload builder should be one function rather than three copies.

FIG 01 — Form entry to Slack channel, and where it blocks

/ Hooks

## Which hook fires when each form plugin finishes?

All three fire after a successful submission, but they hand you very different things — and only one of them gives you the data directly.

| Plugin | Hook | What the callback receives |
| --- | --- | --- |
| Contact Form 7 | wpcf7\_mail\_sent | The WPCF7\_ContactForm object only — read values via WPCF7\_Submission |
| Gravity Forms | gform\_after\_submission | $entry and $form — the entry holds values keyed by field ID |
| WPForms | wpforms\_process\_complete | $fields, $entry, $form\_data, $entry\_id — four arguments |

WPForms is the one with a trap worth knowing: its fourth argument, `$entry_id`, **returns 0 when entry storage is disabled or the site is running WPForms Lite**.² Code that builds a "view this entry" link from it produces a dead link on every Lite install, and it will look fine on your Pro development site.

PHP — three sources, one builder

```
// Contact Form 7
add_action( 'wpcf7_mail_sent', function( $contact_form ) {
    $s = WPCF7_Submission::get_instance();
    if ( $s ) {
        my_notify_slack( $contact_form->title(), $s->get_posted_data() );
    }
} );

// Gravity Forms
add_action( 'gform_after_submission', function( $entry, $form ) {
    my_notify_slack( $form['title'], $entry );
}, 10, 2 );

// WPForms — note the argument count of 4
add_action( 'wpforms_process_complete', function( $fields, $entry, $form_data, $entry_id ) {
    my_notify_slack( $form_data['settings']['form_title'], $fields );
}, 10, 4 );
```

/ The request

## What does a Slack incoming webhook expect?

A POST with `Content-type: application/json` and a body containing at least a `text` field. Success is HTTP 200 with the literal response body `ok` — not JSON, just those two characters, which is worth knowing because a naive `json_decode()` of the response returns null on a perfectly successful send.¹

PHP — posting to the webhook

```
function my_notify_slack( $form_title, array $values ) {

    $lines = [];
    foreach ( $values as $k => $v ) {
        if ( is_array( $v ) ) {
            $v = implode( ', ', $v );
        }
        $lines[] = sprintf( '*%s:* %s', $k, wp_strip_all_tags( (string) $v ) );
    }

    $body = [
        'text' => sprintf( "New submission: %s\n%s", $form_title, implode( "\n", $lines ) ),
    ];

    $res = wp_remote_post( MY_SLACK_WEBHOOK_URL, [
        'timeout' => 10,
        'headers' => [ 'Content-Type' => 'application/json' ],
        'body'    => wp_json_encode( $body ),
    ] );

    // success is the plain string "ok", not JSON
    return ! is_wp_error( $res )
        && 'ok' === trim( wp_remote_retrieve_body( $res ) );
}
```

Note `wp_strip_all_tags()` on every value. Form input reaches Slack unescaped otherwise, and Slack's `mrkdwn` parser treats `*`, `_` and `<>` as formatting — a message body containing `<https://evil.example|click here>` renders as a disguised link in your channel.

/ Formatting

## When should you use Block Kit instead of text?

Use `text` until the message needs structure, then move to `blocks`. A plain-text message is one field and always renders; Block Kit gives you headers, two-column field lists and buttons, at the cost of a much larger payload that fails validation as a unit.

One practical rule: keep `text` populated even when sending `blocks`. It is what Slack shows in the notification preview and in clients that cannot render the blocks, and a message with blocks but no text arrives as a silent, empty-looking notification.

> The webhook URL is the entire authentication story. Anyone who has it can post to your channel as your app, forever, from anywhere.

/ Limits

## What are Slack's rate limits for incoming webhooks?

Incoming webhooks and `chat.postMessage` sit in Slack's Special tier: **one message per second**, with short bursts above that tolerated. Sustained excess returns HTTP 429 with a `Retry-After` header in seconds, and Slack warns that continued abuse risks the app being disconnected or disabled outright.³

One per second sounds ample until a bulk action fans out. Import 500 entries and notify on each and you need 500 seconds — **over eight minutes** — of continuous posting, during which every other notification from the same app is competing for the same budget. Batch the summary instead of sending 500 messages nobody will read.

/ Gaps

## What does Slack not protect you from?

-   **A leaked URL is a live credential.** Slack states it actively searches out and revokes leaked webhook URLs — which means a URL committed to a public repository can be revoked without warning, and your notifications simply stop.
-   **No edit, no delete.** Incoming webhooks cannot modify a message after posting. If a submission contained something that should not have been broadcast, the only remedy is manual.
-   **No delivery guarantee on your side.** Slack returning `ok` means Slack accepted it. If your request never left because the site was mid-deploy, nothing anywhere records that a notification was owed.
-   **Everything you send is readable by the whole channel.** A form that collects a phone number or an address will put it in front of every member — including guests. Send a link and a name, not the payload.

/ Delivery

## Should the send be inline or queued?

Queued, once notifications matter. Inline is defensible for a low-traffic contact form: the failure mode is a missed Slack message, not lost data, because the entry is already stored by the form plugin. That is a genuinely different risk profile from the [Airtable](https://wpwebhooks.org/blog/contact-form-7-to-airtable/) and [Notion](https://wpwebhooks.org/blog/contact-form-7-to-notion/) cases, where the third party holds the only copy.

It stops being defensible as soon as the message is the record — an alert nobody else stores, or a notification an on-call process depends on. Then a 10-second timeout inside a form submission is both a visitor-facing delay and a single point of loss.

| Concern | Inline post | Queued delivery |
| --- | --- | --- |
| Visitor wait | Up to the full timeout | None |
| Slack 429 | Message dropped | Retried after Retry-After |
| Bulk actions | Trips the 1/s limit immediately | Paced automatically |
| Proof it was sent | None | Per-attempt log with the response |

/ Secrets

## How do you keep the webhook URL out of your code?

Put it in `wp-config.php` as a constant, or in an encrypted store — never in a theme file, never in a plugin you commit, and never in a post meta field that an editor can read. The URL grants posting rights with no expiry and no scope, so it deserves the same handling as a database password.

If the site is in version control, add a guard that fails loudly when the constant is missing, rather than defaulting to a hard-coded fallback. A missing-constant fatal on deploy is a five-minute fix; a fallback URL that quietly posts a client's form entries into your own test channel is not.

**Try it without writing the plumbing.** [Open the live preview](https://playground.wordpress.net/?blueprint-url=https://wpwebhooks.org/blueprint.json) — a full WordPress with Webhook Actions already configured, no signup — or [install the free plugin](https://wordpress.org/plugins/flowsystems-webhook-actions/) and point a form trigger at your Slack URL.

/Footnotes

¹ POST format, the JSON body shape, the plain `ok` success response and the note that Slack revokes leaked webhook URLs: [Sending messages using incoming webhooks](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/).

² The four-argument signature and the `$entry_id` returning 0 on WPForms Lite: [wpforms\_process\_complete](https://wpforms.com/developers/wpforms_process_complete/).

³ The Special tier, one message per second with burst tolerance, 429 and `Retry-After`: [Slack Web API rate limits](https://docs.slack.dev/apis/web-api/rate-limits/).

⁴ WordPress HTTP helper used above: [wp\_remote\_post()](https://developer.wordpress.org/reference/functions/wp_remote_post/).

## Structured data

```json
{"@context":"https://schema.org","@type":"Article","headline":"WordPress Form to Slack: Post Every Entry to a Channel","description":"Send WordPress form entries to Slack: completion hooks for CF7, Gravity Forms and WPForms, Block Kit payloads, the 1 msg/s limit and why to queue it.","datePublished":"2026-08-15","dateModified":"2026-08-15","author":{"@type":"Person","name":"Mateusz Skorupa","url":"https://wpwebhooks.org/about/"},"publisher":{"@type":"Organization","name":"WP Webhooks","url":"https://wpwebhooks.org"},"url":"https://wpwebhooks.org/blog/wordpress-form-to-slack/","image":"https://wpwebhooks.org/og_image.jpg","keywords":["wordpress form slack","contact form 7 slack","gravity forms slack","wpforms slack","slack incoming webhook wordpress","wordpress slack notification"]}

{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"WP Webhooks","item":"https://wpwebhooks.org/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://wpwebhooks.org/blog/"},{"@type":"ListItem","position":3,"name":"WordPress Form to Slack: Post Every Entry to a Channel","item":"https://wpwebhooks.org/blog/wordpress-form-to-slack/"}]}

{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"How do you send a WordPress form submission to Slack?","acceptedAnswer":{"@type":"Answer","text":"Hook the form plugin completion action — wpcf7_mail_sent, gform_after_submission or wpforms_process_complete — build a JSON body containing at least a text field, and POST it to your Slack incoming webhook URL with Content-type application/json."}},{"@type":"Question","name":"What does a Slack incoming webhook return on success?","acceptedAnswer":{"@type":"Answer","text":"HTTP 200 with the plain string ok as the response body. It is not JSON, so decoding the response as JSON returns null even on a completely successful send."}},{"@type":"Question","name":"What is the rate limit for Slack incoming webhooks?","acceptedAnswer":{"@type":"Answer","text":"Roughly one message per second, in Slack Special tier, with short bursts above that tolerated. Sustained excess returns HTTP 429 with a Retry-After header, and Slack warns that continued abuse can get an app disconnected or disabled."}},{"@type":"Question","name":"Should I use text or Block Kit blocks?","acceptedAnswer":{"@type":"Answer","text":"Use text until the message needs structure such as headers, field lists or buttons. Even when sending blocks, keep the text field populated — it is what Slack shows in the notification preview and in clients that cannot render blocks."}},{"@type":"Question","name":"Is a Slack webhook URL a secret?","acceptedAnswer":{"@type":"Answer","text":"Yes. The URL is the entire credential — anyone holding it can post to that channel as your app with no expiry. Store it in wp-config.php or an encrypted store, never in committed code. Slack actively searches for leaked webhook URLs and revokes them."}}]}

{"@context":"https://schema.org","@type":"ImageObject","contentUrl":"https://wpwebhooks.org/diagrams/wordpress-form-to-slack.png","caption":"FIG 01 — Form entry to Slack channel, and where it blocks","description":"Each form plugin exposes its own completion hook: wpcf7_mail_sent for Contact Form 7, gform_after_submission for Gravity Forms and wpforms_process_complete for WPForms. All three converge on one payload builder that produces either a plain text message or Block Kit blocks. That JSON is posted to the incoming webhook URL, which is itself the credential and must never be committed or exposed. Slack answers a success with the plain string ok and rate limits incoming webhooks to roughly one message per second, returning 429 with a Retry-After header beyond that. Posting inline ties the visitor to Slack being reachable, so the send belongs behind a queue.","encodingFormat":"image/png","creditText":"WP Webhooks","license":"https://wpwebhooks.org/"}
```
