Why CF7 Plugin Settings Stop Saving After a CF7 Update (And How to Fix It)

php dev.to

A developer posted on the WordPress support forums:

"The API settings via the plugin cannot be saved. Whenever I click Save after completing the form, the text fields are empty again."

Another user confirmed the same issue and traced it to a CF7 version update:

"I'm noticing the same issue for sites that updated Contact Form 7 to version 5.5.3. Rolling back to 5.5.2 fixed it for me."

Thread closed. Not resolved.

Rolling back is not a fix. It is a temporary workaround that leaves your site running an outdated plugin version indefinitely. The actual problem is a CF7 architectural change in 5.5.x that broke how certain third-party plugins registered and saved their settings. Understanding what changed, why it breaks settings saving, and how to build integrations that survive CF7 version bumps is what this post covers.

What Changed in CF7 5.5.x

CF7 version 5.5 introduced significant internal changes to how the plugin manages its admin pages and settings. Specifically:

Menu registration changed. CF7 moved from using add_menu_page() with its own top-level admin menu to a restructured settings architecture. Third-party plugins that hooked into CF7's admin menu using the old approach found their settings pages either disappearing or failing to process $_POST correctly.

Nonce handling tightened. CF7 5.5.x tightened nonce verification on its settings save handlers. Third-party plugins that piggy-backed on CF7's options.php flow without registering their own nonces correctly started failing silently on save.

Settings API registration assumptions broke. Some CF7 add-on plugins assumed CF7's internal action names and page slugs were stable. When CF7 renamed internal hooks and page identifiers in 5.5, any plugin that referenced those by name stopped working.

The result: you click Save, the page reloads, and the fields are empty. No error message. No debug log entry. The settings were never written to the database.

Why Settings Appear to Save But Do Not

When a WordPress settings form submits and the data does not persist, one of four things is happening:

1. Nonce verification failed

WordPress settings forms include a nonce field for CSRF protection. If the nonce is invalid, expired, or missing, WordPress rejects the $_POST data silently and redirects back to the settings page. The fields appear empty because the data was never saved.

Check by temporarily adding to wp-config.php:

define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
Enter fullscreen mode Exit fullscreen mode

Then save the settings and check wp-content/debug.log for nonce-related warnings.

2. The settings were not registered with register_setting()

WordPress's Settings API requires that any option saved via options.php is registered with register_setting($option_group, $option_name). If a plugin saves settings through options.php but did not call register_setting(), WordPress 4.9+ silently discards the unregistered options.

3. The form action attribute points to the wrong handler

If a plugin's settings form submits to options.php but the plugin's $option_group does not match what settings_fields() outputs, the nonce check fails and nothing saves.

4. A PHP error occurs during the save handler

If the plugin's register_setting() sanitize callback throws a PHP error or warning, WordPress may abort the save process mid-execution. With error display off (production sites), this looks identical to a nonce failure.

The Correct Pattern for CF7 Add-On Settings That Survive Version Changes

The mistake most CF7 add-on plugins make is coupling their settings registration to CF7's internal page hooks. When CF7 changes those hooks, the add-on breaks.

The correct pattern is to register settings completely independently of CF7's internals:

// 1. Register your own settings page independently
add_action('admin_menu', 'myplugin_register_settings_page');

function myplugin_register_settings_page() {
    add_options_page(
        'My CF7 Integration Settings',
        'CF7 Integration',
        'manage_options',
        'myplugin-cf7-settings',
        'myplugin_render_settings_page'
    );
}

// 2. Register your options with the Settings API
add_action('admin_init', 'myplugin_register_settings');

function myplugin_register_settings() {
    register_setting(
        'myplugin_cf7_settings_group',   // option group
        'myplugin_api_endpoint',          // option name
        [
            'type'              => 'string',
            'sanitize_callback' => 'esc_url_raw',
            'default'           => '',
        ]
    );

    register_setting(
        'myplugin_cf7_settings_group',
        'myplugin_api_username',
        [
            'type'              => 'string',
            'sanitize_callback' => 'sanitize_text_field',
            'default'           => '',
        ]
    );

    register_setting(
        'myplugin_cf7_settings_group',
        'myplugin_api_password',
        [
            'type'              => 'string',
            'sanitize_callback' => 'sanitize_text_field',
            'default'           => '',
        ]
    );
}

// 3. Render the settings form correctly
function myplugin_render_settings_page() {
    if (!current_user_can('manage_options')) {
        wp_die('Insufficient permissions');
    }
    ?>
    <div class="wrap">
        <h1>CF7 Integration Settings</h1>
        <form method="post" action="options.php">
            <?php
            // This outputs the nonce, action, and option_page fields correctly
            settings_fields('myplugin_cf7_settings_group');
            ?>
            <table class="form-table">
                <tr>
                    <th>API Endpoint</th>
                    <td>
                        <input type="url"
                               name="myplugin_api_endpoint"
                               value="<?php echo esc_attr(get_option('myplugin_api_endpoint')); ?>"
                               class="regular-text" />
                    </td>
                </tr>
                <tr>
                    <th>Username</th>
                    <td>
                        <input type="text"
                               name="myplugin_api_username"
                               value="<?php echo esc_attr(get_option('myplugin_api_username')); ?>"
                               class="regular-text" />
                    </td>
                </tr>
                <tr>
                    <th>Password / API Key</th>
                    <td>
                        <input type="password"
                               name="myplugin_api_password"
                               value="<?php echo esc_attr(get_option('myplugin_api_password')); ?>"
                               class="regular-text" />
                    </td>
                </tr>
            </table>
            <?php submit_button(); ?>
        </form>
    </div>
    <?php
}
Enter fullscreen mode Exit fullscreen mode

This approach has zero dependency on CF7's internal page hooks, menu structure, or version-specific action names. If CF7 completely rewrites its admin in a future version, these settings still save correctly.

Storing API Credentials: get_option vs wp-config.php

Storing API usernames and passwords in the WordPress database via get_option is convenient but not ideal for credentials. The database is accessible to any code running on your WordPress installation, including other plugins.

For better security, store credentials in wp-config.php as constants:

// wp-config.php
define('CF7_API_ENDPOINT', 'https://api.example.com/v1/');
define('CF7_API_USERNAME', 'your-username');
define('CF7_API_PASSWORD', 'your-api-key');
Enter fullscreen mode Exit fullscreen mode

Then in your plugin:

$endpoint = defined('CF7_API_ENDPOINT') ? CF7_API_ENDPOINT : get_option('myplugin_api_endpoint');
$username = defined('CF7_API_USERNAME') ? CF7_API_USERNAME : get_option('myplugin_api_username');
$password = defined('CF7_API_PASSWORD') ? CF7_API_PASSWORD : get_option('myplugin_api_password');
Enter fullscreen mode Exit fullscreen mode

This pattern lets you use wp-config.php constants on servers where you control the environment (staging, production) and fall back to the database UI for development environments where constants are not defined.

Basic Auth: What It Is and When It Is Acceptable

The plugin in this thread is named "CF7 to API + Basic Auth," so it is worth being precise about what Basic Auth is and its limitations.

Basic Auth sends credentials as a Base64-encoded string in the Authorization header:

Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
Enter fullscreen mode Exit fullscreen mode

That encoded string is username:password in Base64. It is trivially decodable. Base64 is encoding, not encryption.

Basic Auth is acceptable when:

  • The connection is over HTTPS (TLS encrypts the header in transit)
  • The API has no more secure auth option available
  • The credentials have minimal scope (read-only or limited write access)

Basic Auth is not acceptable when:

  • The connection is over plain HTTP (credentials are visible to any network observer)
  • The API supports OAuth 2.0 or API key-based auth (use those instead)
  • The credentials are high-privilege account credentials rather than scoped API keys

For CF7 integrations sending form data to an external API, Bearer token auth is preferable to Basic Auth when the API supports it. Bearer tokens can be scoped, rotated, and revoked without changing account passwords.

Contact Form to API supports both Basic Auth and Bearer token authentication, and stores credentials in a way that survives CF7 version updates without breaking the settings page.

Diagnosing Settings That Do Not Save

Run this checklist before doing anything else:

// Add temporarily to functions.php to debug settings saves
add_action('updated_option', function($option_name, $old_value, $new_value) {
    if (strpos($option_name, 'myplugin') !== false) {
        error_log('Option updated: ' . $option_name . ' = ' . print_r($new_value, true));
    }
}, 10, 3);

add_action('update_option', function($option_name, $old_value, $new_value) {
    if (strpos($option_name, 'myplugin') !== false) {
        error_log('Attempting to update: ' . $option_name);
    }
}, 10, 3);
Enter fullscreen mode Exit fullscreen mode

If update_option fires but updated_option does not, the value being saved matches the existing value (no change, so WordPress skips the write). If neither fires, the $_POST data never reached the options handler, which points to a nonce failure or unregistered option.

The Real Fix vs Rolling Back

Rolling back CF7 to 5.5.2 stops the breakage temporarily. It does not fix the underlying issue and leaves you running an outdated plugin with unpatched security issues.

The real fix is either:

  • Update the broken add-on plugin to use CF7-version-agnostic settings registration (as shown above)
  • Replace the broken plugin with one that does not rely on CF7's internal admin architecture

Contact Form to API registers its settings independently of CF7's internals and has maintained compatibility across CF7 major versions without requiring rollbacks.

Source: dev.to

arrow_back Back to Tutorials