WP Webhooks / Blog / WordPress integrations

wpcf7_mail_sent Hook: Contact Form 7 to Webhook Guide

How wpcf7_mail_sent fires, what get_posted_data() returns, and the reliable way to send Contact Form 7 submissions to any webhook endpoint.

8 min 2026-05-30 Updated 2026-10-14
#contact-form-7#wordpress#webhooks

TL;DR

  • wpcf7_mail_sent fires 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_post inside 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.

FIG 01 — CF7 form submission and hook lifecycle

/ 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.

CapabilityRaw wpcf7_mail_sent + wp_remote_postWebhook Actions (built-in CF7)
SetupCustom PHP in functions.php or pluginVisual admin UI — no code
DeliverySynchronous — blocks form responseAsync via its own queue, run by Action Scheduler or WP-Cron
RetryNone — one attempt onlyExponential backoff, 5 attempts
Delivery logNone — failures are silentPer-attempt log with request and response in wp-admin
Field mappingManual array construction in PHPGUI mapper — rename, restructure, exclude fields
Multiple webhooksOne add_action per endpointUnlimited webhooks per form trigger
try_it

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.

/Footnotes
¹ Contact Form 7 active install count from the WordPress.org plugin directory.
² Contact Form 7 documentation — official hook and API reference.
³ Action Scheduler usage guide — async job scheduling for WordPress.
FAQ

Common questions always ask.

Don't see yours? Open an issue on GitHub or check the full reference in the API docs.

What is the difference between wpcf7_mail_sent and wpcf7_before_send_mail? +
wpcf7_before_send_mail fires before CF7 sends its notification email and can abort the send by setting $abort to true. wpcf7_mail_sent fires after the mail step succeeds, meaning wp_mail() accepted the message (or mail was skipped via skip_mail, demo mode or the wpcf7_skip_mail filter). For webhook dispatch where you want confirmation the form was fully processed, wpcf7_mail_sent is the right hook.
How do you access Contact Form 7 field values inside wpcf7_mail_sent? +
Call WPCF7_Submission::get_instance() to retrieve the active submission object, then call $submission->get_posted_data() to get an associative array of field names to submitted values. Field names match the name attributes in the CF7 form editor.
Does wpcf7_mail_sent fire if the CF7 email fails to send? +
No. When the mail step fails, CF7 fires wpcf7_mail_failed instead. If you need to catch all valid submissions regardless of email status, use wpcf7_submit: it fires last, after the mail attempt, for every outcome (including validation_failed and spam), and passes $result, so check $result['status'] for mail_sent or mail_failed.
Is it safe to call wp_remote_post inside wpcf7_mail_sent? +
It works but blocks PHP execution until the remote endpoint responds. On a slow or unavailable endpoint this delays the success response seen by the visitor. The safer pattern is to enqueue the delivery via Action Scheduler or use Webhook Actions, which handles async delivery with automatic retry.
Does Webhook Actions require a separate CF7 webhook plugin? +
No. Webhook Actions has a built-in CF7 integration. You choose a CF7 hook such as wpcf7_mail_sent as the trigger, add a form-ID condition to target one form, and the plugin normalizes the form and submission into a structured payload automatically — no additional plugin or custom PHP code needed.
Ready

Your next automation is
one sentence away.

$ wp plugin install flowsystems-webhook-actions --activate