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
POSTof JSON. Minimum body is{ "text": "..." }; success is HTTP 200 with the bodyok. - 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.
/ 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
okmeans 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 and 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 — a full WordPress with Webhook Actions already configured, no signup — or install the free plugin and point a form trigger at your Slack URL.
ok success response and the note that Slack revokes leaked webhook URLs: Sending messages using incoming webhooks.$entry_id returning 0 on WPForms Lite: wpforms_process_complete.Retry-After: Slack Web API rate limits.