preloader

Contact Form 7 Custom Validation and Hooks: A Developer Reference

Contact Form 7 has a hook system that is more capable than most developers realise. The documentation is thin, but the hooks cover custom validation, pre-send manipulation, post-send processing, and custom form tags. This is a working reference for the most useful ones.

Custom Field Validation

The wpcf7_validate hook fires for each field type. To add custom validation to a text field:

add_filter('wpcf7_validate_text', 'validate_uk_postcode', 10, 2);
add_filter('wpcf7_validate_text*', 'validate_uk_postcode', 10, 2);

function validate_uk_postcode($result, $tag) {{
    if ($tag->name !== 'postcode') {{
        return $result;
    }}

    $value = isset($_POST[$tag->name]) ? trim($_POST[$tag->name]) : '';
    $pattern = '/^[A-Z]{{1,2}}[0-9][0-9A-Z]?s?[0-9][A-Z]{{2}}$/i';

    if ($value && !preg_match($pattern, $value)) {{
        $result->invalidate($tag, 'Please enter a valid UK postcode.');
    }}

    return $result;
}}

The hook name follows the pattern wpcf7_validate_[field_type]. The asterisk version (wpcf7_validate_text*) covers required text fields. Always hook both to cover optional and required variants of the same field type.

Conditional Required Fields

CF7 does not have conditional logic built in, but you can make a field required based on another field’s value using validation hooks:

add_filter('wpcf7_validate_text', 'conditionally_require_company', 10, 2);

function conditionally_require_company($result, $tag) {{
    if ($tag->name !== 'company_name') {{
        return $result;
    }}

    $account_type = isset($_POST['account_type']) ? $_POST['account_type'] : '';
    $company_name = isset($_POST['company_name']) ? trim($_POST['company_name']) : '';

    if ($account_type === 'business' && empty($company_name)) {{
        $result->invalidate($tag, 'Company name is required for business accounts.');
    }}

    return $result;
}}

Need this built properly? Describe the project and get a free estimate.

Modifying the Email Before It Sends

The wpcf7_before_send_mail hook fires after validation passes but before the email is sent. Use it to add data, modify recipients, or add CC/BCC based on form values:

add_action('wpcf7_before_send_mail', 'route_cf7_by_department');

function route_cf7_by_department($contact_form) {{
    // Only affect a specific form by ID
    if ($contact_form->id() !== 42) {{
        return;
    }}

    $submission = WPCF7_Submission::get_instance();
    if (!$submission) return;

    $posted = $submission->get_posted_data();
    $department = isset($posted['department']) ? $posted['department'] : '';

    $mail = $contact_form->prop('mail');

    switch ($department) {{
        case 'sales':
            $mail['recipient'] = 'sales@example.com';
            break;
        case 'support':
            $mail['recipient'] = 'support@example.com';
            break;
        default:
            $mail['recipient'] = 'info@example.com';
    }}

    $contact_form->set_properties(array('mail' => $mail));
}}

Saving Submissions to the Database

CF7 does not store submissions by default. To save to a custom table on successful submission:

add_action('wpcf7_mail_sent', 'save_cf7_submission_to_db');

function save_cf7_submission_to_db($contact_form) {{
    if ($contact_form->id() !== 42) return;

    $submission = WPCF7_Submission::get_instance();
    if (!$submission) return;

    $data = $submission->get_posted_data();

    global $wpdb;
    $wpdb->insert(
        $wpdb->prefix . 'contact_submissions',
        array(
            'form_id'    => $contact_form->id(),
            'name'       => sanitize_text_field($data['your-name'] ?? ''),
            'email'      => sanitize_email($data['your-email'] ?? ''),
            'message'    => sanitize_textarea_field($data['your-message'] ?? ''),
            'created_at' => current_time('mysql'),
        ),
        array('%d', '%s', '%s', '%s', '%s')
    );
}}

You need to create the table first, typically in a plugin activation hook or a Code Snippets “run once” snippet. Alternatively, use Flamingo (from the CF7 author) which handles submission storage without custom code.

Adding a Custom Form Tag

CF7 lets you register custom shortcode-style tags for form fields. A simple custom tag that renders a hidden field with a server-generated value:

add_action('wpcf7_init', 'register_cf7_session_token_tag');

function register_cf7_session_token_tag() {{
    wpcf7_add_form_tag('session_token', 'render_cf7_session_token');
}}

function render_cf7_session_token($tag) {{
    $token = wp_create_nonce('cf7_submission_' . session_id());
    return '';
}}

Keep Reading

Previous Post ACF and the WordPress REST API: Exposing Custom Fields to External Applications Next Post Stopping Contact Form 7 Spam Without reCAPTCHA

Need Help With Your WordPress Site?

If you need help with WordPress fixes, plugin issues, theme customization, or development work, feel free to get in touch.

Get a Free Estimate