Build a page where a member signs into WordPress and sees their own membership dates from campai. You will install a small plugin, connect one mandate, link a website account, and publish the page. The complete plugin is included below.
WordPress handles the login. Your WordPress server reads the linked contact from the campai API and shows only the two membership dates. This is an external integration running on your website; it does not require a campai developer app.
What you’ll have at the end
A private WordPress page showing the signed-in member’s own membership dates.
- Member signs in
- WordPress resolves the account link
- Server reads campai
- Only their dates appear
Before you start
- WordPress administrator access and PHP 8.2+
- A key with Mandates Read and Members Read
- Two verified website accounts and their campai contact IDs
On this page
You need a WordPress administrator account that can install plugins, PHP 8.2 or newer, and HTTPS on the website you publish. Start on a private staging copy with two accounts you control.
- Create an API key with Mandates → Read and Members → Read. Restrict it to the mandate you want this website to use.
- Find your full organization and mandate IDs. Keep all 24 characters; abbreviated IDs from a URL are not enough.
- Choose a member contact whose identity you can verify, and copy its full contact
_id. If you need to find it through the API, use List contacts in a private terminal. - Confirm which WordPress account belongs to that person. An email match alone is not an ownership check. Use your existing account-verification process or verify directly with the person before linking.
Ready to continue: you have one restricted key, two scope IDs, and a verified pair of WordPress account and campai contact ID. Keep these values out of public pages and screenshots.
Download the ZIP below. In WordPress, open Plugins → Add Plugin → Upload Plugin, select the ZIP, choose Install Now, then Activate Plugin. The installed plugin is named campai member area.
The complete WordPress plugin
One PHP file. Install the ZIP or copy the source into wp-content/plugins/campai-member-area/campai-member-area.php, then activate it.
Download ZIPView and copy the complete source
The complete WordPress plugin
<?php
/**
* Plugin Name: campai member area
* Description: A private membership profile for explicitly linked WordPress accounts.
* Version: 1.0.0
* Requires PHP: 8.2
* License: GPL-2.0-or-later
*/
if (!defined('ABSPATH')) exit;
function campai_guide_config() {
return wp_parse_args(get_option('campai_guide_settings', []), [
'organization' => '', 'mandate' => '', 'key' => '',
]);
}
function campai_guide_scope($config) {
return hash('sha256', $config['organization'] . ':' . $config['mandate']);
}
function campai_guide_id($value) {
return is_string($value) && preg_match('/^[a-f0-9]{24}$/iD', $value);
}
function campai_guide_post_text($name) {
$value = $_POST[$name] ?? '';
if (!is_string($value)) wp_die('Expected a text value.', '', ['response' => 400]);
return trim(wp_unslash($value));
}
add_action('admin_menu', function () {
add_options_page('campai member area', 'campai member area', 'manage_options', 'campai-member-area', 'campai_guide_settings_page');
});
add_action('admin_post_campai_guide_settings', function () {
if (!current_user_can('manage_options')) wp_die('Administrator access required.', '', ['response' => 403]);
check_admin_referer('campai_guide_settings');
$config = campai_guide_config();
$organization = strtolower(campai_guide_post_text('organization'));
$mandate = strtolower(campai_guide_post_text('mandate'));
$key = campai_guide_post_text('api_key');
if (!campai_guide_id($organization) || !campai_guide_id($mandate)) wp_die('Use full 24-character organization and mandate IDs.');
if ($key === '') $key = $config['key'];
if ($key === '' || strlen($key) > 4096 || preg_match('/[\x00-\x20\x7f]/', $key)) wp_die('Enter the complete API key without spaces.');
update_option('campai_guide_settings', compact('organization', 'mandate', 'key'), false);
wp_safe_redirect(admin_url('options-general.php?page=campai-member-area&saved=1'));
exit;
});
function campai_guide_settings_page() {
if (!current_user_can('manage_options')) return;
$config = campai_guide_config();
?>
<div class="wrap">
<h1>campai member area</h1>
<p>Connect WordPress to one campai organization and mandate. Only an administrator can link accounts.</p>
<?php if (isset($_GET['saved'])): ?><div class="notice notice-success"><p>Settings saved. Link a user, then add the shortcode to a page.</p></div><?php endif; ?>
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<input type="hidden" name="action" value="campai_guide_settings">
<?php wp_nonce_field('campai_guide_settings'); ?>
<table class="form-table" role="presentation">
<tr><th><label for="campai-organization">Organization ID</label></th><td><input class="regular-text" id="campai-organization" name="organization" required pattern="[a-fA-F0-9]{24}" value="<?php echo esc_attr($config['organization']); ?>"></td></tr>
<tr><th><label for="campai-mandate">Mandate ID</label></th><td><input class="regular-text" id="campai-mandate" name="mandate" required pattern="[a-fA-F0-9]{24}" value="<?php echo esc_attr($config['mandate']); ?>"></td></tr>
<tr><th><label for="campai-key">API key</label></th><td><input class="regular-text" type="password" id="campai-key" name="api_key" autocomplete="new-password" value=""><p class="description"><?php echo $config['key'] ? 'A key is stored. Leave blank to keep it, or enter a replacement.' : 'Enter a key with Mandates Read and Members Read. It stays on your server.'; ?></p></td></tr>
</table>
<?php submit_button('Save connection'); ?>
</form>
<h2>Finish the setup</h2>
<ol><li>Open Users and edit the intended website account.</li><li>Enter its verified campai contact ID in the campai member area section.</li><li>Add <code>[campai_member_area]</code> in a Shortcode block on a new page.</li><li>Exclude that page from every page cache and CDN cache.</li></ol>
<p>The API key is stored in the WordPress options table. Restrict database and backup access. Deactivating this plugin does not revoke the key.</p>
</div>
<?php
}
function campai_guide_user_fields($user) {
if (!current_user_can('manage_options') || !current_user_can('edit_user', $user->ID)) return;
$binding = get_user_meta($user->ID, '_campai_guide_binding', true);
$binding = is_array($binding) ? $binding : [];
wp_nonce_field('campai_guide_user_' . $user->ID, 'campai_guide_user_nonce');
?>
<h2>campai member area</h2>
<table class="form-table" role="presentation"><tr><th><label for="campai-contact">Verified contact ID</label></th><td>
<input class="regular-text" id="campai-contact" name="campai_contact" pattern="[a-fA-F0-9]{24}" value="<?php echo esc_attr($binding['contact'] ?? ''); ?>">
<p class="description">Link only after verifying this account belongs to that person. Leave blank to remove access. Saving binds the contact to the currently configured organization and mandate.</p>
</td></tr></table>
<?php
}
add_action('show_user_profile', 'campai_guide_user_fields');
add_action('edit_user_profile', 'campai_guide_user_fields');
function campai_guide_save_user($user_id) {
if (!current_user_can('manage_options') || !current_user_can('edit_user', $user_id)) return;
if (!isset($_POST['campai_guide_user_nonce']) || !is_string($_POST['campai_guide_user_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['campai_guide_user_nonce'])), 'campai_guide_user_' . $user_id)) return;
$contact = campai_guide_post_text('campai_contact');
if ($contact === '') { delete_user_meta($user_id, '_campai_guide_binding'); return; }
if (!campai_guide_id($contact)) wp_die('Use a full 24-character contact ID.');
$config = campai_guide_config();
if (!campai_guide_id($config['organization']) || !campai_guide_id($config['mandate'])) wp_die('Save the campai connection first.');
update_user_meta($user_id, '_campai_guide_binding', ['contact' => strtolower($contact), 'scope' => campai_guide_scope($config)]);
}
add_action('personal_options_update', 'campai_guide_save_user');
add_action('edit_user_profile_update', 'campai_guide_save_user');
add_action('template_redirect', function () {
if (is_singular() && has_shortcode((string) get_post_field('post_content', get_queried_object_id()), 'campai_member_area')) {
if (!defined('DONOTCACHEPAGE')) define('DONOTCACHEPAGE', true);
nocache_headers();
}
});
function campai_guide_date($value) {
if ($value === null || $value === '') return 'Not set';
if (!is_string($value)) return 'Unavailable';
try { return (new DateTimeImmutable($value))->format('F j, Y'); }
catch (Exception $error) { return 'Unavailable'; }
}
function campai_guide_member_area() {
if (!is_user_logged_in()) {
return '<p><a href="' . esc_url(wp_login_url(get_permalink())) . '">Sign in to view your membership</a></p>';
}
$config = campai_guide_config();
$binding = get_user_meta(get_current_user_id(), '_campai_guide_binding', true);
if (!is_array($binding) || !campai_guide_id($binding['contact'] ?? null) ||
!hash_equals(campai_guide_scope($config), (string) ($binding['scope'] ?? ''))) {
return '<p>Your website account is not linked. Contact your club administrator.</p>';
}
if ($config['key'] === '' || !campai_guide_id($config['organization']) || !campai_guide_id($config['mandate'])) {
return '<p>Membership details are temporarily unavailable. Please try again later.</p>';
}
// The visitor cannot choose the host, organization, mandate, or contact.
$url = 'https://cloud.campai.com/api/' . $config['organization'] . '/' . $config['mandate'] . '/crm/contacts/' . $binding['contact'];
$response = wp_remote_get($url, [
'headers' => ['X-API-Key' => $config['key']],
'timeout' => 10, 'redirection' => 0, 'limit_response_size' => 262144,
]);
if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
return '<p>Membership details are temporarily unavailable. Please try again later.</p>';
}
$contact = json_decode(wp_remote_retrieve_body($response), true);
if (!is_array($contact) || ($contact['_id'] ?? null) !== $binding['contact'] ||
!is_array($contact['types'] ?? null) || !in_array('member', $contact['types'], true)) {
return '<p>Your linked membership could not be verified. Contact your club administrator.</p>';
}
$member = is_array($contact['member'] ?? null) ? $contact['member'] : [];
$html = '<section class="campai-member-area"><h2>Your membership</h2><p>Signed in as ' . esc_html(wp_get_current_user()->display_name) . '.</p><dl>';
foreach (['entryAt' => 'Membership starts', 'exitAt' => 'Membership ends'] as $field => $label) {
$value = array_key_exists($field, $member) ? campai_guide_date($member[$field]) : 'Unavailable';
$html .= '<dt><strong>' . esc_html($label) . '</strong></dt><dd>' . esc_html($value) . '</dd>';
}
return $html . '</dl><p><a href="' . esc_url(wp_logout_url(get_permalink())) . '">Sign out</a></p></section>';
}
add_shortcode('campai_member_area', 'campai_guide_member_area');
You can also copy the complete PHP source into the path shown above. There are no Composer packages, SDKs, or separate helper files. WordPress provides the login, settings, shortcode, and HTTP functions.
You should see: a new Settings → campai member area entry. If WordPress rejects the upload, check your PHP version and that your hosting plan allows custom plugins.
Open Settings → campai member area. Enter your full Organization ID, Mandate ID, and API key, then choose Save connection.
The saved key is never placed back in the form. Leaving the key field blank keeps it; entering a new key replaces it. Saving stores the connection but does not yet call the API. The first linked member page request verifies the connection.
The key is stored in your WordPress database. Limit database and backup access as you would for other website credentials. If you stop using the plugin, revoke the key in campai; deactivating a WordPress plugin does not revoke an API key.
Open Users → All Users, edit the verified website account, and scroll to campai member area. Paste its campai contact _id into Verified contact ID, then choose Update User.
Only an administrator can save this link. Visitors cannot select a contact through the page URL or a form field. Repeat this step for your second test account, using a different member contact.
You should see: the same contact ID when you reopen the user profile. To remove the link, clear the field and save. Changing the plugin's organization or mandate invalidates existing links until you review and save them again.
- Open Pages → Add Page and name the page Member area.
- Add WordPress's Shortcode block. Paste
[campai_member_area]into that block and publish the page. - Copy the page's actual permalink. Exclude that URL from your WordPress page cache, hosting cache, and CDN cache before making it available to members.
- Open the page while signed out. It should offer Sign in to view your membership. Follow that link and sign in as the linked test member.
The plugin sends no-cache headers, but a hosting cache can answer before WordPress runs. Check the exclusion in your hosting provider's cache settings and test with two separate browser sessions.
Your finished page: the member sees their WordPress display name, Membership starts, Membership ends, and Sign out. A date set to null appears as Not set; a field absent from the API response appears as Unavailable. The rest of the contact record stays on your server. Your WordPress theme controls the page's appearance.
The shortcode protects its own membership profile. It does not protect other text, posts, or downloads on the page, assign paid-access roles, or sign someone into the Community App. Add those features only after defining and enforcing their separate access rules.
| Try this | Expected result |
|---|---|
| Sign in with the first linked account | Only that person's dates appear. Compare them with the contact in campai. |
| Sign in with the second linked account in a separate session | Its own dates appear, never the first account's cached profile. |
Add ?contactId=ANOTHER_CONTACT_ID to the page URL |
The profile stays bound to the signed-in account. |
| Sign out | The dates disappear and the sign-in link returns. |
| Use an account with no saved link | The page asks the user to contact the club administrator. |
| Clear a user's link and reload as that user | No membership dates are shown. |
| Use an invalid key on your staging copy | A temporary-unavailability message appears, without API details or credentials. Restore the key afterward. |
In browser developer tools, inspect the member page's HTML and network requests. You should find no API key, no browser request to cloud.campai.com, and no full contact object. Verify this on the deployed website too, because its cache configuration can differ from staging.
| What you see | What to check |
|---|---|
| The shortcode appears literally | Activate the plugin and use a Shortcode block. |
| Account is not linked | Save the verified contact ID as an administrator. Re-save after a scope change. |
| Membership is temporarily unavailable | Check the key's Read permissions, mandate restriction, full IDs, and outbound HTTPS access from WordPress. |
| Linked membership could not be verified | Confirm the saved ID belongs to the intended member contact, with member in its types. |
| A date says Unavailable | Check the source record and key's field access. Do not treat a missing field as a cleared date. |
| One account sees another's profile | Remove the page from every shared cache and purge existing cached copies before reopening it. |
To isolate the campai connection, run this read request in a private terminal. Replace the key, organization, mandate, and contact placeholders with the same values stored in WordPress. It calls Get contact, which returns the record directly.
Example
curl --fail-with-body --silent --show-error --max-time 30 --globoff \
--request GET 'https://cloud.campai.com/api/YOUR_ORGANIZATION_ID/YOUR_MANDATE_ID/crm/contacts/YOUR_CONTACT_ID' \
--header 'X-API-Key: YOUR_API_KEY'The cURL tab displays the complete contact response for a private terminal check. The Node.js and PHP tabs check the mapped member ID and output only membership entry and exit dates. Replace both occurrences of YOUR_CONTACT_ID in those tabs.
The plugin uses WordPress's native HTTP API for that same request and a shortcode for the page. If this terminal request succeeds while the page fails, check the saved mapping and WordPress server's outbound connection.
For a larger integration, continue with member synchronization before adding background updates. Keep any content-access decision separate from merely displaying membership dates.
Check your result
- Two linked accounts
- Each sees only its own dates.
- Signed-out visitor
- Only the sign-in link appears.
- Revoked or invalid key
- No contact details or API diagnostics are exposed.