• 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 REST API Controllers

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

Leveraging WordPress REST API Controllers for Secure ActiveCampaign Integration

Integrating third-party marketing automation platforms like ActiveCampaign into WordPress is a common requirement for enhancing user engagement and lead nurturing. While direct database manipulation or simple AJAX calls are tempting, a robust and secure approach involves utilizing WordPress’s built-in REST API framework. This allows us to create custom endpoints within our WordPress plugin that act as secure intermediaries for communicating with ActiveCampaign’s API. This method encapsulates sensitive API keys, provides a structured way to handle requests and responses, and leverages WordPress’s existing authentication and authorization mechanisms.

Setting Up the WordPress REST API Controller

We’ll start by defining a custom REST API controller class within our WordPress plugin. This class will extend WP_REST_Controller and handle the registration of our custom endpoints. For this example, let’s assume we’re building a plugin named ‘my-activecampaign-integration’.

First, ensure your plugin has a main file (e.g., my-activecampaign-integration.php) and a directory for your classes (e.g., includes/).

Create a file named class-my-ac-rest-controller.php inside the includes/ directory:

<?php
/**
 * My ActiveCampaign Integration REST API Controller.
 *
 * @package My_ActiveCampaign_Integration
 */

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

/**
 * Class My_AC_REST_Controller
 */
class My_AC_REST_Controller extends WP_REST_Controller {

	/**
	 * The namespace for this controller's routes.
	 *
	 * @var string
	 */
	protected $namespace = 'my-ac-integration/v1';

	/**
	 * The base for this controller's routes.
	 *
	 * @var string
	 */
	protected $rest_base = 'contacts';

	/**
	 * Register the routes for this controller.
	 */
	public function register_routes() {
		register_rest_route( $this->namespace, '/' . $this->rest_base, array(
			array(
				'methods'             => WP_REST_Server::CREATABLE, // POST method
				'callback'            => array( $this, 'create_contact' ),
				'permission_callback' => array( $this, 'create_contact_permissions_check' ),
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
			),
		) );
	}

	/**
	 * Get the schema for the items.
	 *
	 * @return array Schema array.
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'contact',
			'description' => 'Represents an ActiveCampaign contact.',
			'type'       => 'object',
			'properties' => array(
				'email' => array(
					'description' => esc_html__( 'The email address of the contact.', 'my-ac-integration' ),
					'type'        => 'string',
					'format'      => 'email',
					'required'    => true,
				),
				'first_name' => array(
					'description' => esc_html__( 'The first name of the contact.', 'my-ac-integration' ),
					'type'        => 'string',
				),
				'last_name' => array(
					'description' => esc_html__( 'The last name of the contact.', 'my-ac-integration' ),
					'type'        => 'string',
				),
				'phone' => array(
					'description' => esc_html__( 'The phone number of the contact.', 'my-ac-integration' ),
					'type'        => 'string',
				),
				// Add other fields as needed for ActiveCampaign
			),
		);
		return $schema;
	}

	/**
	 * Permission check for creating a contact.
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return bool|WP_Error True if the request has permission, WP_Error object otherwise.
	 */
	public function create_contact_permissions_check( WP_REST_Request $request ) {
		// In a production environment, you'd want more robust checks.
		// For example, checking for a specific nonce, user capability, or an API key passed in headers.
		// For simplicity, we'll allow authenticated users for now.
		// A more secure approach would be to use application passwords or custom API keys.
		return current_user_can( 'edit_posts' ); // Example: Only users who can edit posts can create contacts.
	}

	/**
	 * Creates a new contact in ActiveCampaign.
	 *
	 * @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.
	 */
	public function create_contact( WP_REST_Request $request ) {
		$email      = sanitize_email( $request->get_param( 'email' ) );
		$first_name = sanitize_text_field( $request->get_param( 'first_name' ) );
		$last_name  = sanitize_text_field( $request->get_param( 'last_name' ) );
		$phone      = sanitize_text_field( $request->get_param( 'phone' ) );

		// Basic validation
		if ( ! is_email( $email ) ) {
			return new WP_Error( 'invalid_email', esc_html__( 'Invalid email address provided.', 'my-ac-integration' ), array( 'status' => 400 ) );
		}

		// Retrieve ActiveCampaign API credentials from WordPress options
		$api_url  = get_option( 'my_ac_api_url' );
		$api_key  = get_option( 'my_ac_api_key' );
		$list_id  = get_option( 'my_ac_list_id' ); // Assuming you want to add to a specific list

		if ( empty( $api_url ) || empty( $api_key ) || empty( $list_id ) ) {
			return new WP_Error( 'missing_credentials', esc_html__( 'ActiveCampaign API credentials are not configured.', 'my-ac-integration' ), array( 'status' => 500 ) );
		}

		// Prepare data for ActiveCampaign API
		$contact_data = array(
			'email'      => $email,
			'first_name' => $first_name,
			'last_name'  => $last_name,
			'phone'      => $phone,
			'listid'     => $list_id, // Add to a specific list
			'status'     => 1,       // 1 = Active, 2 = Pending, 3 = Unsubscribed, 0 = Cleaned
		);

		// Make the API call to ActiveCampaign
		$response = $this->make_activecampaign_request( 'contacts', $contact_data, 'POST' );

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

		// Process ActiveCampaign API response
		// The exact structure depends on ActiveCampaign's API.
		// Assuming a successful creation returns a 201 Created status and a body with contact details.
		if ( isset( $response['body'] ) ) {
			$body = json_decode( $response['body'], true );
			if ( json_last_error() === JSON_ERROR_NONE ) {
				// Successful creation
				return new WP_REST_Response( array(
					'message' => esc_html__( 'Contact created successfully in ActiveCampaign.', 'my-ac-integration' ),
					'data'    => $body, // Return relevant data from AC response
				), 201 );
			} else {
				return new WP_Error( 'json_decode_error', esc_html__( 'Failed to parse ActiveCampaign API response.', 'my-ac-integration' ), array( 'status' => 500 ) );
			}
		}

		return new WP_Error( 'unknown_error', esc_html__( 'An unknown error occurred while processing the ActiveCampaign API response.', 'my-ac-integration' ), array( 'status' => 500 ) );
	}

	/**
	 * Helper function to make requests to the ActiveCampaign API.
	 *
	 * @param string $endpoint The API endpoint (e.g., 'contacts').
	 * @param array  $data     The data to send in the request body.
	 * @param string $method   The HTTP method (POST, GET, PUT, DELETE).
	 * @return array|WP_Error The response from the API or a WP_Error object.
	 */
	private function make_activecampaign_request( $endpoint, $data = array(), $method = 'GET' ) {
		$api_url  = get_option( 'my_ac_api_url' );
		$api_key  = get_option( 'my_ac_api_key' );

		if ( empty( $api_url ) || empty( $api_key ) ) {
			return new WP_Error( 'missing_credentials', esc_html__( 'ActiveCampaign API URL or Key is missing.', 'my-ac-integration' ), array( 'status' => 500 ) );
		}

		// ActiveCampaign API v3 uses /api/3/
		$url = trailingslashit( $api_url ) . 'api/3/' . $endpoint;

		$headers = array(
			'Api-Token' => $api_key,
			'Content-Type' => 'application/json',
			'Accept' => 'application/json',
		);

		$args = array(
			'method'  => strtoupper( $method ),
			'headers' => $headers,
			'timeout' => 30, // Adjust timeout as needed
		);

		if ( 'GET' !== strtoupper( $method ) && ! empty( $data ) ) {
			$args['body'] = json_encode( $data );
		} elseif ( 'GET' === strtoupper( $method ) && ! empty( $data ) ) {
			// For GET requests, data is usually passed as query parameters
			$url = add_query_arg( $data, $url );
		}

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

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

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

		// ActiveCampaign API typically returns JSON for both success and errors
		if ( $response_code >= 200 && $response_code < 300 ) {
			return array(
				'response_code' => $response_code,
				'body'          => $response_body,
			);
		} else {
			// Attempt to parse error response for more details
			$error_data = json_decode( $response_body, true );
			$error_message = isset( $error_data['errors'] ) ? implode( ', ', $error_data['errors'] ) : esc_html__( 'An unknown API error occurred.', 'my-ac-integration' );

			return new WP_Error(
				'activecampaign_api_error',
				sprintf(
					/* translators: %1$s: HTTP status code, %2$s: Error message from API */
					esc_html__( 'ActiveCampaign API Error: %1$s - %2$s', 'my-ac-integration' ),
					$response_code,
					$error_message
				),
				array( 'status' => $response_code, 'api_response' => $error_data )
			);
		}
	}
}

Registering the Controller

To make this controller active, we need to hook into WordPress’s REST API initialization. Add the following code to your main plugin file (my-activecampaign-integration.php):

<?php
/**
 * Plugin Name: My ActiveCampaign Integration
 * Description: Integrates with ActiveCampaign using REST API.
 * Version: 1.0.0
 * Author: Your Name
 * Text Domain: my-ac-integration
 */

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

// Include the REST API controller class.
require_once plugin_dir_path( __FILE__ ) . 'includes/class-my-ac-rest-controller.php';

/**
 * Register the REST API controller.
 */
function my_ac_register_rest_controller() {
	$controller = new My_AC_REST_Controller();
	$controller->register_routes();
}
add_action( 'rest_api_init', 'my_ac_register_rest_controller' );

// Add settings page for API credentials (simplified example)
function my_ac_settings_page() {
    add_options_page(
        __( 'My ActiveCampaign Settings', 'my-ac-integration' ),
        __( 'ActiveCampaign', 'my-ac-integration' ),
        'manage_options',
        'my-activecampaign-settings',
        'my_ac_settings_page_html'
    );
}
add_action( 'admin_menu', 'my_ac_settings_page' );

function my_ac_settings_page_html() {
    // Check user capabilities
    if ( ! current_user_can( 'manage_options' ) ) {
        return;
    }

    // Save settings if form submitted
    if ( isset( $_POST['my_ac_api_url'] ) && isset( $_POST['_wpnonce'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'my_ac_settings_nonce' ) ) {
        update_option( 'my_ac_api_url', sanitize_url( $_POST['my_ac_api_url'] ) );
        update_option( 'my_ac_api_key', sanitize_text_field( $_POST['my_ac_api_key'] ) );
        update_option( 'my_ac_list_id', sanitize_text_field( $_POST['my_ac_list_id'] ) );
        ?>
        <div class="notice notice-success is-dismissible"><p><?php _e( 'Settings saved.', 'my-ac-integration' ); ?></p></div>
        <?php
    }

    // Get current settings
    $api_url  = get_option( 'my_ac_api_url', '' );
    $api_key  = get_option( 'my_ac_api_key', '' );
    $list_id  = get_option( 'my_ac_list_id', '' );
    ?>
    <div class="wrap">
        <h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
        <form action="" method="post">
            <table class="form-table">
                <tr>
                    <th><label for="my_ac_api_url"><?php _e( 'ActiveCampaign API URL', 'my-ac-integration' ); ?></label></th>
                    <td><input type="url" id="my_ac_api_url" name="my_ac_api_url" value="<?php echo esc_attr( $api_url ); ?>" class="regular-text" required></td>
                </tr>
                <tr>
                    <th><label for="my_ac_api_key"><?php _e( 'ActiveCampaign API Key', 'my-ac-integration' ); ?></label></th>
                    <td><input type="text" id="my_ac_api_key" name="my_ac_api_key" value="<?php echo esc_attr( $api_key ); ?>" class="regular-text" required></td>
                </tr>
                <tr>
                    <th><label for="my_ac_list_id"><?php _e( 'ActiveCampaign List ID', 'my-ac-integration' ); ?></label></th>
                    <td><input type="text" id="my_ac_list_id" name="my_ac_list_id" value="<?php echo esc_attr( $list_id ); ?>" class="regular-text" required></td>
                </tr>
            </table>
            <?php wp_nonce_field( 'my_ac_settings_nonce' ); ?>
            <?php submit_button( __( 'Save Settings', 'my-ac-integration' ) ); ?>
        </form>
    </div>
    <?php
}

// Add settings link on plugin page
function my_ac_add_settings_link( $links ) {
    $settings_link = '' . __( 'Settings', 'my-ac-integration' ) . '';
    array_unshift( $links, $settings_link );
    return $links;
}
$plugin_basename = plugin_basename( __FILE__ );
add_filter( "plugin_action_links_$plugin_basename", 'my_ac_add_settings_link' );
?>

Understanding the Security and Best Practices

API Key Storage: The ActiveCampaign API key and URL are stored as WordPress options using update_option and retrieved with get_option. For enhanced security, consider encrypting these values, especially if your WordPress installation is on a shared hosting environment or if you have very strict security requirements. However, for most typical WordPress setups, storing them in the database options table is standard practice, provided the WordPress database itself is secured.

Endpoint Permissions: The create_contact_permissions_check method is crucial. In the example, it uses current_user_can( 'edit_posts' ), meaning only users with the ‘edit_posts’ capability (typically Editors and Administrators) can trigger this endpoint. For a public-facing form submission, you would typically use a nonce generated on the client-side and verified here. This prevents unauthorized access and abuse of your API endpoint.

Input Sanitization: All data received from the REST API request is sanitized using WordPress functions like sanitize_email(), sanitize_text_field(), and sanitize_url(). This is a fundamental security practice to prevent cross-site scripting (XSS) and other injection attacks.

Error Handling: The make_activecampaign_request helper function includes robust error handling. It checks for is_wp_error() from wp_remote_request and also parses the ActiveCampaign API’s response for specific error codes and messages. Returning WP_Error objects is the standard WordPress way to signal failures in REST API callbacks.

HTTP Requests: We use wp_remote_request() for making HTTP calls to the ActiveCampaign API. This is WordPress’s recommended way to handle external HTTP requests, as it provides better control over timeouts, headers, and error handling compared to direct cURL calls.

Making Requests to the Endpoint

Once the plugin is activated and the API credentials are set in the WordPress admin area, you can make POST requests to your custom endpoint. The endpoint URL will follow the pattern:

https://your-wordpress-site.com/wp-json/my-ac-integration/v1/contacts

Here’s an example using fetch in JavaScript (e.g., from a theme’s JavaScript file or a custom script enqueued by your plugin):

// Assuming you have a form with fields: email, first_name, last_name, phone
const form = document.getElementById('your-contact-form'); // Replace with your form ID

form.addEventListener('submit', async (event) => {
    event.preventDefault();

    const formData = new FormData(form);
    const data = Object.fromEntries(formData.entries());

    // You might need to add a nonce for better security if not relying solely on user authentication
    // For example, if you have a nonce field in your form:
    // const nonceField = document.querySelector('input[name="_wpnonce"]');
    // if (nonceField) {
    //     data._wpnonce = nonceField.value;
    // }

    try {
        const response = await fetch('/wp-json/my-ac-integration/v1/contacts', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                // If you are using nonce-based authentication, you might need to send it in headers or body
                // 'X-WP-Nonce': data._wpnonce // Example if sending nonce in header
            },
            body: JSON.stringify(data)
        });

        const result = await response.json();

        if (!response.ok) {
            // Handle API errors
            console.error('Error:', result.message || 'An unknown error occurred.');
            // Display error message to the user
        } else {
            // Handle successful submission
            console.log('Success:', result.message);
            // Display success message to the user, clear form, etc.
        }
    } catch (error) {
        console.error('Network or fetch error:', error);
        // Handle network errors
    }
});

Extending Functionality

This example focuses on creating contacts. You can extend this pattern to:

  • Retrieve existing contacts (using WP_REST_Server::READABLE and a GET request).
  • Update contacts (using WP_REST_Server::EDITABLE and a PUT/PATCH request).
  • Add contacts to specific automations or tags.
  • Trigger specific ActiveCampaign actions.
  • Handle webhooks from ActiveCampaign by creating new endpoints.

When adding new endpoints, remember to define appropriate HTTP methods, callbacks, permission checks, and argument schemas for each. Always prioritize security by validating and sanitizing all incoming data and by implementing strict permission checks.

Primary Sidebar

A little about the Author

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



Chat on WhatsApp

Recent Posts

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

Categories

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

Recent Posts

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

Top Categories

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

Our Products

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

Our Services

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

Copyright © 2026 · Vinay Vengala