• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » How to implement custom WordPress Options API endpoints with token authentication in Gutenberg blocks

How to implement custom WordPress Options API endpoints with token authentication in Gutenberg blocks

Leveraging the WordPress REST API for Custom Options Management

While the WordPress Options API is fundamental for storing site-wide settings, directly exposing it via the REST API for dynamic, block-level interactions requires careful consideration of security and structure. This post details how to create custom REST API endpoints that interact with the Options API, secured with token authentication, and how to consume these endpoints from within Gutenberg blocks.

Setting Up Custom REST API Endpoints

We’ll define custom routes within the WordPress REST API to handle GET (retrieve) and POST (update) operations for specific options. This involves registering new routes using register_rest_route within a plugin or theme’s functions.php.

Consider a scenario where we need to manage a custom theme setting, such as a “hero section title.” We’ll create an endpoint to fetch this title and another to update it.

Registering the GET Endpoint

This endpoint will retrieve the value of a specific option. We’ll use the get_option() function to fetch the data.

add_action( 'rest_api_init', function () {
    register_rest_route( 'myplugin/v1', '/settings/(?P<key>[a-zA-Z0-9_]+)', array(
        'methods' => 'GET',
        'callback' => 'myplugin_get_setting',
        'permission_callback' => 'myplugin_rest_api_permissions_check',
    ) );
} );

function myplugin_get_setting( WP_REST_Request $request ) {
    $key = $request->get_param( 'key' );

    if ( ! $key ) {
        return new WP_Error( 'missing_param', 'Setting key is required.', array( 'status' => 400 ) );
    }

    // Sanitize the key to prevent unexpected behavior
    $sanitized_key = sanitize_key( $key );

    // Basic check to prevent access to sensitive options
    $allowed_keys = array( 'my_hero_title' ); // Whitelist specific options
    if ( ! in_array( $sanitized_key, $allowed_keys, true ) ) {
        return new WP_Error( 'invalid_key', 'Invalid setting key.', array( 'status' => 400 ) );
    }

    $value = get_option( $sanitized_key );

    return new WP_REST_Response( array( 'value' => $value ), 200 );
}

// Placeholder for permission callback (detailed below)
function myplugin_rest_api_permissions_check() {
    return true; // Replace with actual token validation
}

Registering the POST Endpoint

This endpoint will update the value of a specific option using update_option(). It expects the new value in the request body.

add_action( 'rest_api_init', function () {
    register_rest_route( 'myplugin/v1', '/settings/(?P<key>[a-zA-Z0-9_]+)', array(
        'methods' => 'POST',
        'callback' => 'myplugin_update_setting',
        'permission_callback' => 'myplugin_rest_api_permissions_check',
        'args' => array(
            'value' => array(
                'required' => true,
                'sanitize_callback' => 'sanitize_text_field', // Example sanitization
            ),
        ),
    ) );
} );

function myplugin_update_setting( WP_REST_Request $request ) {
    $key = $request->get_param( 'key' );
    $value = $request->get_param( 'value' );

    if ( ! $key ) {
        return new WP_Error( 'missing_param', 'Setting key is required.', array( 'status' => 400 ) );
    }

    // Sanitize the key to prevent unexpected behavior
    $sanitized_key = sanitize_key( $key );

    // Basic check to prevent access to sensitive options
    $allowed_keys = array( 'my_hero_title' ); // Whitelist specific options
    if ( ! in_array( $sanitized_key, $allowed_keys, true ) ) {
        return new WP_Error( 'invalid_key', 'Invalid setting key.', array( 'status' => 400 ) );
    }

    // Sanitize the value based on expected type. For text, sanitize_text_field is good.
    // For other types (e.g., numbers, booleans), use appropriate sanitization.
    $sanitized_value = sanitize_text_field( $value );

    $updated = update_option( $sanitized_key, $sanitized_value );

    if ( $updated ) {
        return new WP_REST_Response( array( 'success' => true, 'message' => 'Setting updated successfully.' ), 200 );
    } else {
        return new WP_Error( 'update_failed', 'Failed to update setting.', array( 'status' => 500 ) );
    }
}

Implementing Token Authentication

Directly exposing API endpoints without authentication is a significant security risk. We’ll implement a simple token-based authentication mechanism. This involves generating a unique token, storing it securely (e.g., in wp-config.php or a custom plugin setting), and validating it on each API request.

Generating and Storing the Token

A robust way to store sensitive secrets like API tokens is in wp-config.php. This file is not typically committed to version control and is loaded very early in the WordPress bootstrap process.

// In wp-config.php
define( 'MYPLUGIN_API_TOKEN', 'your_super_secret_and_long_api_token_here' );

Alternatively, for more dynamic token management, you could store it as a WordPress option, but ensure it’s protected and not easily discoverable.

Validating the Token

We’ll modify the myplugin_rest_api_permissions_check function to validate the token. The token will be passed in the `Authorization` header as a Bearer token.

function myplugin_rest_api_permissions_check( WP_REST_Request $request ) {
    // Retrieve the token from the Authorization header
    $auth_header = $request->get_header( 'authorization' );

    if ( empty( $auth_header ) ) {
        return new WP_Error( 'rest_not_logged_in', 'Authentication token is missing.', array( 'status' => 401 ) );
    }

    // Expecting "Bearer YOUR_TOKEN"
    list( $token_type, $token ) = explode( ' ', $auth_header, 2 );

    if ( 'Bearer' !== $token_type || empty( $token ) ) {
        return new WP_Error( 'rest_invalid_token_format', 'Invalid token format. Expected "Bearer YOUR_TOKEN".', array( 'status' => 401 ) );
    }

    // Retrieve the expected token from wp-config.php
    if ( ! defined( 'MYPLUGIN_API_TOKEN' ) ) {
        // This should ideally not happen if defined correctly in wp-config.php
        error_log( 'MYPLUGIN_API_TOKEN is not defined.' );
        return new WP_Error( 'rest_server_error', 'API token configuration error.', array( 'status' => 500 ) );
    }

    $expected_token = MYPLUGIN_API_TOKEN;

    // Compare the provided token with the expected token
    if ( ! hash_equals( $expected_token, $token ) ) {
        return new WP_Error( 'rest_invalid_token', 'Invalid authentication token.', array( 'status' => 401 ) );
    }

    // If the token is valid, allow access
    return true;
}

Consuming the API from Gutenberg Blocks

Now, let’s integrate these endpoints into a Gutenberg block. We’ll create a simple block that displays and allows editing of the “hero section title.” This involves using JavaScript (React) within the block’s editor and frontend components.

Block Registration and Editor Component

First, register your block using @wordpress/scripts or a similar build tool. The core logic will reside in your block’s JavaScript file.

// src/index.js (or your block's main JS file)
import { registerBlockType } from '@wordpress/blocks';
import { Edit } from './edit';
import { save } from './save';
import './style.scss'; // For frontend styles
import './editor.scss'; // For editor styles

registerBlockType( 'myplugin/hero-settings', {
    title: 'Hero Settings',
    icon: 'admin-site-alt3',
    category: 'widgets',
    edit: Edit,
    save,
} );

The Editor Component (edit.js)

In the edit.js file, we’ll fetch the current hero title using our GET endpoint and provide an input field to update it using our POST endpoint. We’ll use the @wordpress/data store for managing the block’s state and @wordpress/api-fetch for making REST API calls.

// src/edit.js
import { __ } from '@wordpress/i18n';
import { useBlockProps, RichText } from '@wordpress/block-editor';
import { useState, useEffect } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';

// IMPORTANT: Ensure this token is securely managed and accessible in your frontend JS.
// For production, consider a nonce or a more sophisticated token management system.
// For this example, we'll assume it's available globally or passed via wp_localize_script.
const API_TOKEN = 'your_super_secret_and_long_api_token_here'; // Replace with actual token retrieval

export const Edit = ( { attributes, setAttributes } ) => {
    const blockProps = useBlockProps();
    const [ heroTitle, setHeroTitle ] = useState( '' );
    const [ isLoading, setIsLoading ] = useState( true );
    const [ error, setError ] = useState( null );

    const SETTINGS_API_URL = '/wp-json/myplugin/v1/settings/my_hero_title';

    // Fetch initial title when the block is loaded
    useEffect( () => {
        const fetchTitle = async () => {
            try {
                const response = await apiFetch( {
                    path: SETTINGS_API_URL,
                    method: 'GET',
                    headers: {
                        'Authorization': `Bearer ${ API_TOKEN }`,
                    },
                } );
                setHeroTitle( response.value || '' );
                setAttributes( { title: response.value || '' } ); // Update block attribute
            } catch ( err ) {
                console.error( 'Error fetching hero title:', err );
                setError( 'Could not load hero title.' );
            } finally {
                setIsLoading( false );
            }
        };
        fetchTitle();
    }, [] );

    // Handle title changes from the input field
    const handleTitleChange = async ( newTitle ) => {
        setHeroTitle( newTitle );
        setAttributes( { title: newTitle } ); // Optimistically update block attribute

        try {
            await apiFetch( {
                path: SETTINGS_API_URL,
                method: 'POST',
                headers: {
                    'Authorization': `Bearer ${ API_TOKEN }`,
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify( { value: newTitle } ),
            } );
            // Success feedback could be added here
        } catch ( err ) {
            console.error( 'Error updating hero title:', err );
            setError( 'Could not save hero title.' );
            // Revert optimistic update if save fails
            setHeroTitle( attributes.title );
        }
    };

    if ( isLoading ) {
        return (
            <div { ...blockProps }>
                { __( 'Loading hero settings...', 'myplugin' ) }
            </div>
        );
    }

    if ( error ) {
        return (
            <div { ...blockProps }>
                { error }
            </div>
        );
    }

    return (
        <div { ...blockProps }>
            <h3>{ __( 'Hero Section Settings', 'myplugin' ) }</h3>
            <label htmlFor="hero-title-input">{ __( 'Hero Title:', 'myplugin' ) }</label>
            <RichText
                tagName="div"
                id="hero-title-input"
                value={ heroTitle }
                onChange={ handleTitleChange }
                placeholder={ __( 'Enter hero title...', 'myplugin' ) }
                allowedFormats={ [ 'core/bold', 'core/italic' ] }
                className="hero-title-editor"
            />
        </div>
    );
};

Frontend Rendering (save.js)

The save.js function determines how the block is rendered on the frontend. Since we are managing the hero title via the API and not storing it directly as a block attribute that gets saved to post content, the save function should render a placeholder or fetch the latest value. A common pattern is to save a placeholder and then use JavaScript on the frontend to fetch and render the dynamic content.

// src/save.js
import { useBlockProps, RichText } from '@wordpress/block-editor';

export const save = ( { attributes } ) => {
    const blockProps = useBlockProps.save();
    // The 'title' attribute here is what's saved in post_content.
    // For dynamic content, we might not save anything here, or save a placeholder.
    // A better approach for truly dynamic content is to fetch it client-side on the frontend.
    // For simplicity, let's assume we save the title and it's updated by the editor.
    // If you want it to *always* be dynamic, the save function could return null or a placeholder div.
    return (
        <div { ...blockProps }>
            <RichText.Content
                tagName="h2" // Or whatever tag is appropriate for your hero title
                value={ attributes.title }
            />
        </div>
    );
};

Important Note on Frontend Rendering: If the hero title is meant to be *always* dynamic and reflect the latest saved option (even if the post content itself isn’t updated), the save function should return null or a simple placeholder div. Then, a separate frontend JavaScript file (enqueued for the frontend) would use apiFetch to get the latest title and render it into that placeholder. This ensures the frontend always shows the most current setting.

Enqueuing Scripts and Localizing Data

You need to enqueue your block’s JavaScript and make the API token available to it. For security, avoid directly embedding sensitive tokens in JS. Instead, use wp_localize_script to pass non-sensitive data or a nonce that can be used to fetch a temporary token.

// In your plugin's main file or functions.php
add_action( 'enqueue_block_editor_assets', function() {
    wp_enqueue_script(
        'myplugin-hero-settings-editor-script',
        plugins_url( 'build/index.js', __FILE__ ), // Path to your compiled JS
        array( 'wp-blocks', 'wp-element', 'wp-data', 'wp-api-fetch', 'wp-editor', 'wp-i18n' ),
        filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' )
    );

    // Localize data, including the API token (use with caution, see security notes)
    wp_localize_script(
        'myplugin-hero-settings-editor-script',
        'myplugin_block_data',
        array(
            'api_token' => defined( 'MYPLUGIN_API_TOKEN' ) ? MYPLUGIN_API_TOKEN : '',
            // In a real-world scenario, consider passing a nonce here
            // and fetching the actual token server-side via another endpoint.
        )
    );
} );

// Enqueue frontend script if dynamic rendering is needed
add_action( 'wp_enqueue_scripts', function() {
    // Register and enqueue your frontend script here if needed
    // wp_enqueue_script( 'myplugin-hero-settings-frontend', plugins_url( 'build/frontend.js', __FILE__ ), array( 'wp-api-fetch' ), filemtime( plugin_dir_path( __FILE__ ) . 'build/frontend.js' ) );
    // wp_localize_script( 'myplugin-hero-settings-frontend', 'myplugin_block_data', array( 'api_token' => defined( 'MYPLUGIN_API_TOKEN' ) ? MYPLUGIN_API_TOKEN : '' ) );
} );

Security Considerations and Best Practices

Token Security: Storing the token directly in wp-config.php is better than hardcoding in PHP files, but it’s still a sensitive secret. Ensure your wp-config.php file has restrictive file permissions (e.g., 600 or 400) and is not publicly accessible. For higher security, consider using environment variables or a secrets management system.

Token Distribution to Frontend: Directly passing the API token to the frontend JavaScript via wp_localize_script is convenient but exposes the token in the page source. This is acceptable if the token is intended for public consumption or if the risk is mitigated (e.g., the token only grants access to non-sensitive read operations). For sensitive write operations, a better approach is:

  • Use a nonce: Register a REST API endpoint that issues a temporary, time-limited nonce upon successful authentication (e.g., logged-in user with specific capabilities).
  • Pass the nonce to the frontend.
  • The block’s JavaScript uses the nonce to authenticate requests to your custom settings endpoints.
  • The server-side permission callback verifies the nonce and the user’s capabilities.

Sanitization and Validation: Always sanitize and validate all input received by your REST API endpoints, both parameters and data from the request body. Use WordPress’s built-in sanitization functions (e.g., sanitize_text_field, sanitize_key, absint) and validation callbacks within register_rest_route arguments.

Whitelisting Options: As demonstrated, explicitly whitelist the option keys that your API endpoints can access. Do not allow arbitrary option keys to be read or written, as this could lead to unintended data loss or security vulnerabilities.

Error Handling: Implement comprehensive error handling on both the server and client sides. Return meaningful error messages and appropriate HTTP status codes (e.g., 400 for bad requests, 401 for unauthorized, 403 for forbidden, 500 for server errors).

Conclusion

By combining custom REST API endpoints with token authentication and careful frontend integration, you can effectively extend the WordPress Options API to support dynamic settings management directly within Gutenberg blocks. This approach provides a powerful and flexible way to build interactive plugin settings and theme options, enhancing the user experience for content creators.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store
  • How to refactor legacy event ticket registers queries using modern WP_Query and custom Transient caching
  • Step-by-Step Guide: Offloading high-frequency member profile directories metadata writes to a Redis KV store

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (662)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (873)
  • PHP (5)
  • PHP Development (49)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (647)
  • SEO & Growth (492)
  • Server (118)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (726)
  • WordPress Theme Development (357)

Recent Posts

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (873)
  • WordPress Plugin Development (726)
  • Debugging & Troubleshooting (662)
  • Security & Compliance (647)
  • SEO & Growth (492)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala