Build a WordPress plugin options page with the Settings API
A WordPress plugin usually needs a few site-specific settings: an API key, a sync switch, a batch size or an account identifier. Building those controls directly with update_option() leaves you responsible for form markup, nonces, validation and the redirect after saving. The WordPress Settings API supplies that plumbing, so a small plugin can offer a familiar administration screen without inventing its own settings framework.
The useful design is to register one option, describe its fields and let options.php handle the write. On the front end, the plugin reads the stored value through get_option() and treats missing or malformed data as a recoverable condition. This keeps the option name, sanitisation rules and capability requirements in one place, making the page easier to test, extend and maintain.
Registering the option group and fields
Begin with a data contract rather than a visual design. Decide what the plugin must remember, then give the option a namespaced name such as wpqa_settings. The option group can use a separate identifier, such as wpqa_settings_group, because WordPress uses it when generating the form action and nonce. Prefixing both names reduces the chance of colliding with another plugin that uses a generic key such as api_key.
add_action( 'admin_init', 'wpqa_register_settings' );
function wpqa_register_settings() {
register_setting(
'wpqa_settings_group',
'wpqa_settings',
array(
'type' => 'array',
'default' => array(
'api_key' => '',
'sync_enabled' => false,
'posts_per_page' => 10,
),
'sanitize_callback' => 'wpqa_sanitize_settings',
)
);
add_settings_section( 'wpqa_api', 'API connection', '__return_true', 'wpqa-options' );
add_settings_field( 'api_key', 'API key', 'wpqa_api_key_field', 'wpqa-options', 'wpqa_api' );
add_settings_field( 'sync_enabled', 'Enable sync', 'wpqa_sync_checkbox', 'wpqa-options', 'wpqa_api' );
add_settings_field( 'posts_per_page', 'Posts per batch', 'wpqa_batch_field', 'wpqa-options', 'wpqa_api' );
}
Registration belongs on admin_init, which runs inside the administration area. register_setting() identifies the permitted option and names the callback that will clean incoming data. add_settings_section() creates a visual grouping, while add_settings_field() connects a label and renderer to a section. The field callbacks in this example are intentionally small, but each one must be defined by the plugin.
Storing related values in one array is usually preferable to scattering them across separate records. It gives the plugin a consistent shape, permits a sanitiser to validate the values together and makes later migrations easier. The trade-off is that every field must use array notation in the form, and every consumer must safely handle a missing or incomplete array.
Rendering the administration page
Register the administration callback on admin_menu with add_options_page(). Give it a capability such as manage_options, which is normally reserved for administrators. A plugin that manages a site should use a suitably restrictive capability rather than assuming every logged-in user should see its integration settings.
function wpqa_render_options_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'You do not have permission to access this page.' );
}
echo '<div class="wrap">';
echo '<h1>' . esc_html( get_admin_page_title() ) . '</h1>';
echo '<form action="options.php" method="post">';
settings_fields( 'wpqa_settings_group' );
do_settings_sections( 'wpqa-options' );
submit_button();
echo '</form>';
echo '</div>';
}
The form’s action must point to options.php, and settings_fields() must print the hidden option-page action and nonce. do_settings_sections() then renders the registered sections and their fields. The menu slug, wpqa-options in this example, must match the page identifier used during registration; changing one without the other leaves an empty screen. Keep the capability check inside the callback as well as in the menu definition, since direct requests can bypass the normal menu flow.
Each field callback should retrieve the current array, read its own key and escape the output when printing it:
function wpqa_api_key_field() {
$settings = get_option( 'wpqa_settings', array() );
$value = isset( $settings['api_key'] ) ? $settings['api_key'] : '';
echo '<input type="text" name="wpqa_settings[api_key]" value="' .
esc_attr( $value ) . '" class="regular-text" />';
}
Checkboxes need a submitted value such as 1 and a default of false in the sanitiser, because an unchecked box is absent from the request. The plugin’s public styling should remain independent of the administration theme. For agencies evaluating Elementor-based sites, this Neve theme review provides useful context for checking that a plugin does not interfere with the front-end layout.
Comparing storage approaches
Settings storage can be compared by how much configuration a plugin needs and who should edit it. A small integration usually benefits from the array-based option shown above. Larger systems may need separate records, a custom interface or access through the REST API. The right choice depends on update consistency, future compatibility and the level of control required.
| Approach | Best suited to | Main advantage | Main limitation |
|---|---|---|---|
| One option array | A cohesive set of plugin preferences | One read and one sanitising pass | Requires array-aware field names |
| Separate options | Independent values without dependencies | Very simple access to each value | More rows and weaker cross-field validation |
| Customizer controls | User-facing appearance preferences | Familiar live-preview interface | Unsuitable for secrets or privileged workflows |
| REST-backed settings | Headless or external application access | Structured, programmatic configuration | More schema, authentication and permission work |
An array option fits this page because all three values arrive together and can be validated as a set. Separate options are easier to read with get_option( 'wpqa_sync_enabled' ), but they introduce more database calls and require careful ordering when one setting depends on another. A Customizer works well for user-facing appearance choices, yet it is a poor home for privileged credentials or administrator-only controls.
REST-backed settings suit products that must expose configuration to another application, but they add schemas, permissions and authentication considerations. Whichever route you take, create a small accessor function that normalises defaults rather than repeating get_option() calls throughout the codebase. That function becomes the stable boundary between the stored representation and the rest of the plugin.
Sanitising values and handling updates
Sanitisation and validation solve different problems. Sanitisation makes a submitted value safe to store; validation decides whether the value is acceptable for the feature. Use a WordPress sanitising function such as sanitize_text_field(), sanitize_email(), esc_url_raw() or absint() as appropriate, then apply business limits such as a maximum batch size. Never print an unsanitised request value directly into an administration field.
function wpqa_sanitize_settings( $input ) {
$input = is_array( $input ) ? $input : array();
$output = array(
'api_key' => '',
'sync_enabled' => false,
'posts_per_page' => 10,
);
if ( isset( $input['api_key'] ) ) {
$output['api_key'] = sanitize_text_field( $input['api_key'] );
}
$output['sync_enabled'] = ! empty( $input['sync_enabled'] );
if ( isset( $input['posts_per_page'] ) ) {
$output['posts_per_page'] = min(
20,
max( 1, absint( $input['posts_per_page'] ) )
);
}
return $output;
}
The sanitising callback should always return a complete option array, even when the request is incomplete or malformed. This prevents an absent field from becoming an undefined index notice later in the plugin. Values such as a batch size also need a sensible default and upper bound. Clamping a number is usually more helpful to an administrator than refusing the entire form, but reject invalid email addresses or malformed URLs when those fields would cause a failed integration.
WordPress displays its standard settings-updated notice when the save succeeds. For field-specific feedback, call add_settings_error() during sanitisation and render the collected errors with settings_errors() on the form page. Keep messages actionable by naming the field and its accepted value. Since the option is written through options.php, test the complete round trip: incorrect input, a valid correction, an administrator without access and a plugin update that leaves the option untouched.
Australian deployment and release checks
Australian deployments add practical considerations beyond the API itself. For an operator handling customer information through an integration, assess whether the Privacy Act 1988 and the Australian Privacy Principles apply, and whether marketing workflows engage the Spam Act 2003. An options screen should collect only what the plugin needs, avoid displaying a secret in plaintext and avoid becoming a general personal-data collection form.
Time handling is another common source of support requests. A Sydney client using Australia/Sydney should let WordPress manage daylight-saving changes between AEST and AEDT, while the plugin stores timestamps in UTC and converts them for display. An Australian hosting region in Sydney can reduce latency for local visitors, but data-residency claims must be checked against the provider’s terms rather than inferred from a server label. WooCommerce integrations may also connect to local ERP systems and expose AUD prices with GST-inclusive totals, so their connection settings should allow the relevant account and region to be selected.
The local WordPress market spans agencies and freelancers in Melbourne, Brisbane, Perth and Adelaide, as well as contributors who attend community meetups in Sydney and Melbourne. Plugins built for that audience often need to work across different hosts, managed WordPress platforms and agency maintenance workflows. A final pass should confirm that the screen follows WordPress administration conventions, retains its values after an update and remains readable for keyboard users and screen readers. The practical WP-QA troubleshooting guidance can help structure that pass around reproducible checks rather than visual guesswork.
Release checklist for production readiness
- Register the option, fields and sanitiser on
admin_init, keeping the page slug and option group consistent. - Restrict the page to
manage_optionsor another deliberate capability, including a check inside the rendering callback. - Print every stored value with the correct escaping function, including
esc_attr()for inputs andesc_textarea()for multiline content. - Return a complete, validated option array from the sanitiser, with safe defaults and explicit bounds for numeric settings.
- Exercise the form as an administrator and an unauthorised user, including failed saves, empty values and restored options.
- Document the option name and migration plan so upgrades or multisite changes do not silently discard a site’s configuration.