WordPress Development Recipe: Secure token-based API authentication for Pipedrive custom leads API in custom plugins
Setting Up Secure Token-Based API Authentication for Pipedrive Custom Leads
This recipe details the implementation of secure token-based authentication for a custom WordPress plugin interacting with the Pipedrive API, specifically for managing custom lead fields. We’ll focus on generating, storing, and validating API tokens to ensure secure data exchange.
Prerequisites
- A functional WordPress installation.
- A Pipedrive account with API access enabled.
- Basic understanding of WordPress plugin development and REST API concepts.
- Composer installed for dependency management.
1. Generating and Storing Pipedrive API Tokens
For security and manageability, Pipedrive API tokens should not be hardcoded. We’ll implement a mechanism within your WordPress plugin to securely store these tokens. A common approach is to use WordPress’s options API, but for enhanced security, consider encrypting sensitive data or using environment variables if your hosting environment supports it. For this recipe, we’ll demonstrate storing the token in the WordPress options table, with a note on encryption.
1.1. User Interface for Token Input
Create an administration page within your plugin to allow users to input their Pipedrive API token. This page should be accessible from the WordPress admin menu.
Plugin File Structure (Example)
Assume your plugin is structured as follows:
my-pipedrive-plugin/my-pipedrive-plugin.php(main plugin file)admin/admin/class-pipedrive-admin.phpincludes/includes/class-pipedrive-api.php
admin/class-pipedrive-admin.php
This class will handle the creation of the admin menu and the settings page.
<?php
/**
* Pipedrive Admin Class.
*/
class Pipedrive_Admin {
/**
* Constructor.
*/
public function __construct() {
add_action( 'admin_menu', array( $this, 'add_plugin_page' ) );
add_action( 'admin_init', array( $this, 'page_init' ) );
}
/**
* Add options page.
*/
public function add_plugin_page() {
add_options_page(
__( 'Pipedrive API Settings', 'my-pipedrive-plugin' ),
__( 'Pipedrive API', 'my-pipedrive-plugin' ),
'manage_options',
'pipedrive-api-settings',
array( $this, 'create_admin_page' )
);
}
/**
* Options page callback.
*/
public function create_admin_page() {
?>
<div class="wrap">
<h1><?php _e( 'Pipedrive API Settings', 'my-pipedrive-plugin' ); ?></h1>
<form method="post" action="options.php">
</form>
</div>
<?php
}
/**
* Register and add settings.
*/
public function page_init() {
register_setting(
'pipedrive_option_group', // Option group
'pipedrive_api_token', // Option name
array( $this, 'sanitize_api_token' ) // Sanitize callback
);
add_settings_section(
'pipedrive_setting_section', // ID
__( 'Pipedrive API Configuration', 'my-pipedrive-plugin' ), // Title
array( $this, 'print_section_info' ), // Callback
'pipedrive-api-settings' // Page
);
add_settings_field(
'pipedrive_api_token_field', // ID
__( 'Pipedrive API Token', 'my-pipedrive-plugin' ), // Title
array( $this, 'api_token_callback' ), // Callback
'pipedrive-api-settings', // Page
'pipedrive_setting_section' // Section
);
}
/**
* Print the section introduction.
*/
public function print_section_info() {
print __( 'Enter your Pipedrive API token below. You can find this in your Pipedrive account settings under "Personal preferences" > "API".', 'my-pipedrive-plugin' );
}
/**
* Get the html for the API token field.
*/
public function api_token_callback() {
$api_token = get_option( 'pipedrive_api_token' );
printf(
'<input type="text" id="pipedrive_api_token_field" name="pipedrive_api_token" value="%s" class="regular-text" />',
esc_attr( $api_token )
);
}
/**
* Sanitize API token input.
*
* @param string $input The input from the user.
* @return string The sanitized input.
*/
public function sanitize_api_token( $input ) {
// Basic sanitization: remove whitespace.
// For enhanced security, consider encryption here.
return sanitize_text_field( trim( $input ) );
}
}
// Initialize the admin class
if ( is_admin() ) {
new Pipedrive_Admin();
}
1.2. Storing the Token Securely
The `register_setting` function in `admin/class-pipedrive-admin.php` saves the token to the `wp_options` table. While `sanitize_text_field` provides basic sanitization, for production environments, consider encrypting the token before storing it. WordPress doesn’t have a built-in robust encryption API for arbitrary data, but you can leverage libraries like the defuse/php-encryption library. This would involve generating an encryption key (stored securely, perhaps in `wp-config.php` as a constant) and encrypting/decrypting the token whenever it’s read or written.
2. Implementing the Pipedrive API Client
Create a class to handle all interactions with the Pipedrive API. This class will retrieve the stored API token and use it for authentication.
includes/class-pipedrive-api.php
<?php
/**
* Pipedrive API Client Class.
*/
class Pipedrive_API_Client {
private $api_token;
private $api_base_url = 'https://api.pipedrive.com/v1/';
/**
* Constructor.
*/
public function __construct() {
$this->api_token = get_option( 'pipedrive_api_token' );
// If using encryption, decrypt the token here.
// Example: $this->api_token = Pipedrive_Encryption::decrypt( get_option( 'pipedrive_api_token_encrypted' ) );
}
/**
* Check if API token is set.
*
* @return bool
*/
public function is_token_set() {
return ! empty( $this->api_token );
}
/**
* Make a GET request to the Pipedrive API.
*
* @param string $endpoint The API endpoint.
* @param array $args Optional arguments.
* @return array|WP_Error The API response or a WP_Error object.
*/
public function get( $endpoint, $args = array() ) {
return $this->request( 'GET', $endpoint, $args );
}
/**
* Make a POST request to the Pipedrive API.
*
* @param string $endpoint The API endpoint.
* @param array $args Optional arguments.
* @return array|WP_Error The API response or a WP_Error object.
*/
public function post( $endpoint, $args = array() ) {
return $this->request( 'POST', $endpoint, $args );
}
/**
* Make a PUT request to the Pipedrive API.
*
* @param string $endpoint The API endpoint.
* @param array $args Optional arguments.
* @return array|WP_Error The API response or a WP_Error object.
*/
public function put( $endpoint, $args = array() ) {
return $this->request( 'PUT', $endpoint, $args );
}
/**
* Make a DELETE request to the Pipedrive API.
*
* @param string $endpoint The API endpoint.
* @param array $args Optional arguments.
* @return array|WP_Error The API response or a WP_Error object.
*/
public function delete( $endpoint, $args = array() ) {
return $this->request( 'DELETE', $endpoint, $args );
}
/**
* Core request method.
*
* @param string $method HTTP method (GET, POST, PUT, DELETE).
* @param string $endpoint The API endpoint.
* @param array $args Optional arguments.
* @return array|WP_Error The API response or a WP_Error object.
*/
private function request( $method, $endpoint, $args = array() ) {
if ( ! $this->is_token_set() ) {
return new WP_Error( 'pipedrive_api_error', __( 'Pipedrive API token is not configured.', 'my-pipedrive-plugin' ) );
}
$url = trailingslashit( $this->api_base_url ) . ltrim( $endpoint, '/' );
$request_args = array(
'method' => strtoupper( $method ),
'headers' => array(
'Authorization' => 'Bearer ' . $this->api_token,
'Content-Type' => 'application/json',
),
'timeout' => 30, // Adjust timeout as needed
);
if ( 'GET' === strtoupper( $method ) ) {
// For GET requests, arguments are typically query parameters.
if ( ! empty( $args ) ) {
$url = add_query_arg( $args, $url );
}
} else {
// For POST, PUT, DELETE, arguments are in the body.
if ( ! empty( $args ) ) {
$request_args['body'] = json_encode( $args );
}
}
$response = wp_remote_request( $url, $request_args );
if ( is_wp_error( $response ) ) {
return $response;
}
$response_code = wp_remote_retrieve_response_code( $response );
$response_body = wp_remote_retrieve_body( $response );
$data = json_decode( $response_body, true );
if ( $response_code >= 400 ) {
// Pipedrive API errors are usually in the 'error' key.
$error_message = isset( $data['error'] ) ? $data['error'] : $response_body;
return new WP_Error( 'pipedrive_api_error', sprintf( __( 'Pipedrive API Error (%d): %s', 'my-pipedrive-plugin' ), $response_code, $error_message ) );
}
return $data;
}
/**
* Example: Get custom fields for leads.
*
* @return array|WP_Error
*/
public function get_lead_custom_fields() {
return $this->get( 'leadFields' );
}
/**
* Example: Create a new lead.
*
* @param array $lead_data Lead data.
* @return array|WP_Error
*/
public function create_lead( $lead_data ) {
// Ensure custom fields are passed correctly.
// Pipedrive expects custom fields in a 'custom_fields' array.
// Example: $lead_data['custom_fields'] = ['field_id_1' => 'value1', 'field_id_2' => 'value2'];
return $this->post( 'leads', $lead_data );
}
// Add more methods for other Pipedrive API operations as needed.
}
3. Integrating with WordPress Actions and Filters
Now, let’s hook into WordPress to use the Pipedrive API client. For instance, you might want to sync custom lead data when a WordPress form is submitted or when a specific post type is created.
Example: Syncing Data on Form Submission
Assume you have a form submission handler. You would instantiate the `Pipedrive_API_Client` and use its methods.
<?php
// In your main plugin file (my-pipedrive-plugin.php) or a dedicated handler file.
// Include the API client class.
require_once plugin_dir_path( __FILE__ ) . 'includes/class-pipedrive-api.php';
/**
* Handles form submission and syncs data to Pipedrive.
*/
function my_pipedrive_handle_form_submission() {
// Check if the form was submitted and if the nonce is valid.
if ( isset( $_POST['my_pipedrive_form_submit'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'my_pipedrive_submit_nonce' ) ) {
$pipedrive_client = new Pipedrive_API_Client();
if ( ! $pipedrive_client->is_token_set() ) {
// Handle error: API token not configured.
// You might want to display an admin notice or log this.
error_log( 'Pipedrive API token is not set. Cannot sync lead.' );
return;
}
// Prepare lead data from $_POST.
$lead_data = array(
'title' => sanitize_text_field( $_POST['lead_title'] ),
'person_id' => isset( $_POST['person_id'] ) ? intval( $_POST['person_id'] ) : null, // Example: Link to an existing person
'organization_id' => isset( $_POST['organization_id'] ) ? intval( $_POST['organization_id'] ) : null, // Example: Link to an existing organization
// Add other standard Pipedrive lead fields as needed.
);
// Handle custom fields.
// Assuming your form has fields like 'custom_field_email', 'custom_field_phone'.
// You need to know the Pipedrive custom field IDs.
$custom_fields_to_sync = array();
if ( ! empty( $_POST['custom_field_email'] ) ) {
// Replace 'YOUR_EMAIL_CUSTOM_FIELD_ID' with the actual ID from Pipedrive.
$custom_fields_to_sync['YOUR_EMAIL_CUSTOM_FIELD_ID'] = sanitize_email( $_POST['custom_field_email'] );
}
if ( ! empty( $_POST['custom_field_phone'] ) ) {
// Replace 'YOUR_PHONE_CUSTOM_FIELD_ID' with the actual ID from Pipedrive.
$custom_fields_to_sync['YOUR_PHONE_CUSTOM_FIELD_ID'] = sanitize_text_field( $_POST['custom_field_phone'] );
}
if ( ! empty( $custom_fields_to_sync ) ) {
$lead_data['custom_fields'] = $custom_fields_to_sync;
}
// Make the API call to create the lead.
$result = $pipedrive_client->create_lead( $lead_data );
if ( is_wp_error( $result ) ) {
// Handle API error.
error_log( 'Pipedrive lead creation failed: ' . $result->get_error_message() );
// You might want to redirect back with an error message.
} else {
// Lead created successfully.
// $result will contain the Pipedrive API response.
$lead_id = $result['data']['id'];
// You can now store this lead_id in your WordPress database if needed.
// For example, as a post meta if this form submission is tied to a post.
// update_post_meta( $post_id, '_pipedrive_lead_id', $lead_id );
}
}
}
// Hook this function to your form submission action.
// For example, if you're using a shortcode and processing it within the shortcode handler.
// Or if you have a specific AJAX endpoint.
// add_action( 'my_custom_form_submission_hook', 'my_pipedrive_handle_form_submission' );
?>
4. Handling API Errors and Rate Limiting
Pipedrive’s API has rate limits. Your client should be designed to handle these gracefully. The `WP_Error` objects returned by `wp_remote_request` and our `Pipedrive_API_Client` are crucial for this.
4.1. Error Handling Strategy
Always check the return value of API calls for `is_wp_error()`. Log errors for debugging. Inform the user if the action cannot be completed due to an API issue. For critical operations, consider implementing retry mechanisms with exponential backoff for transient errors (e.g., 5xx server errors or rate limit exceeded errors).
4.2. Rate Limiting Considerations
Pipedrive’s API typically returns a 429 Too Many Requests status code when rate limits are exceeded. Your `request` method in `Pipedrive_API_Client` should detect this specific error code and implement a retry strategy. This might involve pausing execution for a short period (e.g., 5-10 seconds) and retrying the request. For more sophisticated handling, you might want to cache API responses for a short duration or implement a queueing system for outgoing API requests.
// Inside the Pipedrive_API_Client::request method, after checking for WP_Error:
$response_code = wp_remote_retrieve_response_code( $response );
$response_body = wp_remote_retrieve_body( $response );
$data = json_decode( $response_body, true );
// Check for rate limiting (429)
if ( 429 === $response_code ) {
// Implement retry logic here. This is a simplified example.
// A more robust solution would involve a loop, retry count, and backoff.
$retry_after = $response->get_header( 'Retry-After' ); // Pipedrive might provide this header
$delay = $retry_after ? intval( $retry_after ) : 10; // Default to 10 seconds if header is missing
error_log( sprintf( 'Pipedrive API rate limit exceeded. Retrying in %d seconds.', $delay ) );
sleep( $delay ); // Pause execution
// You would typically call $this->request() again here,
// possibly with a retry counter to prevent infinite loops.
// For simplicity, this example just logs and returns an error.
return new WP_Error( 'pipedrive_api_rate_limit', __( 'Pipedrive API rate limit exceeded. Please try again later.', 'my-pipedrive-plugin' ) );
}
if ( $response_code >= 400 ) {
// ... existing error handling ...
}
// ... rest of the method
5. Best Practices and Further Enhancements
- Encryption: As mentioned, encrypting the API token is highly recommended for production. Use a strong encryption library and manage your encryption keys securely.
- Environment Variables: If your hosting allows, consider using environment variables for API keys instead of storing them in the WordPress options table. This is generally more secure.
- Logging: Implement comprehensive logging for all API interactions, including successful requests and errors. This is invaluable for debugging.
- Asynchronous Operations: For high-volume operations, consider using WordPress Cron or a dedicated job queue system (like Redis Queue or RabbitMQ) to process Pipedrive API requests asynchronously. This prevents long page load times and improves user experience.
- Dependency Management: Use Composer to manage external libraries (like encryption libraries) for your plugin.
- Input Validation: Always validate and sanitize all data before sending it to the Pipedrive API, and also validate data received from Pipedrive.
- Custom Field Mapping: For custom fields, maintain a clear mapping between your WordPress form fields/data structures and Pipedrive’s custom field IDs. This mapping could be stored in plugin settings or a dedicated configuration file.
Conclusion
By following this recipe, you can implement a robust and secure token-based authentication system for your WordPress plugin’s Pipedrive API integrations. Remember to prioritize security by encrypting sensitive credentials and to handle API errors and rate limits effectively for a reliable user experience.