Building secure B2B pricing grids with custom WP HTTP API endpoints and role overrides
Securing B2B Pricing Grids: Custom WP HTTP API Endpoints and Role Overrides
Implementing dynamic, role-based pricing grids for B2B clients within WordPress requires a robust and secure approach. Relying solely on frontend JavaScript to hide/show prices is a significant security vulnerability. This post details a production-ready strategy using custom WordPress HTTP API endpoints and advanced user role management to serve pricing data securely.
Leveraging the WP HTTP API for Secure Data Retrieval
The WordPress REST API, particularly when extended with custom endpoints, provides a powerful mechanism for serving data. For pricing grids, we’ll create a dedicated endpoint that validates user permissions before returning sensitive pricing information. This ensures that only authenticated and authorized users can access B2B-specific pricing.
Registering a Custom REST API Endpoint
We’ll use the `register_rest_route` function within a custom plugin or theme’s `functions.php` file. This function allows us to define a new endpoint, specify its callback function, and set access permissions.
Example: `b2b-pricing/v1/products` Endpoint
This endpoint will fetch product pricing data, filtered by the current user’s role or assigned customer group.
<?php
/**
* Register custom REST API endpoint for B2B pricing.
*/
add_action( 'rest_api_init', function () {
register_rest_route( 'b2b-pricing/v1', '/products', array(
'methods' => WP_REST_Server::READABLE, // GET method
'callback' => 'get_b2b_pricing_data',
'permission_callback' => 'get_b2b_pricing_permissions',
) );
} );
/**
* Callback function to retrieve B2B pricing data.
*
* @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.
*/
function get_b2b_pricing_data( WP_REST_Request $request ) {
// In a real-world scenario, fetch this data from a custom table,
// WooCommerce products, or a dedicated pricing plugin's data store.
// For demonstration, we'll use a mock array.
$all_products_pricing = array(
'SKU001' => array(
'name' => 'Premium Widget',
'base_price' => 100.00,
'b2b_tier_1' => 90.00, // e.g., for customers in group A
'b2b_tier_2' => 85.00, // e.g., for customers in group B
),
'SKU002' => array(
'name' => 'Standard Gadget',
'base_price' => 50.00,
'b2b_tier_1' => 45.00,
'b2b_tier_2' => 40.00,
),
// ... more products
);
$current_user_id = get_current_user_id();
$user_pricing_data = array();
// Determine user's pricing tier or specific pricing.
// This logic needs to be robust and based on your B2B customer structure.
if ( user_can( $current_user_id, 'manage_options' ) ) { // Example: Admin role gets highest tier
foreach ( $all_products_pricing as $sku => $product_data ) {
$user_pricing_data[$sku] = array(
'name' => $product_data['name'],
'price' => $product_data['b2b_tier_2'], // Highest discount tier
);
}
} elseif ( is_user_logged_in() ) {
// Example: Check for custom meta or group association
$customer_group = get_user_meta( $current_user_id, 'b2b_customer_group', true );
if ( 'group_a' === $customer_group ) {
foreach ( $all_products_pricing as $sku => $product_data ) {
$user_pricing_data[$sku] = array(
'name' => $product_data['name'],
'price' => $product_data['b2b_tier_1'],
);
}
} else {
// Default B2B pricing or fallback to public pricing if applicable
foreach ( $all_products_pricing as $sku => $product_data ) {
$user_pricing_data[$sku] = array(
'name' => $product_data['name'],
'price' => $product_data['base_price'], // Fallback to base price
);
}
}
} else {
// Not logged in, return public pricing or an empty array
// For this example, we'll return an empty array to enforce login for pricing.
return new WP_Error( 'rest_not_logged_in', 'You must be logged in to view pricing.', array( 'status' => 401 ) );
}
return new WP_REST_Response( $user_pricing_data, 200 );
}
/**
* Permission callback for the B2B pricing endpoint.
*
* @param WP_REST_Request $request Full data about the request.
* @return bool|WP_Error True if the request has access, WP_Error object otherwise.
*/
function get_b2b_pricing_permissions( WP_REST_Request $request ) {
// Check if the user is logged in.
if ( ! is_user_logged_in() ) {
return new WP_Error( 'rest_not_logged_in', 'You must be logged in to view pricing.', array( 'status' => 401 ) );
}
// Further role-based checks can be added here.
// For example, if you have a specific 'B2B Customer' role:
// if ( ! current_user_can( 'b2b_customer_role' ) ) {
// return new WP_Error( 'rest_forbidden', 'You do not have permission to view B2B pricing.', array( 'status' => 403 ) );
// }
// For this example, we'll allow any logged-in user to access,
// but the callback function will determine the specific pricing tier.
// More granular control is recommended for production.
return true;
}
?>
Understanding the Code
- `add_action( ‘rest_api_init’, … )`: Hooks into WordPress to register our custom route.
- `register_rest_route( ‘b2b-pricing/v1’, ‘/products’, … )`: Defines the endpoint namespace (`b2b-pricing/v1`) and path (`/products`).
- `’methods’ => WP_REST_Server::READABLE`: Specifies that this endpoint responds to GET requests.
- `’callback’ => ‘get_b2b_pricing_data’`: The function that will execute when the endpoint is hit.
- `’permission_callback’ => ‘get_b2b_pricing_permissions’`: A crucial function to determine if the current user is allowed to access this endpoint at all.
- `get_b2b_pricing_data( WP_REST_Request $request )`: This function contains the core logic for fetching and filtering pricing data based on user roles, meta, or custom group assignments. It returns a `WP_REST_Response` object or a `WP_Error`.
- `get_b2b_pricing_permissions( WP_REST_Request $request )`: This function performs initial checks. If it returns `true`, the callback is executed. If it returns a `WP_Error`, the request is terminated with that error.
Implementing Role and Capability Overrides
WordPress’s built-in role and capability system is fundamental. For B2B scenarios, you might need more granular control than standard roles like ‘Administrator’ or ‘Editor’ offer. This can involve creating custom roles or leveraging user meta to define customer groups, each with specific pricing tiers.
Custom Roles and Capabilities
You can define custom roles and capabilities using the `add_role()` function. This is typically done once during plugin activation.
<?php
/**
* Plugin activation hook.
* Registers custom roles.
*/
register_activation_hook( __FILE__, 'my_b2b_plugin_activate' );
function my_b2b_plugin_activate() {
// Add a custom role for B2B customers with specific pricing access.
add_role(
'b2b_customer_tier_1',
__( 'B2B Customer (Tier 1)', 'my-b2b-plugin' ),
array(
'read' => true, // Basic read access
'edit_posts' => false, // Prevent editing posts
'upload_files' => false, // Prevent uploading files
'view_b2b_pricing' => true, // Custom capability
)
);
// Add another tier
add_role(
'b2b_customer_tier_2',
__( 'B2B Customer (Tier 2)', 'my-b2b-plugin' ),
array(
'read' => true,
'edit_posts' => false,
'upload_files' => false,
'view_b2b_pricing' => true, // Custom capability
)
);
// Add the custom capability to existing roles if needed, e.g., Administrators
$admin_role = get_role( 'administrator' );
if ( $admin_role ) {
$admin_role->add_cap( 'view_b2b_pricing' );
}
}
/**
* Plugin deactivation hook.
* Removes custom roles.
*/
register_deactivation_hook( __FILE__, 'my_b2b_plugin_deactivate' );
function my_b2b_plugin_deactivate() {
remove_role( 'b2b_customer_tier_1' );
remove_role( 'b2b_customer_tier_2' );
// Remove custom capability from roles if necessary
$admin_role = get_role( 'administrator' );
if ( $admin_role ) {
$admin_role->remove_cap( 'view_b2b_pricing' );
}
}
?>
Using User Meta for Customer Groups
For more dynamic assignments or when you don’t want to create a multitude of roles, user meta is an excellent alternative. You can store a ‘customer_group’ or ‘pricing_tier’ value in the user’s profile.
In the `get_b2b_pricing_data` callback, we already demonstrated fetching this meta: $customer_group = get_user_meta( $current_user_id, 'b2b_customer_group', true );. You would typically manage this meta data via the user profile edit screen (using `show_user_profile` and `edit_user_profile` actions) or through an admin interface for managing B2B clients.
Updating the Permission Callback
The `get_b2b_pricing_permissions` function can be enhanced to check these custom roles or meta values:
/**
* Enhanced permission callback for the B2B pricing endpoint.
*
* @param WP_REST_Request $request Full data about the request.
* @return bool|WP_Error True if the request has access, WP_Error object otherwise.
*/
function get_b2b_pricing_permissions( WP_REST_Request $request ) {
if ( ! is_user_logged_in() ) {
return new WP_Error( 'rest_not_logged_in', 'You must be logged in to view pricing.', array( 'status' => 401 ) );
}
$current_user_id = get_current_user_id();
// Option 1: Check for custom capability
if ( ! user_can( $current_user_id, 'view_b2b_pricing' ) ) {
return new WP_Error( 'rest_forbidden', 'You do not have permission to view B2B pricing.', array( 'status' => 403 ) );
}
// Option 2: Check for specific roles (if you created them)
// $user = wp_get_current_user();
// $allowed_roles = array( 'b2b_customer_tier_1', 'b2b_customer_tier_2', 'administrator' );
// if ( array_intersect( $allowed_roles, $user->roles ) ) {
// return true; // User has one of the allowed roles
// } else {
// return new WP_Error( 'rest_forbidden', 'You do not have permission to view B2B pricing.', array( 'status' => 403 ) );
// }
// Option 3: Check user meta for customer group
// $customer_group = get_user_meta( $current_user_id, 'b2b_customer_group', true );
// if ( ! empty( $customer_group ) && in_array( $customer_group, array( 'group_a', 'group_b' ) ) ) {
// return true; // User belongs to an allowed group
// } else {
// return new WP_Error( 'rest_forbidden', 'You do not have permission to view B2B pricing.', array( 'status' => 403 ) );
// }
// If any of the above checks pass, return true.
// For this example, we'll assume the 'view_b2b_pricing' capability is sufficient.
return true;
}
Frontend Integration and Security Considerations
On the frontend, JavaScript will be responsible for fetching data from your custom endpoint and rendering the pricing grid. Crucially, it should *never* contain the pricing logic itself.
Fetching Data with JavaScript
Use the `fetch` API or jQuery’s `$.ajax` to call your endpoint. Ensure you include appropriate authentication headers if using nonces or JWTs for more advanced security.
// Example using Fetch API
document.addEventListener('DOMContentLoaded', function() {
const pricingGridContainer = document.getElementById('b2b-pricing-grid');
if (!pricingGridContainer) {
return; // Pricing grid element not found
}
// Get the nonce from the WordPress REST API settings
// Ensure this is properly enqueued and localized in your theme/plugin
const nonce = wpApiSettings.nonce; // Assuming wpApiSettings is localized
fetch('/wp-json/b2b-pricing/v1/products', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': nonce // Important for authentication
}
})
.then(response => {
if (!response.ok) {
// Handle errors: unauthorized, forbidden, not found, etc.
if (response.status === 401 || response.status === 403) {
pricingGridContainer.innerHTML = '<p>Please log in or check your permissions to view pricing.</p>';
} else {
pricingGridContainer.innerHTML = '<p>Could not load pricing data. Please try again later.</p>';
}
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (Object.keys(data).length === 0) {
pricingGridContainer.innerHTML = '<p>No pricing information available for your account.</p>';
return;
}
renderPricingGrid(data, pricingGridContainer);
})
.catch(error => {
console.error('Error fetching B2B pricing:', error);
// Error message already set in the .then block for !response.ok
});
});
function renderPricingGrid(pricingData, container) {
let html = '<table><thead><tr><th>Product</th><th>Price</th></tr></thead><tbody>';
for (const sku in pricingData) {
html += `<tr><td>${pricingData[sku].name} (${sku})</td><td>$${pricingData[sku].price.toFixed(2)}</td></tr>`;
}
html += '</tbody></table>';
container.innerHTML = html;
}
Important: Ensure you enqueue your JavaScript file and localize the `wpApiSettings.nonce` correctly. This is typically done using `wp_localize_script`:
/**
* Enqueue and localize script for B2B pricing frontend.
*/
add_action( 'wp_enqueue_scripts', function() {
// Only enqueue on pages where B2B pricing is relevant
if ( is_page('pricing') || is_user_logged_in() ) { // Adjust condition as needed
wp_enqueue_script(
'b2b-pricing-frontend',
get_template_directory_uri() . '/js/b2b-pricing-frontend.js', // Path to your JS file
array( 'jquery' ), // Dependencies
'1.0.0',
true // Load in footer
);
// Localize script to pass nonce and other data to JavaScript
wp_localize_script(
'b2b-pricing-frontend',
'wpApiSettings',
array(
'nonce' => wp_create_nonce( 'wp_rest' ), // Standard nonce for REST API
// Add other settings if needed, e.g., base API URL
)
);
}
} );
Security Best Practices
- Never expose pricing logic client-side. All price calculations and data retrieval must happen server-side via the API.
- Use Nonces for Authentication. The `X-WP-Nonce` header is critical for verifying that the request originates from a legitimate, logged-in user.
- Strict Permission Callbacks. The `permission_callback` should be as restrictive as possible. If a user doesn’t need to see pricing, they shouldn’t even be able to hit the callback function.
- Sanitize and Validate. While less critical for GET requests returning data, always sanitize any input parameters if your endpoint were to accept them (e.g., for fetching specific product pricing).
- Rate Limiting. For public-facing APIs, consider implementing rate limiting to prevent abuse. For internal B2B APIs, this might be less of a concern but still good practice.
- HTTPS. Always serve your WordPress site over HTTPS to encrypt data in transit.
Conclusion
By combining custom WordPress REST API endpoints with robust role and capability management, you can build a secure and dynamic B2B pricing system. This approach centralizes pricing logic on the server, preventing unauthorized access and ensuring data integrity, which is paramount for sensitive business information.