• 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 securely integrate OpenAI Completion API endpoints into WordPress custom plugins using Heartbeat API

How to securely integrate OpenAI Completion API endpoints into WordPress custom plugins using Heartbeat API

Leveraging WordPress Heartbeat API for Real-time OpenAI Integrations

Integrating advanced AI capabilities, such as OpenAI’s Completion API, into a WordPress environment demands a robust and responsive mechanism. Traditional AJAX calls can be cumbersome for frequent, low-latency interactions. The WordPress Heartbeat API offers a powerful, built-in solution for persistent, real-time communication between the browser and the server, making it ideal for scenarios requiring continuous data exchange, like feeding user input to an AI model and receiving responses without full page reloads.

This post details a production-ready approach to securely integrating OpenAI’s API into a custom WordPress plugin, utilizing the Heartbeat API for a seamless user experience. We’ll cover API key management, secure server-side requests, and client-side JavaScript for dynamic interaction.

Secure OpenAI API Key Management

Storing API keys directly in plugin files or the database is a significant security risk. A more secure method involves using environment variables or WordPress’s built-in constants, ideally defined in a `wp-config.php` file that is not publicly accessible. For this example, we’ll assume the key is defined as a constant.

Add the following line to your `wp-config.php` file, replacing `YOUR_OPENAI_API_KEY` with your actual key:

define( 'OPENAI_API_KEY', 'YOUR_OPENAI_API_KEY' );

Server-Side Implementation: Handling Heartbeat and API Requests

We’ll create a custom plugin that hooks into the WordPress Heartbeat API. The Heartbeat API allows us to send data to the server periodically and receive data back. We’ll use this to send user input to our OpenAI integration endpoint and receive the AI’s completion.

First, let’s set up the plugin structure and the main PHP file. Create a directory named `my-openai-integration` in your `wp-content/plugins/` directory, and inside it, create `my-openai-integration.php`.

/*
Plugin Name: My OpenAI Integration
Description: Integrates OpenAI Completion API with WordPress using Heartbeat API.
Version: 1.0
Author: Antigravity
*/

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

// Ensure OpenAI API key is defined.
if ( ! defined( 'OPENAI_API_KEY' ) || empty( OPENAI_API_KEY ) ) {
    // In a production environment, you might want to log this or display a more user-friendly error.
    error_log( 'OpenAI API Key is not defined or is empty. Please define OPENAI_API_KEY in wp-config.php.' );
    return;
}

/**
 * Enqueue scripts and localize data.
 */
function my_openai_enqueue_scripts() {
    // Only load on specific admin pages where you want the integration.
    // For example, on the post edit screen:
    if ( get_current_screen() && 'post' === get_current_screen()->base ) {
        wp_enqueue_script( 'my-openai-heartbeat', plugin_dir_url( __FILE__ ) . 'js/openai-heartbeat.js', array( 'jquery', 'heartbeat' ), '1.0', true );

        wp_localize_script( 'my-openai-heartbeat', 'myOpenAI', array(
            'ajax_url' => admin_url( 'admin-ajax.php' ),
            'nonce'    => wp_create_nonce( 'my_openai_heartbeat_nonce' ),
            'openai_endpoint' => rest_url( 'my-openai-integration/v1/complete' ), // Using REST API for the actual call
        ) );
    }
}
add_action( 'admin_enqueue_scripts', 'my_openai_enqueue_scripts' );

/**
 * Register REST API endpoint for OpenAI completion.
 */
function my_openai_register_rest_route() {
    register_rest_route( 'my-openai-integration/v1', '/complete', array(
        'methods' => WP_REST_Server::CREATABLE, // Use CREATABLE for POST requests
        'callback' => 'my_openai_handle_completion_request',
        'permission_callback' => function() {
            // Basic permission check: ensure user is logged in and has edit capabilities.
            // You might want more granular permissions.
            return current_user_can( 'edit_posts' );
        },
    ) );
}
add_action( 'rest_api_init', 'my_openai_register_rest_route' );

/**
 * Handles the OpenAI completion request via REST API.
 *
 * @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_openai_handle_completion_request( WP_REST_Request $request ) {
    // Verify nonce for security.
    if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'my_openai_heartbeat_nonce' ) ) {
        return new WP_Error( 'rest_nonce_invalid', 'Nonce is invalid.', array( 'status' => 403 ) );
    }

    $data = $request->get_json_params(); // Get JSON data from the request body

    if ( empty( $data['prompt'] ) ) {
        return new WP_Error( 'rest_missing_param', 'Missing required parameter: prompt.', array( 'status' => 400 ) );
    }

    $prompt = sanitize_text_field( $data['prompt'] );
    $model = isset( $data['model'] ) ? sanitize_text_field( $data['model'] ) : 'gpt-3.5-turbo-instruct'; // Default model
    $max_tokens = isset( $data['max_tokens'] ) ? intval( $data['max_tokens'] ) : 150;
    $temperature = isset( $data['temperature'] ) ? floatval( $data['temperature'] ) : 0.7;

    $openai_response = my_openai_call_api( $prompt, $model, $max_tokens, $temperature );

    if ( is_wp_error( $openai_response ) ) {
        return $openai_response;
    }

    return new WP_REST_Response( $openai_response, 200 );
}

/**
 * Makes the actual call to the OpenAI API.
 *
 * @param string $prompt The text prompt for the AI.
 * @param string $model The OpenAI model to use.
 * @param int $max_tokens Maximum tokens to generate.
 * @param float $temperature Controls randomness.
 * @return array|WP_Error The API response or a WP_Error object.
 */
function my_openai_call_api( $prompt, $model, $max_tokens, $temperature ) {
    $api_url = 'https://api.openai.com/v1/completions'; // For older models like text-davinci-003
    // For newer chat models, use 'https://api.openai.com/v1/chat/completions' and adjust payload.
    // This example uses the older completions endpoint for simplicity with 'gpt-3.5-turbo-instruct'.

    $body = json_encode( array(
        'model' => $model,
        'prompt' => $prompt,
        'max_tokens' => $max_tokens,
        'temperature' => $temperature,
        'n' => 1, // Number of completions to generate
        'stop' => null, // Sequence where the API will stop generating further tokens.
    ) );

    $args = array(
        'body' => $body,
        'headers' => array(
            'Authorization' => 'Bearer ' . OPENAI_API_KEY,
            'Content-Type' => 'application/json',
        ),
        'method' => 'POST',
        'timeout' => 30, // Adjust timeout as needed
    );

    $response = wp_remote_post( $api_url, $args );

    if ( is_wp_error( $response ) ) {
        error_log( 'OpenAI API Request Failed: ' . $response->get_error_message() );
        return new WP_Error( 'openai_api_error', 'OpenAI API request failed.', array( 'status' => 500, 'details' => $response->get_error_message() ) );
    }

    $response_code = wp_remote_retrieve_response_code( $response );
    $response_body = wp_remote_retrieve_body( $response );
    $decoded_body = json_decode( $response_body, true );

    if ( $response_code !== 200 ) {
        $error_message = isset( $decoded_body['error']['message'] ) ? $decoded_body['error']['message'] : 'Unknown error';
        error_log( "OpenAI API Error ({$response_code}): {$error_message}" );
        return new WP_Error( 'openai_api_error', 'OpenAI API returned an error.', array( 'status' => $response_code, 'details' => $error_message ) );
    }

    // The structure of the response depends on the model used.
    // For 'gpt-3.5-turbo-instruct', it's typically $decoded_body['choices'][0]['text'].
    // For chat models, it's $decoded_body['choices'][0]['message']['content'].
    if ( isset( $decoded_body['choices'][0]['text'] ) ) {
        return array( 'completion' => trim( $decoded_body['choices'][0]['text'] ) );
    } elseif ( isset( $decoded_body['choices'][0]['message']['content'] ) ) {
        // This part is for chat models, if you switch the endpoint and model.
        return array( 'completion' => trim( $decoded_body['choices'][0]['message']['content'] ) );
    } else {
        error_log( 'OpenAI API response format unexpected.' );
        return new WP_Error( 'openai_api_error', 'OpenAI API response format unexpected.', array( 'status' => 500 ) );
    }
}

/**
 * Hook into Heartbeat API to send and receive data.
 *
 * @param array $response Heartbeat response data.
 * @param array $data Data sent from the client.
 * @return array Modified response data.
 */
function my_openai_heartbeat_received( $response, $data ) {
    if ( ! empty( $data['my_openai_prompt'] ) ) {
        $prompt = sanitize_text_field( $data['my_openai_prompt'] );
        $model = isset( $data['my_openai_model'] ) ? sanitize_text_field( $data['my_openai_model'] ) : 'gpt-3.5-turbo-instruct';
        $max_tokens = isset( $data['my_openai_max_tokens'] ) ? intval( $data['my_openai_max_tokens'] ) : 150;
        $temperature = isset( $data['my_openai_temperature'] ) ? floatval( $data['my_openai_temperature'] ) : 0.7;

        // For Heartbeat, we directly call the API function.
        // Note: For very long-running operations, Heartbeat might time out.
        // The REST API approach is generally more robust for complex tasks.
        // This example demonstrates Heartbeat for simple, quick responses.
        $openai_result = my_openai_call_api( $prompt, $model, $max_tokens, $temperature );

        if ( is_wp_error( $openai_result ) ) {
            $response['my_openai_error'] = $openai_result->get_error_message();
        } else {
            $response['my_openai_completion'] = $openai_result['completion'];
        }
    }
    return $response;
}
add_filter( 'heartbeat_received', 'my_openai_heartbeat_received', 10, 2 );

/**
 * Control Heartbeat interval.
 *
 * @param array $settings Heartbeat settings.
 * @return array Modified settings.
 */
function my_openai_heartbeat_settings( $settings ) {
    // Adjust interval for faster updates if needed, but be mindful of server load.
    // Default is 60 seconds. Setting to 15 seconds for more real-time feel.
    $settings['interval'] = 15; // seconds
    return $settings;
}
add_filter( 'heartbeat_settings', 'my_openai_heartbeat_settings' );

In this PHP file:

  • We define constants for security and check for their existence.
  • We enqueue a JavaScript file (`openai-heartbeat.js`) and localize necessary data, including a nonce for security and the REST API endpoint URL.
  • We register a custom REST API endpoint (`/my-openai-integration/v1/complete`) to handle the actual POST request to OpenAI. This is crucial for security and scalability, as Heartbeat has limitations for complex or long-running tasks.
  • The `my_openai_handle_completion_request` function verifies the nonce, sanitizes input, and calls `my_openai_call_api`.
  • The `my_openai_call_api` function constructs the request payload for OpenAI, sets the necessary headers (including the API key), and uses `wp_remote_post` to send the request. It handles potential errors and decodes the JSON response.
  • We hook into `heartbeat_received` to process data sent from the client via Heartbeat. This function directly calls `my_openai_call_api` for immediate responses.
  • We adjust the Heartbeat interval using `heartbeat_settings` for a more responsive feel, though this should be done cautiously.

Client-Side Implementation: JavaScript for Interaction

Now, let’s create the JavaScript file that will interact with the Heartbeat API and the REST API endpoint. Create a `js` directory inside your plugin folder and add `openai-heartbeat.js`.

jQuery(document).ready(function($) {

    // Heartbeat interval for sending data to the server
    var heartbeatInterval = 15000; // 15 seconds, should match server setting if possible
    var lastHeartbeatTime = 0;
    var heartbeatTimer;

    // Function to send data via Heartbeat
    function sendHeartbeatData(dataToSend) {
        if (typeof window.heartbeatSettings !== 'undefined' && window.heartbeatSettings.interval) {
            // Use the interval defined by WordPress Heartbeat API if available
            heartbeatInterval = window.heartbeatSettings.interval * 1000;
        }

        var currentTime = new Date().getTime();
        if (currentTime - lastHeartbeatTime > heartbeatInterval) {
            lastHeartbeatTime = currentTime;

            // Send data to the Heartbeat API endpoint
            $(document).trigger('heartbeat-send', {
                // Add your custom data here
                my_openai_prompt: dataToSend.prompt,
                my_openai_model: dataToSend.model || 'gpt-3.5-turbo-instruct',
                my_openai_max_tokens: dataToSend.max_tokens || 150,
                my_openai_temperature: dataToSend.temperature || 0.7
            });
        }
    }

    // Listen for Heartbeat responses
    $(document).on('heartbeat-tick', function(e, data) {
        // Check if the response contains data from our OpenAI integration
        if (data.my_openai_completion) {
            console.log('OpenAI Completion received via Heartbeat:', data.my_openai_completion);
            // Update UI with the completion
            $('#openai-response-area').text(data.my_openai_completion);
        }
        if (data.my_openai_error) {
            console.error('OpenAI Error received via Heartbeat:', data.my_openai_error);
            $('#openai-response-area').text('Error: ' + data.my_openai_error);
        }
    });

    // Example: Triggering an OpenAI request when a button is clicked
    $('#get-openai-completion-button').on('click', function(e) {
        e.preventDefault();

        var userPrompt = $('#user-prompt-input').val();
        if (!userPrompt) {
            alert('Please enter a prompt.');
            return;
        }

        // Option 1: Send via Heartbeat (for quick, background tasks)
        // sendHeartbeatData({ prompt: userPrompt });

        // Option 2: Send via REST API (more robust for direct user-initiated actions)
        // This is generally preferred for explicit user actions.
        var data = {
            prompt: userPrompt,
            model: 'gpt-3.5-turbo-instruct', // Or get from a dropdown
            max_tokens: 150,
            temperature: 0.7
        };

        // Add nonce to the data for REST API verification
        data._wpnonce = myOpenAI.nonce;

        $.ajax({
            url: myOpenAI.openai_endpoint, // Use the localized REST API endpoint
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify(data),
            beforeSend: function(xhr) {
                // Show a loading indicator
                $('#openai-response-area').text('Generating completion...');
            },
            success: function(response) {
                if (response.completion) {
                    console.log('OpenAI Completion received via REST API:', response.completion);
                    $('#openai-response-area').text(response.completion);
                } else {
                    console.error('Unexpected response format:', response);
                    $('#openai-response-area').text('Error: Unexpected response from server.');
                }
            },
            error: function(jqXHR, textStatus, errorThrown) {
                console.error('REST API Error:', textStatus, errorThrown, jqXHR.responseText);
                var errorMessage = 'An unknown error occurred.';
                try {
                    var errorData = JSON.parse(jqXHR.responseText);
                    if (errorData && errorData.message) {
                        errorMessage = errorData.message;
                    }
                } catch (e) {
                    // Ignore parsing errors
                }
                $('#openai-response-area').text('Error: ' + errorMessage);
            },
            complete: function() {
                // Hide loading indicator
            }
        });
    });

    // Example of how to use sendHeartbeatData for background tasks
    // This would typically be triggered by some event or on a timer
    // For demonstration, let's send a prompt every 30 seconds if the input field has content
    setInterval(function() {
        var userPrompt = $('#user-prompt-input').val();
        if (userPrompt) {
            // Send via Heartbeat for background processing
            sendHeartbeatData({ prompt: userPrompt });
        }
    }, 30000); // Every 30 seconds

});

In `openai-heartbeat.js`:

  • We define `sendHeartbeatData` to send custom data to the server via the Heartbeat API. This function checks the Heartbeat interval and sends data periodically.
  • We listen for `heartbeat-tick` events. When a response comes back from the server, we check for `my_openai_completion` or `my_openai_error` and update a designated HTML element (`#openai-response-area`).
  • An example click handler for a button (`#get-openai-completion-button`) demonstrates how to trigger an OpenAI request. It shows two options:
    • Option 1 (Commented out): Using `sendHeartbeatData` to send the prompt via Heartbeat. This is suitable for background tasks or when you want to leverage the persistent connection for frequent, small updates.
    • Option 2: Using `$.ajax` to directly call the custom REST API endpoint (`myOpenAI.openai_endpoint`). This is generally more robust for explicit user actions, as it provides better control over request/response cycles and error handling.
  • The REST API call includes the nonce and sends the prompt as JSON.
  • Basic UI updates (showing “Generating completion…” and displaying the result or error) are included.
  • A `setInterval` example shows how `sendHeartbeatData` can be used for background processing.

Integrating into WordPress UI

To make this functional, you need to add some HTML to a WordPress page or post where you want the integration to appear. For example, you could add this to the content of a post or page, or within a custom meta box on the post edit screen.

<div>
    <h3>OpenAI Completion Example</h3>
    <label for="user-prompt-input">Enter your prompt:</label><br>
    <textarea id="user-prompt-input" rows="4" cols="50"></textarea><br>
    <button id="get-openai-completion-button">Get Completion (REST API)</button>
    <p>
        <strong>Response:</strong>
        <span id="openai-response-area">Waiting for input...</span>
    </p>
</div>

This HTML provides a textarea for user input, a button to trigger the REST API call, and a span to display the AI’s response. The JavaScript will target these elements.

Security Considerations and Best Practices

  • API Key Security: Never expose your OpenAI API key in client-side JavaScript or directly in publicly accessible files. Using `wp-config.php` constants is a good first step. For enhanced security, consider using a secrets management system or a dedicated WordPress options page with appropriate access controls.
  • Nonce Verification: Always verify nonces for any AJAX or REST API requests originating from the client. This prevents Cross-Site Request Forgery (CSRF) attacks.
  • Input Sanitization: Sanitize all user-provided input on the server-side before using it in API requests or database queries. WordPress functions like `sanitize_text_field`, `sanitize_textarea_field`, and `intval` are essential.
  • Rate Limiting: Implement rate limiting on your REST API endpoint to prevent abuse and control costs. WordPress’s built-in rate limiting for REST API can be extended, or you can implement custom logic.
  • Error Handling: Provide clear error messages to the user while logging detailed errors on the server for debugging. Use `WP_Error` objects for consistent error reporting.
  • Heartbeat vs. REST API: Understand the strengths of each. Heartbeat is excellent for background, periodic updates and low-latency communication. The REST API is more suitable for explicit user actions, complex operations, and when you need more control over the request lifecycle. This example uses both to illustrate their capabilities.
  • Model Selection: Allow users to select models if appropriate, but validate their choices server-side to prevent unexpected API calls.
  • Cost Management: Be mindful of OpenAI API costs. Implement usage limits or notifications if necessary.

Conclusion

By combining the WordPress Heartbeat API with a secure REST API endpoint, you can create dynamic and responsive AI-powered features within your WordPress site. This approach ensures that sensitive API keys are protected, user input is handled securely, and the user experience is seamless, with real-time feedback from AI models. Remember to adapt the UI integration and specific logic to your plugin’s unique requirements.

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

  • Building secure B2B pricing grids with custom WP HTTP API endpoints and role overrides
  • Debugging and Resolving deep-seated hook priority conflicts in third-party Shopify headless API connectors
  • How to construct high-throughput import engines for large vendor commission records sets using custom XML/JSON parsers
  • Optimizing p99 database query response latency in multi-site Service Provider custom tables
  • Troubleshooting guide: Resolving memory leak spikes caused by unclosed custom database loops in custom product catalogs

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 (48)
  • 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 (155)
  • WordPress Plugin Development (178)
  • WordPress Plugin Development (330)
  • WordPress Theme Development (357)

Recent Posts

  • Building secure B2B pricing grids with custom WP HTTP API endpoints and role overrides
  • Debugging and Resolving deep-seated hook priority conflicts in third-party Shopify headless API connectors
  • How to construct high-throughput import engines for large vendor commission records sets using custom XML/JSON parsers

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