# WordPress 7.2 Secrets API: Stop Storing API Keys in Plain Text

> Source: <https://byteiota.com/wordpress-7-2-secrets-api-stop-storing-api-keys-in-plain-text/>
> Published: 2026-11-15 20:08:21+00:00

An audit of a single WordPress 7.1 installation with 48 active plugins turned up 56 API keys and tokens sitting in the options table as plain text. Thirty-six of them were autoloaded on every page request. Forty-eight appeared in a database dump from six months prior — still sitting unencrypted on the server. Fifteen of the AI service keys were still active when tested. That is not a misconfiguration story. That is how WordPress has worked for two decades.

WordPress 7.2 ships a Secrets API that replaces this pattern with encrypted, versioned credential storage. RC1 lands November 17. Final release is December 8. If you write plugins, here is what you need to know before your users upgrade.

## What the Secrets API Actually Does

The core idea is simple: instead of a string sitting in `wp_options`, you get a `WP_Secret` object. The object prints as `[secret:my-plugin/api-key]` in logs, in `var_dump()` output, in JSON responses, and in REST API calls. The actual value only surfaces when you call `->reveal()` on it explicitly. Serializing or cloning the object throws an exception.

Under the hood, WordPress uses XChaCha20-Poly1305 (libsodium) with a four-tier key hierarchy. The root key is stored encrypted, derived from a `WP_SECRETS_KEY` constant you add to `wp-config.php`. The encrypted credential sits in the options table — same location as before — but now it is ciphertext, not a string your database admin can read.

Two version slots exist per secret: `CURRENT` and `PREVIOUS`. Writing a new value moves the old one to `PREVIOUS`, giving you credential rotation without a service interruption window.

## The Four Functions You Need

The API surface is narrow by design. The full [official documentation is on Make WordPress Core](https://make.wordpress.org/core/2026/11/11/the-secrets-api-in-wordpress-7-2/).

- `wp_set_secret( 'my-plugin/api-key', $value )` — stores encrypted; returns`true` or`WP_Error`
- `wp_get_secret( 'my-plugin/api-key' )` — retrieves a`WP_Secret` object,`null` , or`WP_Error`
- `wp_delete_secret( 'my-plugin/api-key' )` — removes the stored credential
- `wp_import_option_as_secret( 'old_option_name', 'my-plugin/api-key' )` — migrates existing plaintext options

Secret names follow the convention `plugin-slug/key-name` — lowercase letters, digits, hyphens, and underscores, single slash, no leading or trailing hyphens. Names without a slash trigger `_doing_it_wrong()`. The slash is namespacing, not access control: any plugin on the site can read any secret.

## Write Your Plugin’s Getter Correctly

The right pattern uses feature detection so your plugin runs on 7.1 and earlier while taking advantage of the new API on 7.2+:

```
function my_plugin_get_api_key(): string {
    // Allow ops to override via wp-config.php
    if ( defined( 'MY_PLUGIN_API_KEY' ) && MY_PLUGIN_API_KEY ) {
        return (string) MY_PLUGIN_API_KEY;
    }

    // Use Secrets API on WordPress 7.2+
    if ( function_exists( 'wp_get_secret' ) ) {
        $secret = wp_get_secret( 'my-plugin/api-key' );
        if ( $secret instanceof WP_Secret ) {
            return (string) $secret->reveal();
        }
        // WP_Error means the secret exists but cannot be decrypted.
        // Do NOT fall back to get_option() here.
        if ( is_wp_error( $secret ) ) {
            return '';
        }
    }

    // Legacy fallback for pre-7.2 installs
    return (string) get_option( 'my_plugin_api_key', '' );
}
```

The return value of `wp_get_secret()` has three states, not two, and you must handle all three. `WP_Secret` means success. `null` means the secret was never stored — show your setup screen. `WP_Error` means the secret exists but cannot be decrypted, usually because the encryption key changed. Falling back to `get_option()` on a `WP_Error` is the worst possible mistake: you silently bypass encryption for a credential that was deliberately migrated away from plain-text storage.

## Migrating Existing Plugins

`wp_import_option_as_secret()` handles the common case: you have an option with a plaintext key, and you want to move it to encrypted storage without requiring users to re-enter it:

```
function my_plugin_migrate_to_secrets_api(): void {
    if ( ! function_exists( 'wp_import_option_as_secret' ) ) {
        return;
    }

    $legacy = get_option( 'my_plugin_api_key' );
    if ( ! is_string( $legacy ) || '' === $legacy ) {
        return;
    }

    $result = wp_import_option_as_secret( 'my_plugin_api_key', 'my-plugin/api-key' );
    if ( is_wp_error( $result ) ) {
        return;
    }

    // Verify the migration before deleting the original
    $secret = wp_get_secret( 'my-plugin/api-key' );
    if ( $secret instanceof WP_Secret && hash_equals( $legacy, (string) $secret->reveal() ) ) {
        delete_option( 'my_plugin_api_key' );
    }
}
add_action( 'admin_init', 'my_plugin_migrate_to_secrets_api' );
```

Run it on `admin_init`. Existing installs migrate silently on the first admin page load after upgrading to 7.2. The `hash_equals()` check is not paranoia — it is the guard against deleting the legacy option before confirming the encrypted copy matches.

## What Changed in Beta 5 (Breaking)

If you tested against an earlier beta, re-test now. [Beta 5 (November 10) introduced two breaking changes](https://developer.wordpress.org/news/2026/11/whats-new-for-developers-november-2026/) that affect anyone who already built against the API.

First, `WP_Secrets_Keyring`, `WP_Secrets_Store`, and `WP_Secrets_Provider` are now abstract classes, not interfaces. Any `secrets.php` drop-in that uses `implements WP_Secrets_Keyring` will throw a fatal error on 7.2. Update to `extends`.

Second, `WP_SECRETS_KEY` in `wp-config.php` is now mandatory. The earlier fallback to `LOGGED_IN_KEY` and `LOGGED_IN_SALT` is gone. If you have secrets stored under the old fallback key on a beta site, you need to perform a three-step re-encryption sequence before RC1. The [WordPress hosting team has the procedure](https://make.wordpress.org/hosting/2026/11/12/call-to-test-wordpress-7-2-secrets-api/).

New installs get `WP_SECRETS_KEY` generated automatically. Existing sites will see a “Recommended” notice in Site Health until it is added. Generate the key with:

```
php -r 'echo base64_encode( random_bytes( 32 ) ), "\n";'
```

## Start Now

RC1 lands in two days. The final release ships December 8. The adoption path is clear: add `function_exists()` guards, handle all three return states, run the import on `admin_init`. The options table was never a secrets manager. Everyone knew it. Nobody fixed it. WordPress 7.2 fixes it — use it.
