• 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 WP HTTP API schemas

How to build custom ACF Pro dynamic fields extensions utilizing modern WP HTTP API schemas

Leveraging the WordPress HTTP API for Advanced ACF Pro Dynamic Field Extensions

Advanced Custom Fields (ACF) Pro offers powerful dynamic field capabilities, allowing developers to populate field choices, values, and even entire field structures programmatically. While ACF provides built-in methods for fetching data from local sources or simple APIs, integrating with complex, modern HTTP APIs often requires a more robust approach. This guide details how to build custom ACF Pro dynamic field extensions by effectively utilizing the WordPress HTTP API, focusing on schema adherence, error handling, and performance.

Understanding ACF Pro Dynamic Field Hooks

ACF Pro exposes several filters that enable dynamic field manipulation. The most relevant for fetching external data are:

  • acf/load_field: Allows modification of a field’s properties before it’s rendered. This is ideal for setting default values, changing labels, or conditionally hiding/showing fields.
  • acf/update_field: Used to intercept and modify field values before they are saved to the database.
  • acf/load_field_choices: Specifically designed to dynamically populate the choices for select, radio, and checkbox fields. This is our primary focus for API integrations.

Structuring Your Dynamic Field Extension

A well-structured extension will encapsulate API interaction logic, handle data transformation, and integrate seamlessly with ACF’s hooks. We’ll create a custom plugin to house this functionality.

Plugin Setup

Create a new directory in wp-content/plugins/, for example, acf-dynamic-api-fields, and add a main plugin file (e.g., acf-dynamic-api-fields.php).

/*
Plugin Name: ACF Dynamic API Fields
Plugin URI: https://example.com/
Description: Integrates ACF Pro dynamic fields with external APIs.
Version: 1.0.0
Author: Your Name
Author URI: https://example.com/
License: GPL-2.0+
License URI: http://www.gnu.org/licenses/gpl-2.0.txt
Text Domain: acf-dynamic-api-fields
*/

// Prevent direct access
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

// Define plugin constants
define( 'ACF_DYNAMIC_API_FIELDS_VERSION', '1.0.0' );
define( 'ACF_DYNAMIC_API_FIELDS_PATH', plugin_dir_path( __FILE__ ) );
define( 'ACF_DYNAMIC_API_FIELDS_URL', plugin_dir_url( __FILE__ ) );

// Include core functionality
require_once ACF_DYNAMIC_API_FIELDS_PATH . 'includes/class-api-field-manager.php';

/**
 * Initialize the plugin.
 */
function acf_dynamic_api_fields_init() {
    new ACF_Dynamic_API_Fields\API_Field_Manager();
}
add_action( 'plugins_loaded', 'acf_dynamic_api_fields_init' );

Implementing the API Field Manager

The core logic will reside in a class that manages the ACF hooks and interacts with the WordPress HTTP API. We’ll use the WP_Http class for making requests.

`class-api-field-manager.php`

<?php
/**
 * API Field Manager class.
 *
 * Handles dynamic field population from external APIs.
 */

namespace ACF_Dynamic_API_Fields;

// Prevent direct access
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class API_Field_Manager {

    /**
     * Constructor.
     */
    public function __construct() {
        // Hook into ACF to load field choices dynamically.
        // We use a lower priority to ensure ACF's core logic has run.
        add_action( 'acf/load_field_choices', array( $this, 'load_dynamic_choices' ), 10, 3 );
    }

    /**
     * Dynamically loads choices for ACF fields based on API data.
     *
     * @param array $field The field array.
     * @return void
     */
    public function load_dynamic_choices( &$field, $field_key, $post_id ) {

        // Check if the field is configured for dynamic API loading.
        // We'll use a custom 'api_source' setting in the ACF field configuration.
        if ( ! isset( $field['api_source'] ) || empty( $field['api_source'] ) ) {
            return;
        }

        // Retrieve API configuration from field settings.
        $api_config = $this->get_api_config( $field );

        if ( ! $api_config ) {
            // Log an error or provide a default message if config is invalid.
            $field['choices'] = array(
                '' => __( 'API configuration error', 'acf-dynamic-api-fields' ),
            );
            return;
        }

        // Fetch data from the API.
        $api_data = $this->fetch_api_data( $api_config );

        if ( is_wp_error( $api_data ) ) {
            // Handle API request errors.
            $field['choices'] = array(
                '' => sprintf( __( 'API Error: %s', 'acf-dynamic-api-fields' ), $api_data->get_error_message() ),
            );
            return;
        }

        // Transform and populate choices.
        $choices = $this->transform_api_data_to_choices( $api_data, $api_config );

        if ( empty( $choices ) ) {
            $field['choices'] = array(
                '' => __( 'No data found', 'acf-dynamic-api-fields' ),
            );
        } else {
            $field['choices'] = $choices;
        }
    }

    /**
     * Retrieves API configuration from field settings.
     *
     * @param array $field The ACF field array.
     * @return array|false Configuration array or false if invalid.
     */
    private function get_api_config( $field ) {
        $api_source = $field['api_source']; // e.g., 'https://api.example.com/items'

        // Basic validation for the API source URL.
        if ( ! filter_var( $api_source, FILTER_VALIDATE_URL ) ) {
            return false;
        }

        $config = array(
            'url'       => $api_source,
            'method'    => isset( $field['api_method'] ) ? strtoupper( $field['api_method'] ) : 'GET',
            'headers'   => array(),
            'body'      => null,
            'timeout'   => isset( $field['api_timeout'] ) ? intval( $field['api_timeout'] ) : 15, // Default timeout 15 seconds
            'data_path' => isset( $field['api_data_path'] ) ? $field['api_data_path'] : '', // e.g., 'data.items' for JSON
            'value_key' => isset( $field['api_value_key'] ) ? $field['api_value_key'] : 'id', // Key for the choice value
            'label_key' => isset( $field['api_label_key'] ) ? $field['api_label_key'] : 'name', // Key for the choice label
        );

        // Handle authentication if configured (e.g., API Key in header)
        if ( ! empty( $field['api_auth_header_key'] ) && ! empty( $field['api_auth_header_value'] ) ) {
            $config['headers'][ sanitize_text_field( $field['api_auth_header_key'] ) ] = sanitize_text_field( $field['api_auth_header_value'] );
        }

        // Handle custom headers
        if ( ! empty( $field['api_custom_headers'] ) ) {
            $custom_headers = json_decode( $field['api_custom_headers'], true );
            if ( is_array( $custom_headers ) ) {
                foreach ( $custom_headers as $key => $value ) {
                    $config['headers'][ sanitize_text_field( $key ) ] = sanitize_text_field( $value );
                }
            }
        }

        // Handle request body for POST/PUT requests
        if ( $config['method'] !== 'GET' && ! empty( $field['api_request_body'] ) ) {
            $config['body'] = $field['api_request_body'];
            // Assume JSON if Content-Type is not explicitly set and body is JSON-like
            if ( ! isset( $config['headers']['Content-Type'] ) ) {
                if ( is_array( json_decode( $field['api_request_body'], true ) ) || is_object( json_decode( $field['api_request_body'] ) ) ) {
                    $config['headers']['Content-Type'] = 'application/json';
                }
            }
        }

        // Ensure Content-Type is set for JSON bodies
        if ( isset( $config['headers']['Content-Type'] ) && strpos( $config['headers']['Content-Type'], 'json' ) !== false && is_string( $config['body'] ) ) {
             // Attempt to decode and re-encode to ensure valid JSON
            $decoded_body = json_decode( $config['body'] );
            if ( json_last_error() === JSON_ERROR_NONE ) {
                $config['body'] = json_encode( $decoded_body );
            } else {
                // If it's not valid JSON, maybe it's form data?
                // For simplicity, we'll assume JSON for now and let WP_Http handle it.
                // A more robust solution might inspect the body content.
            }
        }


        return $config;
    }

    /**
     * Fetches data from the specified API endpoint.
     *
     * @param array $config API configuration.
     * @return array|object|WP_Error The API response data or a WP_Error object.
     */
    private function fetch_api_data( $config ) {
        $args = array(
            'method'    => $config['method'],
            'timeout'   => $config['timeout'],
            'headers'   => $config['headers'],
            'body'      => $config['body'],
            'sslverify' => apply_filters( 'acf_dynamic_api_fields_sslverify', true ), // Allow SSL verification to be filtered
        );

        // Remove body if it's null and method is GET (or other methods that don't typically have bodies)
        if ( $args['body'] === null && in_array( $args['method'], array( 'GET', 'DELETE' ) ) ) {
            unset( $args['body'] );
        }

        $response = wp_remote_request( $config['url'], $args );

        if ( is_wp_error( $response ) ) {
            return $response; // Return the WP_Error object
        }

        $response_code = wp_remote_retrieve_response_code( $response );
        $response_body = wp_remote_retrieve_body( $response );

        if ( $response_code >= 400 ) {
            return new \WP_Error(
                'api_request_failed',
                sprintf(
                    __( 'API request failed with status %d: %s', 'acf-dynamic-api-fields' ),
                    $response_code,
                    $response_body // Consider sanitizing or truncating for security/display
                )
            );
        }

        // Attempt to decode JSON response.
        $decoded_body = json_decode( $response_body, true );

        if ( json_last_error() === JSON_ERROR_NONE ) {
            return $decoded_body;
        } else {
            // If not JSON, return raw body or an error.
            // For this example, we'll assume JSON is expected.
            return new \WP_Error(
                'invalid_json_response',
                __( 'API response is not valid JSON.', 'acf-dynamic-api-fields' )
            );
        }
    }

    /**
     * Transforms raw API data into an array of choices suitable for ACF.
     *
     * @param array|object $data       The decoded API response data.
     * @param array        $api_config The API configuration.
     * @return array An array of choices (value => label).
     */
    private function transform_api_data_to_choices( $data, $api_config ) {
        $choices = array();

        // Navigate through the data structure using the data_path.
        $data_items = $data;
        if ( ! empty( $api_config['data_path'] ) ) {
            $path_segments = explode( '.', $api_config['data_path'] );
            foreach ( $path_segments as $segment ) {
                if ( is_array( $data_items ) && isset( $data_items[ $segment ] ) ) {
                    $data_items = $data_items[ $segment ];
                } elseif ( is_object( $data_items ) && isset( $data_items->{ $segment } ) ) {
                    $data_items = $data_items->{ $segment };
                } else {
                    // Path not found.
                    return array();
                }
            }
        }

        // Ensure we have an array to iterate over.
        if ( ! is_array( $data_items ) ) {
            // If the data_path points to a single item, wrap it in an array.
            if ( ! empty( $api_config['data_path'] ) ) {
                 // This case might occur if data_path points to a single object,
                 // and we expect a list of choices. We might need to handle this
                 // more gracefully depending on API design. For now, assume it's an array.
                 return array();
            } else {
                // If no data_path, and the root is not an array, it's likely an error or unexpected format.
                return array();
            }
        }

        // Iterate and build choices.
        foreach ( $data_items as $item ) {
            $value = null;
            $label = null;

            // Extract value.
            if ( is_array( $item ) && isset( $item[ $api_config['value_key'] ] ) ) {
                $value = $item[ $api_config['value_key'] ];
            } elseif ( is_object( $item ) && isset( $item->{ $api_config['value_key'] } ) ) {
                $value = $item->{ $api_config['value_key'] };
            }

            // Extract label.
            if ( is_array( $item ) && isset( $item[ $api_config['label_key'] ] ) ) {
                $label = $item[ $api_config['label_key'] ];
            } elseif ( is_object( $item ) && isset( $item->{ $api_config['label_key'] } ) ) {
                $label = $item->{ $api_config['label_key'] };
            }

            // Ensure both value and label are found and are not empty.
            if ( $value !== null && $label !== null ) {
                // Sanitize labels for display.
                $choices[ (string) $value ] = sanitize_text_field( (string) $label );
            }
        }

        return $choices;
    }
}

Configuring ACF Fields for Dynamic Data

Within the ACF Field Group editor, you’ll configure your select, radio, or checkbox fields to use the dynamic data. Navigate to the field’s settings and under the “Conditional Logic” or “Advanced” tab (depending on ACF version and field type), you’ll find options to add custom attributes. We’ll use these to pass our API configuration.

Field Settings in ACF UI

For a field (e.g., a ‘Select’ field), you would add the following custom attributes:

  • API Source (api_source): The full URL of the API endpoint.
  • API Method (api_method): ‘GET’, ‘POST’, etc. (Defaults to ‘GET’).
  • API Data Path (api_data_path): A dot-notation path to the array of items within the JSON response (e.g., results.items). If the root of the JSON is the array, leave this blank.
  • API Value Key (api_value_key): The key within each item that holds the value for the choice (e.g., id).
  • API Label Key (api_label_key): The key within each item that holds the display label for the choice (e.g., name).
  • API Timeout (api_timeout): Request timeout in seconds (Defaults to 15).
  • API Auth Header Key (api_auth_header_key): For API key authentication, the header name (e.g., X-API-Key).
  • API Auth Header Value (api_auth_header_value): The corresponding value for the authentication header.
  • API Request Body (api_request_body): For POST/PUT requests, the JSON string or form data.
  • API Custom Headers (api_custom_headers): A JSON string of additional headers (e.g., {"Accept": "application/json"}).

To add these custom attributes, you typically need to enable “Custom Attributes” in the field’s “Advanced” settings. Then, you can add them as key-value pairs.

Advanced Considerations and Best Practices

Caching API Responses

Repeatedly fetching data from external APIs can be slow and resource-intensive, potentially leading to timeouts or exceeding API rate limits. Implement caching using WordPress Transients API.

// Inside API_Field_Manager::load_dynamic_choices()

// ... after getting $api_config ...

$cache_key = 'acf_dynamic_api_field_' . md5( json_encode( $api_config ) );
$cached_data = get_transient( $cache_key );

if ( $cached_data === false ) {
    // Data not in cache, fetch from API
    $api_data = $this->fetch_api_data( $api_config );

    if ( ! is_wp_error( $api_data ) ) {
        // Cache the data for a specified duration (e.g., 1 hour)
        $cache_duration = apply_filters( 'acf_dynamic_api_fields_cache_duration', HOUR_IN_SECONDS );
        set_transient( $cache_key, $api_data, $cache_duration );
    }
} else {
    // Use cached data
    $api_data = $cached_data;
}

// ... rest of the logic using $api_data ...

Error Handling and User Feedback

The current implementation returns error messages directly into the field choices. This is crucial for debugging and informing administrators when integrations fail. Ensure error messages are user-friendly but also informative enough for developers.

Security

Be cautious when handling sensitive API credentials (keys, tokens). Avoid hardcoding them directly in the plugin. Consider using WordPress options or environment variables managed securely. The example uses ACF field settings, which are stored in the database; ensure your WordPress installation is secure.

Sanitize all user-provided configuration (like custom headers or API keys) before using them in `wp_remote_request` arguments. The provided code includes basic sanitization for headers.

Handling Different API Schemas

The `transform_api_data_to_choices` method is the most critical part for adapting to various API response structures. If your API returns data in a nested structure, the api_data_path is essential. If the keys for values and labels differ, adjust api_value_key and api_label_key accordingly.

For APIs that don’t return JSON, you would need to modify `fetch_api_data` to handle different content types and parsing methods (e.g., XML parsing).

Performance Optimization

Beyond caching, consider the following:

  • API Request Timeout: Set a reasonable timeout to prevent requests from hanging indefinitely.
  • Data Payload Size: If APIs return large datasets, consider if pagination or filtering options are available and how to implement them (this would require more complex field configurations or logic).
  • Asynchronous Operations: For very slow APIs, consider background processing (e.g., WP-Cron) to pre-fetch data, although this adds significant complexity.

Example: Integrating with a Hypothetical REST API

Suppose you have an external API at https://api.example.com/v1/products that returns a JSON structure like this:

{
  "status": "success",
  "data": {
    "items": [
      {
        "product_id": "prod_123",
        "product_name": "Awesome Gadget",
        "category": "Electronics"
      },
      {
        "product_id": "prod_456",
        "product_name": "Super Widget",
        "category": "Tools"
      }
    ],
    "total": 2
  }
}

To populate an ACF ‘Select’ field with product names and their IDs, you would configure the field as follows:

  • Field Type: Select
  • Name: product_selector
  • API Source: https://api.example.com/v1/products
  • API Data Path: data.items
  • API Value Key: product_id
  • API Label Key: product_name

The `API_Field_Manager` class would then correctly parse this response, navigate to data.items, and use product_id for the choice value and product_name for the choice label.

Conclusion

By extending ACF Pro with custom logic that leverages the robust WordPress HTTP API, developers can create dynamic, data-driven fields that integrate seamlessly with virtually any external service. Careful attention to configuration, error handling, caching, and security ensures these extensions are production-ready and maintainable.

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

  • How to analyze and reduce CPU consumption of custom Singleton Registry Pattern event mediators
  • How to analyze and reduce CPU consumption of custom Factory Method design structures event mediators
  • WordPress Development Recipe: High-efficiency server-side rendering for Gutenberg blocks using Readonly classes
  • How to securely integrate SendGrid transactional mailer endpoints into WordPress custom plugins using Filesystem API
  • How to design secure Algolia Search API webhook listeners using signature validation and payload queues

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 (42)
  • 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 (114)
  • WordPress Plugin Development (123)
  • WordPress Plugin Development (330)
  • WordPress Theme Development (357)

Recent Posts

  • How to analyze and reduce CPU consumption of custom Singleton Registry Pattern event mediators
  • How to analyze and reduce CPU consumption of custom Factory Method design structures event mediators
  • WordPress Development Recipe: High-efficiency server-side rendering for Gutenberg blocks using Readonly classes

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