TL;DR
wpcf7_mail_sentfires after CF7's mail step succeeds (wp_mail()accepted the message, or mail was skipped) — use it when the webhook should only follow a fully processed submission- Access field values via
WPCF7_Submission::get_instance()->get_posted_data() - Calling
wp_remote_postinside the hook is synchronous — it blocks PHP until your endpoint responds - Webhook Actions ships a built-in CF7 integration with async queue, retry, and delivery logs — no custom PHP needed
/ What is wpcf7_mail_sent
What is wpcf7_mail_sent and when does it fire?
wpcf7_mail_sent is a WordPress action hook provided by Contact Form 7 (CF7) — with over 10 million active installs¹, the most widely deployed form plugin in the WordPress ecosystem. The hook fires at the end of CF7's form processing pipeline, specifically after the plugin's mail step reports success. "Success" means wp_mail() returned true — the message was handed to the mailer, not necessarily delivered to an inbox. The hook also fires when mail is skipped entirely (the skip_mail form setting, demo mode or the wpcf7_skip_mail filter), in which case no email is sent at all.
The practical implication: if you hook into wpcf7_mail_sent, you know the form passed validation, cleared spam checks, and that CF7's mail step did not fail. This makes it the right attachment point for downstream automations — CRM entries, Slack notifications, and webhook dispatches — where you want confirmation the form fully succeeded before triggering side effects.
/ Before vs After
How does wpcf7_mail_sent differ from wpcf7_before_send_mail?
Contact Form 7 exposes two hooks around its mail-sending step. wpcf7_before_send_mail fires immediately before CF7 attempts to send the email and receives a reference to the $abort flag — setting it to true prevents both the email send and any wpcf7_mail_sent callback. This makes wpcf7_before_send_mail the right hook for altering payload data or implementing custom spam filtering.
wpcf7_mail_sent fires only after the mail step succeeds (when it fails, CF7 fires wpcf7_mail_failed instead). You cannot abort or modify the submission at this point — the hook is purely for reacting to a confirmed submission. Use wpcf7_mail_sent when your downstream system should only receive data that CF7 itself treated as fully processed.
A third hook, wpcf7_submit, fires last — after the whole pipeline has finished — and it fires for every outcome, including validation_failed, spam, aborted and mail_failed. It passes ( $contact_form, $result ), so you must filter on $result['status'] yourself (for example, accept both mail_sent and mail_failed to capture valid submissions regardless of email status). For webhook dispatch, wpcf7_mail_sent is usually simpler because it already excludes invalid, spam, aborted and failed submissions.
/ Hook parameters
What parameters does wpcf7_mail_sent pass to your callback?
The hook passes a single argument: $contact_form, an instance of WPCF7_ContactForm. This object exposes the form configuration — its ID, title, and properties — but not the submitted field values. Submitted values live on the submission object, which is accessed separately.
PHP — hook registration
add_action( 'wpcf7_mail_sent', function( $contact_form ) { // $contact_form is WPCF7_ContactForm $form_id = $contact_form->id(); // integer $form_name = $contact_form->title(); // string — the form name from CF7 editor // Submitted values are NOT on $contact_form — see next section } );
/ Accessing field values
How do you access submitted field values inside wpcf7_mail_sent?
Submitted data is available on the WPCF7_Submission singleton. Call WPCF7_Submission::get_instance() — this returns the submission object for the current request, or null outside a CF7 processing context. Always null-check before proceeding.
The get_posted_data() method returns an associative array keyed by the name attributes set in the CF7 form editor (the values inside square brackets in CF7 shortcodes, e.g. [text your-name]). All values are run through CF7's sanitization pipeline before being available here.
PHP — reading submitted field values
add_action( 'wpcf7_mail_sent', function( $contact_form ) { $submission = WPCF7_Submission::get_instance(); if ( ! $submission ) { return; } $data = $submission->get_posted_data(); // $data is an associative array: // [ 'your-name' => 'Jane Smith', 'your-email' => '[email protected]', ... ] $payload = [ 'form_id' => $contact_form->id(), 'form_name' => $contact_form->title(), 'name' => $data['your-name'] ?? '', 'email' => $data['your-email'] ?? '', 'message' => $data['your-message'] ?? '', ]; } );
/ Webhook dispatch
How do you dispatch a webhook from wpcf7_mail_sent?
Once you have the payload array, pass it to wp_remote_post with a JSON body. The implementation is a direct HTTP call: serialise the payload with wp_json_encode, set Content-Type: application/json, and send to your endpoint URL.
PHP — wp_remote_post dispatch inside wpcf7_mail_sent
add_action( 'wpcf7_mail_sent', function( $contact_form ) { $submission = WPCF7_Submission::get_instance(); if ( ! $submission ) { return; } $data = $submission->get_posted_data(); $payload = [ 'form_id' => $contact_form->id(), 'name' => $data['your-name'] ?? '', 'email' => $data['your-email'] ?? '', 'message' => $data['your-message'] ?? '', ]; wp_remote_post( 'https://your-endpoint.com/webhook', [ 'headers' => [ 'Content-Type' => 'application/json' ], 'body' => wp_json_encode( $payload ), 'timeout' => 5, 'blocking' => true, // ← PHP waits here 'data_format' => 'body', ] ); } );
/ Reliability risks
What are the reliability risks of synchronous dispatch inside a hook?
The code above works — but it is synchronous. When PHP executes wp_remote_post with 'blocking' => true, the process stalls until your webhook endpoint sends a response or the timeout elapses (5 seconds in the example). During that window, the user's browser is waiting for the CF7 success response, the PHP-FPM worker is occupied, and any slow or unavailable endpoint directly degrades your form's perceived performance.
There is also no retry. If your endpoint returns a 5xx error or times out, the delivery is silently lost — no log entry, no re-attempt, no alert. On high-traffic forms this adds up quickly: a downstream endpoint that experiences 2% transient failures across 500 daily submissions means 10 missed integrations per day with no visibility.
Calling wp_remote_post synchronously inside a form hook means every visitor who submits your form waits for your webhook endpoint — not just for WordPress. / Webhook Actions
How does Webhook Actions handle CF7 webhook delivery without custom code?
The Webhook Actions plugin includes a built-in Contact Form 7 integration. You pick the CF7 hook (wpcf7_mail_sent or wpcf7_before_send_mail) as the webhook trigger, add a condition on the form ID if only one form should fire it, and configure the endpoint URL. The plugin normalizes CF7's objects into a clean payload (form id, title, name, locale and a submission block with the fields and meta), which you can then reshape with the visual field mapper — no PHP code required.
Delivery is handled asynchronously: Webhook Actions enqueues each submission into its own queue table, and Action Scheduler (or WP-Cron when Action Scheduler is absent) runs that queue, which means the form response returns immediately and the HTTP dispatch happens in the next queue cycle — typically within seconds but always outside the visitor's request. Failed deliveries retry automatically with exponential backoff (1 min → 2 min → 4 min → 8 min, capped at 1 hour, default 5 attempts; 4xx responses are not retried) and are logged with the full request body and HTTP response for inspection in wp-admin.
| Capability | Raw wpcf7_mail_sent + wp_remote_post | Webhook Actions (built-in CF7) |
|---|---|---|
| Setup | Custom PHP in functions.php or plugin | Visual admin UI — no code |
| Delivery | Synchronous — blocks form response | Async via its own queue, run by Action Scheduler or WP-Cron |
| Retry | None — one attempt only | Exponential backoff, 5 attempts |
| Delivery log | None — failures are silent | Per-attempt log with request and response in wp-admin |
| Field mapping | Manual array construction in PHP | GUI mapper — rename, restructure, exclude fields |
| Multiple webhooks | One add_action per endpoint | Unlimited webhooks per form trigger |
Seeing it run beats reading about it. The live preview boots a throwaway WordPress with Webhook Actions already installed and demo deliveries sitting in the log — no signup, nothing left on your machine afterwards.
/ Testing and monitoring
How do you test and monitor CF7 webhook delivery in production?
The quickest way to validate the raw wpcf7_mail_sent approach is to point the endpoint URL at a request inspector like webhook.site — a temporary URL that logs every inbound request with full headers and body. Submit the form, then inspect the payload on webhook.site to confirm field values and structure before switching to the real destination.
For production monitoring without a plugin, is_wp_error on the wp_remote_post return value surfaces transport errors (timeouts, DNS failures), and wp_remote_retrieve_response_code gives the HTTP status. Log both to error_log or a custom table for visibility. With Webhook Actions, delivery logs are built-in: every attempt records the timestamp, HTTP status code, full response body, and retry count — visible from the Webhook Actions → Logs screen.
The Contact Form 7 to Webhook overview covers the full Webhook Actions setup flow including test delivery from the plugin admin.
Once the hook fires reliably, the remaining work is destination-specific — the receiving API decides the payload shape, not Contact Form 7. Two worked examples: sending entries to an Airtable base, where every key must already exist as a field, and creating a Notion page per entry, where each value has to be wrapped in its declared property type.