WordPress Development Recipe: Secure token-based API authentication for ActiveCampaign automation API in custom plugins
Securing Custom WordPress Plugins with ActiveCampaign API Token Authentication
Integrating custom WordPress plugins with external services like ActiveCampaign requires robust authentication mechanisms. For automation APIs, token-based authentication is a standard and secure approach. This recipe details how to implement secure token-based API authentication for ActiveCampaign within a custom WordPress plugin, ensuring sensitive credentials are managed safely and API calls are authenticated correctly.
Storing ActiveCampaign API Credentials Securely
Directly embedding API keys in plugin code is a critical security vulnerability. WordPress provides several secure methods for storing sensitive information. The most appropriate for API keys that might need to be updated by an administrator is via the WordPress options API, stored in a way that is not directly accessible via the frontend and ideally encrypted or at least not exposed in plain text in the database if possible. For this recipe, we’ll use the standard WordPress options API, with a strong recommendation for further encryption if the data sensitivity warrants it.
Implementing a Settings Page
A dedicated settings page within the WordPress admin area allows administrators to input and manage their ActiveCampaign API credentials. This page should be accessible only to users with appropriate capabilities (e.g., ‘manage_options’).
Plugin File Structure (Example)
Assume a basic plugin structure:
my-activecampaign-plugin/my-activecampaign-plugin.php(Main plugin file)includes/includes/class-ac-api-manager.php(Handles API interactions)includes/class-ac-settings-page.php(Handles settings page)
Settings Page Code (includes/class-ac-settings-page.php)
This class will register a menu item and render the settings form.
<?php
/**
* Handles the ActiveCampaign API Settings Page.
*/
class AC_Settings_Page {
private $option_group = 'ac_api_settings';
private $option_name = 'ac_api_credentials';
private $api_key_field = 'ac_api_key';
private $api_url_field = 'ac_api_url';
public function __construct() {
add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
add_action( 'admin_init', array( $this, 'settings_init' ) );
}
/**
* Adds the admin menu page.
*/
public function add_admin_menu() {
add_options_page(
__( 'ActiveCampaign API Settings', 'my-activecampaign-plugin' ),
__( 'ActiveCampaign API', 'my-activecampaign-plugin' ),
'manage_options',
'activecampaign_api_settings',
array( $this, 'settings_page_html' )
);
}
/**
* Initializes the settings sections and fields.
*/
public function settings_init() {
register_setting( $this->option_group, $this->option_name, array( $this, 'sanitize_credentials' ) );
add_settings_section(
'ac_api_section',
__( 'ActiveCampaign API Configuration', 'my-activecampaign-plugin' ),
null, // Callback for section description (optional)
'activecampaign_api_settings'
);
add_settings_field(
$this->api_key_field,
__( 'API Key', 'my-activecampaign-plugin' ),
array( $this, 'api_key_render' ),
'activecampaign_api_settings',
'ac_api_section'
);
add_settings_field(
$this->api_url_field,
__( 'API URL', 'my-activecampaign-plugin' ),
array( $this, 'api_url_render' ),
'activecampaign_api_settings',
'ac_api_section'
);
}
/**
* Renders the API Key input field.
*/
public function api_key_render() {
$options = get_option( $this->option_name );
$api_key = isset( $options[$this->api_key_field] ) ? $options[$this->api_key_field] : '';
?>
<input type='text' name='option_name ); ?>[api_key_field ); ?>]' value='' class='regular-text' />
<p class="description">
<?php esc_html_e( 'Your ActiveCampaign API Key. Find it in your ActiveCampaign account under "My Settings" > "Developer".', 'my-activecampaign-plugin' ); ?>
</p>
<?php
}
/**
* Renders the API URL input field.
*/
public function api_url_render() {
$options = get_option( $this->option_name );
$api_url = isset( $options[$this->api_url_field] ) ? $options[$this->api_url_field] : '';
?>
<input type='url' name='option_name ); ?>[api_url_field ); ?>]' value='' class='regular-text' placeholder='https://youraccount.api-us1.com' />
<p class="description">
<?php esc_html_e( 'Your ActiveCampaign API URL. This typically looks like "https://youraccount.api-usX.com".', 'my-activecampaign-plugin' ); ?>
</p>
<?php
}
/**
* Sanitizes the API credentials before saving.
*
* @param array $input The raw input from the form.
* @return array Sanitized input.
*/
public function sanitize_credentials( $input ) {
$sanitized_input = array();
if ( isset( $input[$this->api_key_field] ) ) {
// Basic sanitization for API key. Consider more robust validation if needed.
$sanitized_input[$this->api_key_field] = sanitize_text_field( $input[$this->api_key_field] );
}
if ( isset( $input[$this->api_url_field] ) ) {
// Sanitize URL and ensure it's a valid format.
$sanitized_input[$this->api_url_field] = esc_url_raw( $input[$this->api_url_field] );
// Further validation to ensure it matches the expected ActiveCampaign URL pattern.
if ( ! preg_match( '/^https:\/\/[a-zA-Z0-9\-]+\.api\-[a-z0-9\-]+\.com$/', $sanitized_input[$this->api_url_field] ) ) {
add_settings_error(
$this->option_group,
'invalid_api_url',
__( 'Invalid ActiveCampaign API URL format. Please use the correct format (e.g., https://youraccount.api-us1.com).', 'my-activecampaign-plugin' ),
'error'
);
// Clear the invalid URL to prevent saving a bad value.
unset( $sanitized_input[$this->api_url_field] );
}
}
// Merge with existing options to preserve other settings if any.
$existing_options = get_option( $this->option_name );
return array_merge( (array) $existing_options, $sanitized_input );
}
/**
* Renders the settings page HTML.
*/
public function settings_page_html() {
?>
<div class="wrap">
<h1><?php esc_html_e( 'ActiveCampaign API Settings', 'my-activecampaign-plugin' ); ?></h1>
<form action="options.php" method="post">
<?php
settings_fields( $this->option_group );
do_settings_sections( 'activecampaign_api_settings' );
submit_button();
?>
</form>
</div>
<?php
}
}
// Instantiate the settings page.
new AC_Settings_Page();
Main Plugin File (my-activecampaign-plugin.php)
Include the settings page class and the API manager class.
<?php
/**
* Plugin Name: My ActiveCampaign Integration
* Description: Integrates custom functionality with ActiveCampaign API.
* Version: 1.0.0
* Author: Your Name
* Text Domain: my-activecampaign-plugin
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// Include the settings page class.
require_once plugin_dir_path( __FILE__ ) . 'includes/class-ac-settings-page.php';
// Include the API manager class.
require_once plugin_dir_path( __FILE__ ) . 'includes/class-ac-api-manager.php';
/**
* Main plugin class to initialize components.
*/
class My_ActiveCampaign_Plugin {
private static $instance = null;
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
// Settings page is handled by AC_Settings_Page class.
// API Manager will be instantiated when needed or here if global access is required.
$this->ac_api_manager = AC_API_Manager::get_instance();
}
/**
* Get the ActiveCampaign API Manager instance.
*
* @return AC_API_Manager
*/
public function get_ac_api_manager() {
return $this->ac_api_manager;
}
}
// Initialize the plugin.
My_ActiveCampaign_Plugin::get_instance();
Implementing the ActiveCampaign API Manager
This class will be responsible for retrieving the stored credentials and making authenticated API requests. We’ll use the WordPress HTTP API for making requests, which is secure and handles many underlying complexities.
API Manager Code (includes/class-ac-api-manager.php)
<?php
/**
* Handles interactions with the ActiveCampaign API.
*/
class AC_API_Manager {
private static $instance = null;
private $api_key_option_name = 'ac_api_credentials';
private $api_key_field = 'ac_api_key';
private $api_url_field = 'ac_api_url';
private $api_key;
private $api_url;
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->load_credentials();
}
/**
* Loads API credentials from WordPress options.
*/
private function load_credentials() {
$options = get_option( $this->api_key_option_name );
if ( $options && isset( $options[$this->api_key_field] ) && ! empty( $options[$this->api_key_field] ) ) {
$this->api_key = $options[$this->api_key_field];
}
if ( $options && isset( $options[$this->api_url_field] ) && ! empty( $options[$this->api_url_field] ) ) {
// Ensure the URL is properly formatted and ends with a slash for consistency.
$this->api_url = rtrim( $options[$this->api_url_field], '/' ) . '/';
}
}
/**
* Checks if API credentials are set.
*
* @return bool True if credentials are set, false otherwise.
*/
public function credentials_are_set() {
return ! empty( $this->api_key ) && ! empty( $this->api_url );
}
/**
* Makes a GET request to the ActiveCampaign API.
*
* @param string $endpoint The API endpoint (e.g., '/contacts').
* @param array $args Optional arguments for the request.
* @return array|WP_Error The API response or a WP_Error object on failure.
*/
public function get( $endpoint, $args = array() ) {
return $this->make_request( 'GET', $endpoint, $args );
}
/**
* Makes a POST request to the ActiveCampaign API.
*
* @param string $endpoint The API endpoint (e.g., '/contacts').
* @param array $args Optional arguments for the request.
* @return array|WP_Error The API response or a WP_Error object on failure.
*/
public function post( $endpoint, $args = array() ) {
return $this->make_request( 'POST', $endpoint, $args );
}
/**
* Makes a PUT request to the ActiveCampaign API.
*
* @param string $endpoint The API endpoint (e.g., '/contacts/1').
* @param array $args Optional arguments for the request.
* @return array|WP_Error The API response or a WP_Error object on failure.
*/
public function put( $endpoint, $args = array() ) {
return $this->make_request( 'PUT', $endpoint, $args );
}
/**
* Makes a DELETE request to the ActiveCampaign API.
*
* @param string $endpoint The API endpoint (e.g., '/contacts/1').
* @param array $args Optional arguments for the request.
* @return array|WP_Error The API response or a WP_Error object on failure.
*/
public function delete( $endpoint, $args = array() ) {
return $this->make_request( 'DELETE', $endpoint, $args );
}
/**
* Constructs and executes the API request.
*
* @param string $method The HTTP method (GET, POST, PUT, DELETE).
* @param string $endpoint The API endpoint.
* @param array $args Arguments for the request.
* @return array|WP_Error The API response or a WP_Error object on failure.
*/
private function make_request( $method, $endpoint, $args = array() ) {
if ( ! $this->credentials_are_set() ) {
return new WP_Error( 'ac_api_credentials_missing', __( 'ActiveCampaign API credentials are not configured.', 'my-activecampaign-plugin' ) );
}
// Ensure endpoint starts with a slash.
$endpoint = '/' . ltrim( $endpoint, '/' );
$url = $this->api_url . ltrim( $endpoint, '/' );
// ActiveCampaign API uses 'api_key' and 'api_url' for authentication.
// For POST/PUT, data is sent in the 'body'. For GET, parameters are in 'args'.
$request_args = array(
'method' => strtoupper( $method ),
'timeout' => 30, // Adjust timeout as needed.
'headers' => array(
'Api-Token' => $this->api_key, // Use Api-Token header for authentication.
'Content-Type' => 'application/json',
'Accept' => 'application/json',
),
);
if ( 'GET' === strtoupper( $method ) ) {
$request_args['args'] = $args; // For GET, args are query parameters.
} else {
// For POST, PUT, DELETE, data is in the body.
// ActiveCampaign API expects JSON payload.
$request_args['body'] = json_encode( $args );
}
// Use WordPress HTTP API for making the request.
$response = wp_remote_request( $url, $request_args );
if ( is_wp_error( $response ) ) {
return $response; // Return WP_Error object.
}
$response_code = wp_remote_retrieve_response_code( $response );
$response_body = wp_remote_retrieve_body( $response );
$decoded_body = json_decode( $response_body, true );
// Check for API-specific errors or non-2xx status codes.
if ( $response_code >= 400 || ( isset( $decoded_body['errors'] ) && ! empty( $decoded_body['errors'] ) ) ) {
// Log or handle API errors.
error_log( "ActiveCampaign API Error: Code {$response_code}, Response: " . print_r( $decoded_body, true ) );
return new WP_Error( 'ac_api_error', __( 'ActiveCampaign API request failed.', 'my-activecampaign-plugin' ), $decoded_body );
}
// ActiveCampaign API often wraps responses in a 'meta' and 'contacts'/'lists' etc. key.
// We'll return the decoded body as is, and let the caller handle parsing.
return $decoded_body;
}
/**
* Example: Get a list of contacts.
*
* @param array $params Query parameters for the contacts endpoint.
* @return array|WP_Error
*/
public function get_contacts( $params = array() ) {
return $this->get( '/contacts', $params );
}
/**
* Example: Add a new contact.
*
* @param array $contact_data Data for the new contact.
* @return array|WP_Error
*/
public function add_contact( $contact_data ) {
// ActiveCampaign API expects contact data under a 'contact' key.
return $this->post( '/contacts', array( 'contact' => $contact_data ) );
}
/**
* Example: Update an existing contact.
*
* @param int $contact_id The ID of the contact to update.
* @param array $contact_data Data to update.
* @return array|WP_Error
*/
public function update_contact( $contact_id, $contact_data ) {
// ActiveCampaign API expects contact data under a 'contact' key.
return $this->put( "/contacts/{$contact_id}", array( 'contact' => $contact_data ) );
}
}
Using the API Manager in Your Plugin
Once the settings are saved and the API manager is initialized, you can make API calls from anywhere in your plugin’s logic.
Example Usage in Another Plugin File
Let’s say you have a function that adds a new user to ActiveCampaign upon WordPress user registration.
<?php
/**
* Hook into user registration to add contact to ActiveCampaign.
*/
function my_ac_add_contact_on_registration( $user_id ) {
// Get the API Manager instance.
$ac_api_manager = AC_API_Manager::get_instance();
// Check if credentials are set. If not, log an error or do nothing.
if ( ! $ac_api_manager->credentials_are_set() ) {
error_log( 'ActiveCampaign API credentials not set. Cannot add contact.' );
return;
}
// Get user data.
$user_info = get_userdata( $user_id );
if ( ! $user_info ) {
return;
}
$contact_data = array(
'email' => $user_info->user_email,
'firstName' => $user_info->user_firstname,
'lastName' => $user_info->user_lastname,
// Add custom fields or tags as needed.
// 'field[1]' => 'Custom Value', // Example for a custom field with ID 1
// 'tags' => 'New User, WordPress',
);
// Make the API call to add the contact.
$result = $ac_api_manager->add_contact( $contact_data );
if ( is_wp_error( $result ) ) {
// Handle the error, e.g., log it.
error_log( 'Failed to add contact to ActiveCampaign: ' . $result->get_error_message() );
} else {
// Success! Log or process the response.
// The response might contain the new contact ID.
if ( isset( $result['contact']['id'] ) ) {
// Optionally, save the ActiveCampaign contact ID to user meta.
update_user_meta( $user_id, 'activecampaign_contact_id', $result['contact']['id'] );
error_log( "Successfully added contact {$user_info->user_email} to ActiveCampaign. AC ID: {$result['contact']['id']}" );
} else {
error_log( "Successfully added contact {$user_info->user_email} to ActiveCampaign, but no ID returned in response." );
}
}
}
// Hook into the 'user_register' action.
add_action( 'user_register', 'my_ac_add_contact_on_registration', 10, 1 );
Advanced Considerations and Best Practices
Error Handling and Logging
The provided code includes basic error logging using error_log(). For production environments, consider a more sophisticated logging solution, such as integrating with a dedicated logging service or using a WordPress plugin for enhanced logging capabilities. This is crucial for debugging API integration issues.
Rate Limiting
ActiveCampaign, like most APIs, has rate limits. Implement retry mechanisms with exponential backoff for transient errors (e.g., 429 Too Many Requests) and ensure your plugin doesn’t exceed the allowed request frequency. The WordPress HTTP API doesn’t natively handle this, so it needs to be implemented in your API manager.
Data Validation and Sanitization
Always validate and sanitize data before sending it to the ActiveCampaign API, and also when receiving data from it. The sanitize_credentials method provides a starting point. For API payloads, ensure data types and formats match ActiveCampaign’s requirements.
Security Enhancements
While storing credentials in the WordPress options table is standard, for extremely sensitive environments, consider:
- Encryption: Encrypt the API key and URL before storing them in the database and decrypt them only when needed for API calls. WordPress doesn’t have a built-in robust encryption API for arbitrary data, so you might need to implement AES encryption using PHP’s OpenSSL extension.
- Environment Variables: If your WordPress hosting environment supports it (e.g., via a `wp-config.php` modification or a hosting panel), consider storing sensitive keys in environment variables and accessing them via PHP’s `$_ENV` or `getenv()`. This keeps them out of the database entirely.
- Access Control: Ensure the settings page is protected by appropriate WordPress capabilities.
API Versioning and Endpoint Changes
ActiveCampaign API endpoints can change. Regularly review their API documentation for updates. Your API manager should be designed to be adaptable to these changes, perhaps by using constants for endpoints or a more dynamic configuration.
Testing
Thoroughly test your integration in a staging environment before deploying to production. Test various scenarios, including successful API calls, error conditions, and credential management.