• 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 build custom ACF Pro dynamic fields extensions utilizing modern Rewrite API custom endpoints schemas

How to build custom ACF Pro dynamic fields extensions utilizing modern Rewrite API custom endpoints schemas

Leveraging the Rewrite API for Advanced ACF Pro Dynamic Field Extensions

Customizing Advanced Custom Fields (ACF) Pro beyond its built-in dynamic field capabilities often requires deeper integration with WordPress’s core functionalities. While ACF offers hooks for modifying field values and choices, creating entirely new dynamic field *types* or complex data-driven field behaviors necessitates a more robust approach. This post details how to architect and implement custom ACF Pro dynamic field extensions by leveraging the WordPress Rewrite API to create custom REST API endpoints, enabling sophisticated data retrieval and manipulation for your fields.

Understanding the Need for Custom Endpoints

ACF Pro’s dynamic field features, such as “Dynamically Populate Choices,” are powerful for populating select fields, radio buttons, and checkboxes from custom queries or external data sources. However, these features are primarily designed for static data retrieval during the rendering of the post edit screen. When you need:

  • Real-time data fetching that updates field options without a page reload (e.g., live search suggestions).
  • Complex data filtering and sorting logic that is too intricate for simple ACF query parameters.
  • Integration with external APIs where authentication or complex request/response handling is required.
  • Dynamic field behavior that depends on user roles or other contextual information not directly available to ACF’s choice population hooks.
  • Creating entirely new field types that require programmatic data generation or manipulation.

…you’ll find the standard ACF dynamic population methods insufficient. This is where the WordPress Rewrite API and its integration with the REST API become invaluable. By creating custom REST API endpoints, we can expose a structured data interface that ACF fields can query asynchronously.

Architecting the Solution: Plugin Structure and REST API Registration

We’ll build a custom WordPress plugin to house our ACF extension. This plugin will register a custom REST API route using register_rest_route(). This function is the cornerstone of extending the WordPress REST API and allows us to define our own endpoints, specify their methods (GET, POST, etc.), and hook in callback functions to handle requests.

Consider a scenario where we need an ACF field to dynamically populate with a list of products from a custom post type, with advanced filtering capabilities (e.g., by category, stock status, and price range). We’ll create an endpoint like /my-plugin/v1/products.

Plugin Initialization and REST Route Registration

The core of our plugin will involve hooking into the rest_api_init action. This action fires when the REST API is being initialized, providing the perfect opportunity to register our custom routes.

my-acf-dynamic-fields.php (Main Plugin File)

<?php
/**
 * Plugin Name: My ACF Dynamic Fields
 * Description: Extends ACF Pro with custom dynamic fields using REST API endpoints.
 * Version: 1.0.0
 * Author: Your Name
 * Text Domain: my-acf-dynamic-fields
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

/**
 * Register custom REST API routes.
 */
function my_acf_dynamic_fields_register_routes() {
    // Register the products endpoint.
    register_rest_route( 'my-acf-dynamic-fields/v1', '/products', array(
        'methods'  => WP_REST_Server::READABLE, // Equivalent to 'GET'
        'callback' => 'my_acf_dynamic_fields_get_products',
        'permission_callback' => '__return_true', // For simplicity, allow public access. Adjust for security.
        'args'     => array(
            'search' => array(
                'required'          => false,
                'type'              => 'string',
                'sanitize_callback' => 'sanitize_text_field',
                'description'       => 'Search term for product name.',
            ),
            'category' => array(
                'required'          => false,
                'type'              => 'string',
                'sanitize_callback' => 'sanitize_text_field',
                'description'       => 'Product category slug.',
            ),
            'in_stock' => array(
                'required'          => false,
                'type'              => 'boolean',
                'description'       => 'Filter for products in stock.',
            ),
            'min_price' => array(
                'required'          => false,
                'type'              => 'number',
                'description'       => 'Minimum price filter.',
            ),
            'max_price' => array(
                'required'          => false,
                'type'              => 'number',
                'description'       => 'Maximum price filter.',
            ),
            'limit' => array(
                'required'          => false,
                'type'              => 'integer',
                'default'           => 20,
                'minimum'           => 1,
                'maximum'           => 100,
                'description'       => 'Number of results to return.',
            ),
            'page' => array(
                'required'          => false,
                'type'              => 'integer',
                'default'           => 1,
                'minimum'           => 1,
                'description'       => 'Page number for pagination.',
            ),
        ),
    ) );
}
add_action( 'rest_api_init', 'my_acf_dynamic_fields_register_routes' );

/**
 * Callback function to retrieve products.
 *
 * @param WP_REST_Request $request Full data about the request.
 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
 */
function my_acf_dynamic_fields_get_products( WP_REST_Request $request ) {
    $args = array(
        'post_type'      => 'product', // Assuming 'product' is your custom post type slug.
        'post_status'    => 'publish',
        'posts_per_page' => $request->get_param( 'limit' ),
        'paged'          => $request->get_param( 'page' ),
        'meta_query'     => array(
            'relation' => 'AND',
        ),
        'tax_query'      => array(
            'relation' => 'AND',
        ),
    );

    // Apply search term.
    $search_term = $request->get_param( 'search' );
    if ( ! empty( $search_term ) ) {
        // This is a basic search. For more advanced search, consider WP_Query's 's' parameter or a dedicated search plugin.
        $args['s'] = $search_term;
    }

    // Apply category filter.
    $category_slug = $request->get_param( 'category' );
    if ( ! empty( $category_slug ) ) {
        $args['tax_query'][] = array(
            'taxonomy' => 'product_category', // Assuming 'product_category' is your taxonomy slug.
            'field'    => 'slug',
            'terms'    => $category_slug,
        );
    }

    // Apply in_stock filter.
    $in_stock = $request->get_param( 'in_stock' );
    if ( $in_stock !== null ) { // Check for null as false is a valid boolean value.
        $stock_status = $in_stock ? 'instock' : 'outofstock';
        $args['meta_query'][] = array(
            'key'     => '_stock_status', // Example meta key for stock status.
            'value'   => $stock_status,
            'compare' => '=',
        );
    }

    // Apply price filters.
    $min_price = $request->get_param( 'min_price' );
    $max_price = $request->get_param( 'max_price' );

    if ( $min_price !== null ) {
        $args['meta_query'][] = array(
            'key'     => '_price', // Example meta key for price.
            'value'   => $min_price,
            'type'    => 'NUMERIC',
            'compare' => '>=',
        );
    }
    if ( $max_price !== null ) {
        $args['meta_query'][] = array(
            'key'     => '_price', // Example meta key for price.
            'value'   => $max_price,
            'type'    => 'NUMERIC',
            'compare' => '<=',
        );
    }

    // Remove empty meta_query or tax_query if no filters were applied.
    if ( count( $args['meta_query'] ) === 1 ) { // Only the 'relation' key exists.
        unset( $args['meta_query'] );
    }
    if ( count( $args['tax_query'] ) === 1 ) { // Only the 'relation' key exists.
        unset( $args['tax_query'] );
    }

    $query = new WP_Query( $args );

    $products_data = array();
    if ( $query->have_posts() ) {
        while ( $query->have_posts() ) {
            $query->the_post();
            $product_id = get_the_ID();
            $products_data[] = array(
                'id'    => $product_id,
                'text'  => get_the_title(), // Or a more descriptive title/name.
                'value' => $product_id,    // The value to be stored in ACF.
                // Add more relevant data if needed by the frontend.
                'price' => get_post_meta( $product_id, '_price', true ),
                'stock' => get_post_meta( $product_id, '_stock_status', true ),
            );
        }
        wp_reset_postdata();
    }

    // Prepare response.
    $response_data = array(
        'products' => $products_data,
        'total_products' => $query->found_posts,
        'max_num_pages' => $query->max_num_pages,
    );

    $response = new WP_REST_Response( $response_data, 200 );
    $response->add_links( array(
        'self' => rest_url( 'my-acf-dynamic-fields/v1/products' ),
        // Add pagination links if needed.
    ) );

    return $response;
}

// Include other endpoint callbacks here if necessary.
// For example, a callback to get product categories.
function my_acf_dynamic_fields_get_categories( WP_REST_Request $request ) {
    $categories = get_terms( array(
        'taxonomy' => 'product_category', // Assuming 'product_category' is your taxonomy slug.
        'hide_empty' => true,
    ) );

    $categories_data = array();
    if ( ! is_wp_error( $categories ) ) {
        foreach ( $categories as $category ) {
            $categories_data[] = array(
                'text'  => $category->name,
                'value' => $category->slug,
            );
        }
    }

    return new WP_REST_Response( $categories_data, 200 );
}

function my_acf_dynamic_fields_register_category_route() {
    register_rest_route( 'my-acf-dynamic-fields/v1', '/categories', array(
        'methods'  => WP_REST_Server::READABLE,
        'callback' => 'my_acf_dynamic_fields_get_categories',
        'permission_callback' => '__return_true',
    ) );
}
add_action( 'rest_api_init', 'my_acf_dynamic_fields_register_category_route' );

In this code:

  • We define a custom route /my-acf-dynamic-fields/v1/products.
  • WP_REST_Server::READABLE specifies that this endpoint responds to GET requests.
  • my_acf_dynamic_fields_get_products is the callback function that will handle incoming requests to this endpoint.
  • The args array defines the expected query parameters, their types, whether they are required, and importantly, their sanitize_callback. This is crucial for security.
  • The permission_callback is set to __return_true for simplicity. In a production environment, you would implement robust checks here (e.g., checking user capabilities, nonces, or API keys).
  • The my_acf_dynamic_fields_get_products function constructs a WP_Query based on the sanitized parameters.
  • It iterates through the results, formatting them into an array suitable for an ACF field (typically with 'text' for display and 'value' for storage).
  • The response is wrapped in a WP_REST_Response object, which includes the data and an HTTP status code.
  • We also added a /categories endpoint as an example of how to fetch taxonomy terms.

Integrating with ACF Pro Fields

Now that we have our REST API endpoints set up, we need to configure ACF Pro fields to utilize them. This is typically done within the ACF Field Group editor. For fields like “Select,” “Checkbox,” “Radio Button,” or “Button Group,” you can enable “Dynamically Populate Choices.”

Configuring a “Select” Field for Dynamic Product Population

When editing a field group in ACF Pro, locate the field you want to make dynamic (e.g., a “Select” field named “Related Product”). Enable the “Dynamically Populate Choices” option. This will reveal several new settings:

ACF Field Settings (UI Description)

  • Filter by Post Type: Set this to “None” as we are using a custom endpoint.
  • Post Type: Leave blank.
  • Taxonomy: Leave blank.
  • Post Status: Leave blank.
  • Post Order: Leave blank.
  • Dynamically Populate Choices: Yes.
  • Query Type: Select “Custom Endpoint”.
  • Endpoint URL: Enter the full URL to your custom endpoint. For example: [site_url]/wp-json/my-acf-dynamic-fields/v1/products. You can use the [site_url] placeholder, which ACF will resolve.
  • Field Type: Select “Select”.
  • Value Format: This determines what part of the JSON response is used for the field’s value. Typically, this will be value (matching our API output).
  • Label Format: This determines what part of the JSON response is used for the field’s display text. Typically, this will be text (matching our API output).
  • Return Format: Choose how the value should be saved (e.g., “Value”).
  • Allow Null: Yes/No.
  • Placeholder: Optional placeholder text.

ACF Pro will automatically append the query parameters defined in your REST API route to the Endpoint URL when it makes the request. For instance, if you add a “Search” field to your ACF field group (e.g., a text input field), ACF will automatically send its value as the search parameter to your /products endpoint.

Handling Dynamic Search and Filtering in ACF

ACF Pro’s dynamic population feature includes built-in support for search inputs. When you add a text input field to your ACF field group and configure your dynamic field to use it for searching, ACF will automatically pass the input’s value as a query parameter to your endpoint. You need to ensure your endpoint’s args definition matches the parameter name ACF sends (e.g., search).

For more complex filtering (like dropdowns for categories or checkboxes for stock status), you’ll need to:

  • Add additional ACF fields (e.g., a “Select” field for categories, populated by our /categories endpoint).
  • Use ACF’s acf/load_field filter to dynamically modify the endpoint_args for your dynamic field based on the values of these other filter fields.

Example: Modifying Endpoint Arguments with acf/load_field

<?php
/**
 * Modify endpoint arguments for the 'Related Product' field based on filter fields.
 *
 * @param array $field The field array.
 * @return array The modified field array.
 */
function my_acf_dynamic_fields_modify_product_endpoint_args( $field ) {
    // Ensure this is the correct field we want to modify.
    if ( 'select' === $field['type'] && 'Related Product' === $field['label'] ) {

        // Get the value of the 'product_category' filter field.
        $category_slug = get_field( 'product_category_filter' ); // Assuming you have a field named 'product_category_filter'.

        // Get the value of the 'product_stock_status' filter field.
        $stock_status_value = get_field( 'product_stock_status_filter' ); // Assuming a field that returns 'instock' or 'outofstock'.

        // Initialize endpoint_args if it doesn't exist.
        if ( ! isset( $field['endpoint_args'] ) || ! is_array( $field['endpoint_args'] ) ) {
            $field['endpoint_args'] = array();
        }

        // Add category argument if a category is selected.
        if ( ! empty( $category_slug ) ) {
            $field['endpoint_args']['category'] = $category_slug;
        }

        // Add stock status argument.
        if ( ! empty( $stock_status_value ) ) {
            // ACF sends boolean for checkboxes, but our API expects string 'instock'/'outofstock'.
            // We need to map this. If the filter field is a select/radio, it might directly return the string.
            // Adjust this logic based on your filter field's return format.
            $field['endpoint_args']['in_stock'] = ( 'instock' === $stock_status_value );
        }

        // You can also dynamically set the endpoint URL if needed.
        // $field['endpoint_url'] = '[site_url]/wp-json/my-acf-dynamic-fields/v1/products';
    }
    return $field;
}
add_filter( 'acf/load_field/name=related_product', 'my_acf_dynamic_fields_modify_product_endpoint_args' ); // Replace 'related_product' with your field's actual name.
// Or use 'acf/load_field' to target by type or other properties if needed.
// add_filter( 'acf/load_field', 'my_acf_dynamic_fields_modify_product_endpoint_args' );

In this example:

  • We hook into acf/load_field, targeting a specific field by its name ('related_product').
  • Inside the callback, we retrieve the values from other ACF fields ('product_category_filter' and 'product_stock_status_filter') using get_field().
  • We then append these values as query parameters (category and in_stock) to the $field['endpoint_args'] array. ACF Pro automatically merges these arguments with the base endpoint_url.
  • Note the conversion of the stock status value. Ensure your filter field’s return format aligns with what your API expects.

Security Considerations

Exposing API endpoints, even for internal use, requires careful security considerations:

  • Authentication and Authorization: The permission_callback in register_rest_route is critical. For internal use, you might check user capabilities (e.g., current_user_can('edit_posts')). For external access, implement API keys, OAuth, or JWT.
  • Input Validation and Sanitization: Always sanitize all incoming data using WordPress functions like sanitize_text_field(), absint(), esc_url_raw(), etc. The args array in register_rest_route is the primary place for this.
  • Rate Limiting: Protect your endpoints from abuse by implementing rate limiting, especially if they are publicly accessible.
  • Data Exposure: Ensure your API only returns necessary data. Avoid exposing sensitive information.
  • Nonces: For actions that modify data (POST, PUT, DELETE), always use nonces to verify requests.

Advanced Use Cases and Further Development

This pattern opens doors to numerous advanced ACF extensions:

  • Real-time Autocomplete Fields: Use the search parameter for live suggestions as a user types.
  • Dependent Dynamic Fields: Chain multiple dynamic fields where the endpoint for one field depends on the selection of another. This can be achieved by using acf/load_field to dynamically set the endpoint_url or endpoint_args.
  • Custom Field Types: While this post focuses on populating choices, you could extend this to create entirely new field types by registering them with ACF and having their rendering logic fetch data from your custom endpoints.
  • Integration with External Services: Build endpoints that fetch data from third-party APIs, perform transformations, and return it in a format ACF can use.
  • Complex Data Visualization: Use custom endpoints to feed data into JavaScript-based charting libraries integrated within ACF’s flexible content fields or custom blocks.

By combining the power of the WordPress REST API and the flexibility of ACF Pro’s dynamic population, developers can build highly sophisticated and data-driven user interfaces within the WordPress admin, significantly enhancing content management workflows.

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

  • WordPress Development Recipe: Efficient binary storage and retrieval in custom tables using PHP 8.x Attributes
  • Step-by-Step Guide to building a custom XML sitemap generator block for Gutenberg using PHP block-render callbacks
  • WordPress Development Recipe: High-efficiency server-side rendering for Gutenberg blocks using PHP 8.x Attributes
  • How to securely integrate Firebase Realtime DB endpoints into WordPress custom plugins using WordPress Database Class ($wpdb)
  • Debugging and Resolving complex broken WP-Cron schedules issues during heavy concurrent database traffic

Categories

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

Recent Posts

  • WordPress Development Recipe: Efficient binary storage and retrieval in custom tables using PHP 8.x Attributes
  • Step-by-Step Guide to building a custom XML sitemap generator block for Gutenberg using PHP block-render callbacks
  • WordPress Development Recipe: High-efficiency server-side rendering for Gutenberg blocks using PHP 8.x Attributes

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (872)
  • Debugging & Troubleshooting (658)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Business & Monetization (390)

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