TL;DR
wpcf7_mail_sentfires after CF7 successfully sends its notification email — use it when you need confirmed delivery before dispatching a webhook- 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 has successfully delivered its configured notification email.
The practical implication: if you hook into wpcf7_mail_sent, you know the form passed validation, cleared spam checks, and that CF7's own mailer completed without error. 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.
Visitor submits form (POST)
│
▼
CF7 validates fields
(required fields, email format, etc.)
│ validation passes
▼
Spam check (Akismet / CAPTCHA)
│ passes
▼
wpcf7_before_send_mail fires ◄─ can set $abort = true here
│ not aborted
▼
CF7 sends notification email
│
├─ mail success ──► wpcf7_mail_sent fires ◄─ your webhook here
│
└─ mail failure ──► wpcf7_mail_failed fires / 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 send completes successfully. 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 after validation but before the mail step. If you need to capture every validated submission regardless of email delivery status, wpcf7_submit is the hook to use — but for webhook dispatch, the later wpcf7_mail_sent is usually preferable because it filters out aborted or failed submissions automatically.
/ 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. When CF7 is active alongside Webhook Actions, CF7 form submissions appear as native trigger types in the plugin's admin UI. You select the form, map fields to your payload using the visual mapper, and configure the endpoint URL — no PHP code required.
Delivery is handled asynchronously: Webhook Actions enqueues each submission via Action Scheduler when available, 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) 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 Action Scheduler queue |
| 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 |
/ 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.