Developer guide for custom integrations
Client Portal is built on WordPress custom post types and uses standard WordPress hooks, making it extensible for developers who want to build custom integrations.
This documentation is provided for developers who want to build custom integrations. We're unable to provide support for custom code, but this should point you in the right direction.
Custom post types
Client Portal uses three custom post types:
Post type | Description |
|---|---|
| Portals - the main project pages your clients see |
| Content pages - nested pages within portals |
| Portal templates - for creating new portals from a template |
Since these are standard WordPress custom post types, you can query them using WP_Query, get_posts(), or any standard WordPress function.
Action hooks
These hooks fire at key moments and allow you to trigger your own code (e.g., sync with a CRM, send notifications, etc.).
User and portal events
leco_cp_registered_user
Fires when a new client user registers through the Client Portal registration form.
add_action( 'leco_cp_registered_user', function( $user_id, $user_data ) {
// $user_id - The newly created user's ID
// $user_data - Array of user data from the registration form
// Example: Send to your CRM
my_crm_api_create_contact( $user_data['user_email'], $user_data['first_name'] );
}, 10, 2 );leco_cp_project_created
Fires when a portal is created and assigned to a client.
add_action( 'leco_cp_project_created', function( $post_id, $user_id ) {
// $post_id - The portal post ID
// $user_id - The client user ID
// Example: Log to external system
my_api_log_event( 'portal_created', [
'portal_id' => $post_id,
'portal_title' => get_the_title( $post_id ),
'client_id' => $user_id,
]);
}, 10, 2 );Module events
leco_cp_module_completed
Fires when a client marks a module as complete.
add_action( 'leco_cp_module_completed', function( $post_id, $phase, $module ) {
// $post_id - The portal post ID
// $phase - The phase/section identifier (e.g., "part_0")
// $module - The module's key within that phase
// Example: Notify via Slack
wp_remote_post( 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL', [
'body' => json_encode([
'text' => 'A client completed a module in ' . get_the_title( $post_id )
])
]);
}, 10, 3 );leco_cp_module_activated
Fires when a module is activated (marked as ready for the client).
add_action( 'leco_cp_module_activated', function( $post_id, $phase, $module ) {
// $post_id - The portal post ID
// $phase - The phase identifier
// $module - The module's key
// Your code here
}, 10, 3 );leco_cp_client_uploaded
Fires when a client uploads a file to a module.
add_action( 'leco_cp_client_uploaded', function( $post_id, $phase, $module, $user_id ) {
// $post_id - The portal post ID
// $phase - The phase identifier
// $module - The module's key
// $user_id - The client who uploaded
// Your code here
}, 10, 4 );Standard WordPress hooks
These standard WordPress hooks also work with Client Portal post types:
// Fires when any portal is created or updated
add_action( 'save_post_leco_client', function( $post_id, $post, $update ) {
if ( ! $update ) {
// New portal created
}
}, 10, 3 );
// Fires when any user is registered (WordPress core hook)
add_action( 'user_register', function( $user_id ) {
$user = get_userdata( $user_id );
if ( in_array( 'leco_client', $user->roles ) ) {
// This is a Client Portal client
}
});Programmatically creating portals
You can create portals programmatically using \LECO\Client_Portal\Project::create().
client_id and template_id are required. post_title is optional.
\LECO\Client_Portal\Project::create( array(
'template_id' => 123, // Template ID to copy content from
'client_id' => $user_id, // Required: User ID to assign
'post_title' => 'My Portal' // Optional: Custom title (defaults to "New Portal")
) );If you don’t provide a post_title, the method checks the client’s leco_cp_user_project_title user meta value. If that’s empty, it defaults to New Portal.
Example: Auto-create a portal when a user gets the client role
// Standard WordPress hook (works with most scenarios)
add_action( 'set_user_role', function( $user_id, $role, $old_roles ) {
if ( 'leco_client' !== $role ) {
return;
}
$user = get_userdata( $user_id );
\LECO\Client_Portal\Project::create( array(
'template_id' => 6,
'client_id' => $user_id,
'post_title' => $user->display_name . "'s Portal",
) );
}, 10, 3 ); Example: Role assignment via add_user_role
If your integration assigns roles with add_user_role, use that hook instead. It only passes two parameters.
add_action( 'add_user_role', function( $user_id, $role ) {
if ( 'leco_client' !== $role ) {
return;
}
$user = get_userdata( $user_id );
\LECO\Client_Portal\Project::create( array(
'template_id' => 6,
'client_id' => $user_id,
'post_title' => $user->display_name . "'s Portal",
) );
}, 10, 2 );Filters
Filters allow you to modify data or behavior.
Redirects
leco_cp_login_redirect
Change where users are redirected after login.
add_filter( 'leco_cp_login_redirect', function( $redirect_url, $request, $user ) {
// Customize redirect based on user
return $redirect_url;
}, 10, 3 );Notifications
leco_cp_admin_email_to
Change who receives admin notification emails.
add_filter( 'leco_cp_admin_email_to', function( $admin_email, $post_id, $phase, $module ) {
// Send to a different email based on the project
return '[email protected]';
}, 10, 4 );Data structure
Portal data is stored using WordPress post meta with the leco_cp_ prefix.
Key meta fields
Portal meta (leco_client post type):
Meta key | Description |
|---|---|
| Array of client user IDs assigned to the portal |
| Title for phase n (e.g., "Getting Started", "In Progress") |
| Serialized array of modules in phase n (0-indexed) |
| Number of phases in the portal |
| Template ID if created from a template |
| Whether the portal is publicly accessible ('yes' or 'no') |
| Portal-specific notification settings |
User meta:
Meta key | Description |
|---|---|
| Portal ID assigned to this user |
| Organization name from registration |
| Custom project title for user |
Example: Get a client's portal
$user_id = get_current_user_id();
$portal_id = get_user_meta( $user_id, 'leco_cp_project', true );
if ( $portal_id ) {
$portal = get_post( $portal_id );
// Work with the portal
}Example: Get all clients for a portal
$portal_id = 123;
$client_ids = get_post_meta( $portal_id, 'leco_cp_client', true );
if ( is_array( $client_ids ) ) {
foreach ( $client_ids as $client_id ) {
$user = get_userdata( $client_id );
// Work with each client
}
}Complete example: CRM integration
Here's a complete example that syncs new portals to an external CRM:
<?php
/**
* Plugin Name: Client Portal CRM Integration
* Description: Syncs Client Portal data to our CRM
*/
// When a new client registers and gets a portal
add_action( 'leco_cp_project_created', 'my_crm_sync_new_portal', 10, 2 );
function my_crm_sync_new_portal( $portal_id, $user_id ) {
$user = get_userdata( $user_id );
$portal = get_post( $portal_id );
// Prepare data for CRM
$data = [
'contact_email' => $user->user_email,
'contact_name' => $user->display_name,
'project_name' => $portal->post_title,
'project_url' => get_permalink( $portal_id ),
'created_at' => current_time( 'mysql' ),
];
// Send to CRM API
wp_remote_post( 'https://api.yourcrm.com/contacts', [
'headers' => [
'Authorization' => 'Bearer ' . MY_CRM_API_KEY,
'Content-Type' => 'application/json',
],
'body' => json_encode( $data ),
]);
}
// When a module is completed, update CRM
add_action( 'leco_cp_module_completed', 'my_crm_log_progress', 10, 3 );
function my_crm_log_progress( $portal_id, $phase, $module ) {
// Get the client(s) for this portal
$client_ids = get_post_meta( $portal_id, 'leco_cp_client', true );
if ( ! is_array( $client_ids ) ) {
return;
}
foreach ( $client_ids as $client_id ) {
$user = get_userdata( $client_id );
// Log activity in CRM
wp_remote_post( 'https://api.yourcrm.com/activities', [
'headers' => [
'Authorization' => 'Bearer ' . MY_CRM_API_KEY,
'Content-Type' => 'application/json',
],
'body' => json_encode([
'contact_email' => $user->user_email,
'activity_type' => 'module_completed',
'project_name' => get_the_title( $portal_id ),
]),
]);
}
}Tips for developers
Test on staging first - Always test custom integrations on a staging site before deploying to production.
Use a custom plugin - Put your integration code in a custom plugin rather than your theme's functions.php. This keeps it separate and easier to maintain.
Handle errors gracefully - External API calls can fail. Use wp_remote_post() with proper error checking.
Explore the database - Install the plugin on a test site and explore the wp_postmeta table to see how data is structured.