• 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 ActiveCampaign automation API endpoints into WordPress custom plugins using Heartbeat API

How to securely integrate ActiveCampaign automation API endpoints into WordPress custom plugins using Heartbeat API

Securing ActiveCampaign API Calls with WordPress Heartbeat

Integrating third-party APIs, especially for sensitive operations like marketing automation, demands robust security measures within a WordPress environment. This guide details a production-ready approach to securely interact with ActiveCampaign’s API endpoints from within a custom WordPress plugin, leveraging the WordPress Heartbeat API for real-time, secure data exchange.

Understanding the Security Imperative

Directly embedding API keys or making synchronous, long-running API calls from the front-end or even within standard AJAX handlers in WordPress can expose sensitive credentials and lead to poor user experience due to blocking operations. The WordPress Heartbeat API provides a mechanism for frequent, non-blocking communication between the browser and the server. By channeling our ActiveCampaign API interactions through Heartbeat, we can achieve:

  • Credential Obfuscation: API keys remain server-side, never exposed to the client.
  • Asynchronous Operations: Heartbeat requests are designed to be lightweight and non-blocking, preventing UI freezes.
  • Contextual Security: Actions can be tied to user capabilities and specific admin contexts.
  • Rate Limiting & Throttling: Easier to implement server-side controls.

Plugin Structure and Setup

We’ll assume a basic custom plugin structure. For this example, let’s call our plugin `my-activecampaign-integration`. The core logic will reside in a main plugin file (e.g., `my-activecampaign-integration.php`) and potentially a separate class file for API interactions.

Storing API Credentials Securely

Never hardcode API keys directly in your plugin files. The recommended approach is to use WordPress’s built-in settings API or, for enhanced security, store them in environment variables and access them via `getenv()` or a configuration file outside the webroot. For simplicity in this example, we’ll use `add_option()` and `get_option()`, but ensure these are set via a secure admin page and not directly in code.

Registering the Heartbeat Hook

The Heartbeat API hooks into the `heartbeat_send` filter. We’ll register a callback function to listen for specific actions we want to perform.

<?php
/**
 * Plugin Name: My ActiveCampaign Integration
 * Description: Securely integrates ActiveCampaign API via WordPress Heartbeat.
 * Version: 1.0
 * Author: Your Name
 */

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

// Define constants for API endpoints and keys (ideally loaded from secure sources)
define( 'MY_AC_API_URL', 'https://YOUR_ACCOUNT.api-us1.com' ); // Replace with your ActiveCampaign API URL
define( 'MY_AC_API_KEY', get_option( 'my_ac_api_key' ) ); // Stored via WordPress options

/**
 * Enqueue scripts that will initiate the Heartbeat API.
 */
function my_ac_enqueue_heartbeat_scripts() {
    // Only load on specific admin pages where interaction is needed.
    // Example: A custom admin page or post edit screen.
    if ( is_admin() ) {
        wp_enqueue_script( 'my-ac-heartbeat', plugin_dir_url( __FILE__ ) . 'js/my-ac-heartbeat.js', array( 'jquery', 'heartbeat' ), '1.0', true );

        // Localize script to pass necessary data to JavaScript.
        wp_localize_script( 'my-ac-heartbeat', 'my_ac_heartbeat_params', array(
            'ajax_url' => admin_url( 'admin-ajax.php' ),
            'nonce'    => wp_create_nonce( 'my_ac_heartbeat_nonce' ),
            'api_key'  => MY_AC_API_KEY, // Pass key if absolutely necessary for client-side checks, but prefer server-side.
            'api_url'  => MY_AC_URL,
        ) );
    }
}
add_action( 'admin_enqueue_scripts', 'my_ac_enqueue_heartbeat_scripts' );

/**
 * Handle Heartbeat API data.
 *
 * @param array $response Heartbeat response data.
 * @param array $data     Data sent from the client.
 * @return array Modified response data.
 */
function my_ac_heartbeat_send( $response, $data ) {
    // Check for our custom action.
    if ( ! empty( $data['my_ac_action'] ) ) {
        // Verify the nonce for security.
        if ( ! isset( $data['nonce'] ) || ! wp_verify_nonce( $data['nonce'], 'my_ac_heartbeat_nonce' ) ) {
            $response['error'] = __( 'Security check failed.', 'my-activecampaign-integration' );
            return $response;
        }

        // Ensure API key is available.
        if ( empty( MY_AC_API_KEY ) ) {
            $response['error'] = __( 'ActiveCampaign API key is not configured.', 'my-activecampaign-integration' );
            return $response;
        }

        $action = sanitize_text_field( $data['my_ac_action'] );

        switch ( $action ) {
            case 'get_contacts':
                // Example: Fetch contacts.
                $response['my_ac_contacts'] = my_ac_fetch_contacts();
                break;
            case 'add_contact':
                if ( isset( $data['contact_data'] ) && is_array( $data['contact_data'] ) ) {
                    $response['my_ac_add_contact_result'] = my_ac_add_contact( $data['contact_data'] );
                } else {
                    $response['error'] = __( 'Invalid contact data provided.', 'my-activecampaign-integration' );
                }
                break;
            // Add more cases for other ActiveCampaign API interactions.
            default:
                $response['error'] = __( 'Unknown ActiveCampaign action.', 'my-activecampaign-integration' );
                break;
        }
    }
    return $response;
}
add_filter( 'heartbeat_send', 'my_ac_heartbeat_send', 10, 2 );

/**
 * Helper function to fetch contacts from ActiveCampaign.
 *
 * @return array|WP_Error
 */
function my_ac_fetch_contacts() {
    $url = MY_AC_API_URL . '/api/3/contacts';
    $args = array(
        'headers' => array(
            'Api-Token' => MY_AC_API_KEY,
            'Accept'    => 'application/json',
        ),
        'timeout' => 15, // Set a reasonable timeout.
    );

    $response = wp_remote_get( $url, $args );

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

    $body = wp_remote_retrieve_body( $response );
    $data = json_decode( $body, true );

    if ( json_last_error() !== JSON_ERROR_NONE ) {
        return new WP_Error( 'json_decode_error', __( 'Failed to decode API response.', 'my-activecampaign-integration' ) );
    }

    if ( $response['response']['code'] >= 200 && $response['response']['code'] < 300 ) {
        return $data;
    } else {
        return new WP_Error( 'api_error', sprintf( __( 'ActiveCampaign API Error: %s', 'my-activecampaign-integration' ), $data['errors'][0]['title'] ?? 'Unknown error' ) );
    }
}

/**
 * Helper function to add a contact to ActiveCampaign.
 *
 * @param array $contact_data Contact data.
 * @return array|WP_Error
 */
function my_ac_add_contact( $contact_data ) {
    $url = MY_AC_API_URL . '/api/3/contacts';
    $args = array(
        'headers' => array(
            'Api-Token' => MY_AC_API_KEY,
            'Content-Type' => 'application/json',
            'Accept'    => 'application/json',
        ),
        'body'    => json_encode( array( 'contact' => $contact_data ) ),
        'timeout' => 15,
    );

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

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

    $body = wp_remote_retrieve_body( $response );
    $data = json_decode( $body, true );

    if ( json_last_error() !== JSON_ERROR_NONE ) {
        return new WP_Error( 'json_decode_error', __( 'Failed to decode API response.', 'my-activecampaign-integration' ) );
    }

    if ( $response['response']['code'] >= 200 && $response['response']['code'] < 300 ) {
        return $data;
    } else {
        // Log the detailed error for debugging.
        error_log( 'ActiveCampaign Add Contact Error: ' . print_r( $data, true ) );
        return new WP_Error( 'api_error', sprintf( __( 'ActiveCampaign API Error: %s', 'my-activecampaign-integration' ), $data['errors'][0]['title'] ?? 'Unknown error' ) );
    }
}

// Add an AJAX handler for fallback or direct calls if needed, though Heartbeat is preferred.
// This is not strictly necessary if all interactions are via Heartbeat, but good for completeness.
add_action( 'wp_ajax_my_ac_ajax_handler', 'my_ac_ajax_handler' );
add_action( 'wp_ajax_nopriv_my_ac_ajax_handler', 'my_ac_ajax_handler' ); // If public access is needed (use with caution)

function my_ac_ajax_handler() {
    // Verify nonce for security.
    if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'my_ac_ajax_nonce' ) ) {
        wp_send_json_error( array( 'message' => __( 'Security check failed.', 'my-activecampaign-integration' ) ) );
    }

    if ( empty( MY_AC_API_KEY ) ) {
        wp_send_json_error( array( 'message' => __( 'ActiveCampaign API key is not configured.', 'my-activecampaign-integration' ) ) );
    }

    $action = isset( $_POST['my_ac_action'] ) ? sanitize_text_field( $_POST['my_ac_action'] ) : '';

    switch ( $action ) {
        case 'get_contacts':
            $result = my_ac_fetch_contacts();
            if ( is_wp_error( $result ) ) {
                wp_send_json_error( array( 'message' => $result->get_error_message() ) );
            } else {
                wp_send_json_success( $result );
            }
            break;
        case 'add_contact':
            if ( isset( $_POST['contact_data'] ) && is_array( $_POST['contact_data'] ) ) {
                $result = my_ac_add_contact( $_POST['contact_data'] );
                if ( is_wp_error( $result ) ) {
                    wp_send_json_error( array( 'message' => $result->get_error_message() ) );
                } else {
                    wp_send_json_success( $result );
                }
            } else {
                wp_send_json_error( array( 'message' => __( 'Invalid contact data provided.', 'my-activecampaign-integration' ) ) );
            }
            break;
        default:
            wp_send_json_error( array( 'message' => __( 'Unknown ActiveCampaign action.', 'my-activecampaign-integration' ) ) );
            break;
    }
    wp_die(); // This is required to terminate immediately and return a proper response.
}

Client-Side JavaScript for Heartbeat Interaction

The JavaScript file (`js/my-ac-heartbeat.js`) will initiate the Heartbeat requests. It listens for Heartbeat intervals and sends custom data, including our desired action and a nonce.

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

    // Ensure the Heartbeat API is available.
    if (typeof wp.heartbeat === 'undefined') {
        console.error('WordPress Heartbeat API not available.');
        return;
    }

    // Start Heartbeat with custom intervals if needed, or let it run at default.
    // The default interval is 60 seconds, but can be adjusted.
    // wp.heartbeat.interval( 30000 ); // Example: 30 seconds

    // Hook into the heartbeat-send event.
    $(document).on('heartbeat-send', function(e, data) {
        // Add our custom data to the heartbeat payload.
        // This is where you'd trigger actions based on user interaction or page context.

        // Example: Trigger fetching contacts every few Heartbeat intervals.
        // We use a simple counter to avoid sending on every single heartbeat.
        if (typeof my_ac_heartbeat_params.heartbeat_counter === 'undefined') {
            my_ac_heartbeat_params.heartbeat_counter = 0;
        }
        my_ac_heartbeat_params.heartbeat_counter++;

        // Send request to fetch contacts every 5 heartbeats (approx. 5 minutes if default interval is 60s)
        if (my_ac_heartbeat_params.heartbeat_counter % 5 === 0) {
            data.my_ac_action = 'get_contacts';
            data.nonce = my_ac_heartbeat_params.nonce; // Pass the nonce
            console.log('Sending request to fetch ActiveCampaign contacts...');
        }

        // Example: Trigger adding a contact (e.g., from a form submission handled elsewhere)
        // This would typically be triggered by a specific event, not a general heartbeat.
        // For demonstration, let's assume we have a button with ID 'add-ac-contact-btn'
        // and contact data is available in a JS variable.
        /*
        if ($('#add-ac-contact-btn').length && typeof my_contact_data_to_send !== 'undefined') {
            data.my_ac_action = 'add_contact';
            data.nonce = my_ac_heartbeat_params.nonce;
            data.contact_data = my_contact_data_to_send;
            console.log('Sending request to add ActiveCampaign contact...');
            // Potentially disable the button after sending to prevent duplicates.
        }
        */
    });

    // Hook into the heartbeat-tick event to process the response.
    $(document).on('heartbeat-tick', function(e, data) {
        // Check for our custom response data.
        if (data.my_ac_contacts) {
            console.log('Received ActiveCampaign contacts:', data.my_ac_contacts);
            // Process the received contacts data here.
            // Example: Update a list on the admin page.
            if (data.my_ac_contacts.hasOwnProperty('error')) {
                console.error('Error fetching contacts:', data.my_ac_contacts.error.message);
            } else if (data.my_ac_contacts.hasOwnProperty('contacts')) {
                console.log(`Successfully fetched ${data.my_ac_contacts.contacts.length} contacts.`);
                // Update UI or perform other actions.
            }
        }

        if (data.my_ac_add_contact_result) {
            console.log('ActiveCampaign add contact result:', data.my_ac_add_contact_result);
            if (data.my_ac_add_contact_result.hasOwnProperty('error')) {
                console.error('Error adding contact:', data.my_ac_add_contact_result.error.message);
            } else if (data.my_ac_add_contact_result.hasOwnProperty('contact')) {
                console.log('Contact added successfully:', data.my_ac_add_contact_result.contact.id);
                // Provide user feedback.
            }
        }

        if (data.error) {
            console.error('Heartbeat error from server:', data.error);
            // Handle server-side errors (e.g., API key missing, invalid action).
        }
    });

    // Optional: Handle Heartbeat connection errors.
    $(document).on('heartbeat-error', function(e, error) {
        console.error('Heartbeat connection error:', error);
        // Implement retry logic or user notifications.
    });

    // Example of triggering an action directly via AJAX (fallback or specific use cases)
    // This bypasses Heartbeat's interval but still uses server-side logic.
    function trigger_ac_action_ajax(action, data = {}, success_callback, error_callback) {
        const ajax_data = {
            action: 'my_ac_ajax_handler', // The WordPress AJAX hook
            nonce: my_ac_heartbeat_params.nonce, // Use the same nonce for consistency, or create a new one
            my_ac_action: action,
            ...data
        };

        $.post(my_ac_heartbeat_params.ajax_url, ajax_data, function(response) {
            if (response.success) {
                if (typeof success_callback === 'function') {
                    success_callback(response.data);
                }
            } else {
                console.error('AJAX Error:', response.data.message);
                if (typeof error_callback === 'function') {
                    error_callback(response.data.message);
                }
            }
        }).fail(function(xhr, status, error) {
            console.error('AJAX Request Failed:', status, error);
            if (typeof error_callback === 'function') {
                error_callback('Request failed: ' + error);
            }
        });
    }

    // Example usage of the direct AJAX trigger:
    /*
    $('#manual-fetch-contacts-btn').on('click', function() {
        trigger_ac_action_ajax('get_contacts', {}, function(data) {
            console.log('Manually fetched contacts:', data);
        }, function(errorMessage) {
            alert('Failed to fetch contacts: ' + errorMessage);
        });
    });
    */
});

Implementing ActiveCampaign API Logic

The PHP functions `my_ac_fetch_contacts()` and `my_ac_add_contact()` demonstrate how to make authenticated requests to the ActiveCampaign API using `wp_remote_get` and `wp_remote_post`. Key considerations:

  • Authentication: The `Api-Token` header is crucial.
  • Error Handling: Always check for `is_wp_error()` and parse API error responses.
  • Data Serialization: Use `json_encode()` for POST requests and `json_decode()` for responses.
  • Timeouts: Set reasonable `timeout` values for `wp_remote_*` functions to prevent script hangs.
  • Sanitization: Sanitize any data received from the client before using it in API calls (e.g., `sanitize_text_field`).

Security Best Practices and Considerations

While Heartbeat API offers a more secure channel than direct front-end AJAX, further hardening is recommended:

  • Nonce Verification: Always verify nonces on the server-side for both Heartbeat and AJAX requests.
  • User Capabilities: Before performing sensitive actions (like adding contacts), check user capabilities using `current_user_can()`. This can be done within the `my_ac_heartbeat_send` function.
  • Rate Limiting: Implement server-side rate limiting for your plugin’s AJAX actions and potentially for Heartbeat requests if they become too frequent or resource-intensive.
  • Environment Variables: For production, store API keys in environment variables rather than WordPress options. Use a library like `vlucas/phpdotenv` to load them.
  • HTTPS: Ensure your WordPress site uses HTTPS to encrypt data in transit.
  • API Key Rotation: Regularly rotate your ActiveCampaign API keys.
  • Logging: Implement robust logging for API errors and unexpected behavior.
  • Specific Page Loading: The `admin_enqueue_scripts` hook allows you to conditionally load your JavaScript only on the admin pages where ActiveCampaign integration is actually needed, reducing unnecessary Heartbeat traffic.

Advanced Scenarios and Alternatives

For highly complex or performance-critical integrations, consider these:

  • WP-Cron: For scheduled, non-real-time tasks (e.g., nightly data syncs), WP-Cron is more appropriate than Heartbeat.
  • Dedicated REST API Endpoints: For complex interactions or when Heartbeat’s interval is too slow, consider creating custom WordPress REST API endpoints. These offer more control over request/response handling and authentication.
  • Server-Side Queues: For very high-volume operations, offload tasks to a background processing queue (e.g., Redis Queue, RabbitMQ) managed by a separate worker process.

By strategically employing the WordPress Heartbeat API, you can build secure, responsive, and efficient integrations with external services like ActiveCampaign directly within your custom WordPress plugins, safeguarding sensitive credentials and enhancing the user experience.

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 implement custom Filesystem API endpoints with token authentication in Gutenberg blocks
  • How to analyze and reduce CPU consumption of custom Command Query Responsibility Segregation (CQRS) event mediators
  • Step-by-Step Guide: Refactoring legacy hooks to use Active Record Wrapper pattern in theme layers
  • Step-by-Step Guide to building a custom custom analytics tracker block for Gutenberg using Next.js headless configurations
  • Troubleshooting guide: Resolving memory leak spikes caused by unclosed custom database loops in member profile directories

Categories

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

Recent Posts

  • How to implement custom Filesystem API endpoints with token authentication in Gutenberg blocks
  • How to analyze and reduce CPU consumption of custom Command Query Responsibility Segregation (CQRS) event mediators
  • Step-by-Step Guide: Refactoring legacy hooks to use Active Record Wrapper pattern in theme layers

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (868)
  • Debugging & Troubleshooting (652)
  • Security & Compliance (635)
  • 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