Building secure B2B pricing grids with custom Block Patterns API endpoints and role overrides
Leveraging WordPress REST API for Dynamic B2B Pricing Grids
Building a robust B2B pricing system within WordPress often necessitates dynamic, role-based pricing displays. While standard WordPress roles and capabilities offer a foundation, complex tiered pricing or client-specific discounts demand a more granular approach. This post details how to construct secure, custom pricing grids using the WordPress REST API, specifically by registering custom endpoints and implementing role-based access control overrides.
Registering Custom REST API Endpoints for Pricing Data
The WordPress REST API provides a powerful mechanism for exposing data and functionality. We’ll register a custom endpoint to serve pricing information, ensuring it’s only accessible under specific conditions. This involves hooking into the rest_api_init action.
Endpoint Registration Logic
The core of our endpoint registration will reside within a custom plugin. This keeps our logic organized and maintainable. We’ll define a route that accepts a product ID and potentially a customer ID (or relies on the current user’s role).
Example Plugin Structure
Assume a plugin file named b2b-pricing-api.php in wp-content/plugins/.
b2b-pricing-api.php – Endpoint Registration
<?php
/**
* Plugin Name: B2B Pricing API
* Description: Provides custom REST API endpoints for B2B pricing grids.
* Version: 1.0
* Author: Antigravity
*/
add_action( 'rest_api_init', function () {
register_rest_route( 'b2b-pricing/v1', '/product/(?P<id>\d+)', array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'get_b2b_product_pricing',
'permission_callback' => 'get_b2b_product_pricing_permissions_check',
'args' => array(
'id' => array(
'validate_callback' => function( $param, $request, $key ) {
return is_numeric( $param );
}
),
),
) );
} );
/**
* Callback function to retrieve B2B pricing for a product.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
*/
function get_b2b_product_pricing( WP_REST_Request $request ) {
$product_id = $request['id'];
$current_user = wp_get_current_user();
// In a real-world scenario, you'd fetch pricing based on user roles,
// custom user meta, or specific customer IDs associated with pricing tiers.
// For this example, we'll simulate tiered pricing based on a hypothetical 'customer_tier' role.
$pricing_data = array();
if ( user_can( $current_user->ID, 'wholesale_customer' ) ) {
// Pricing for wholesale customers
$pricing_data['base_price'] = get_post_meta( $product_id, '_wholesale_price', true );
$pricing_data['discount_percentage'] = get_post_meta( $product_id, '_wholesale_discount', true );
$pricing_data['display_price'] = ! empty( $pricing_data['base_price'] ) ?
$pricing_data['base_price'] * ( 1 - ( floatval( $pricing_data['discount_percentage'] ?? 0 ) / 100 ) ) :
__( 'Contact for Price', 'b2b-pricing-api' );
} elseif ( user_can( $current_user->ID, 'vip_customer' ) ) {
// Pricing for VIP customers
$pricing_data['base_price'] = get_post_meta( $product_id, '_vip_price', true );
$pricing_data['special_offer'] = get_post_meta( $product_id, '_vip_offer', true );
$pricing_data['display_price'] = ! empty( $pricing_data['base_price'] ) ?
$pricing_data['base_price'] :
__( 'Contact for Price', 'b2b-pricing-api' );
} else {
// Default public pricing or fallback
$pricing_data['base_price'] = get_post_meta( $product_id, '_regular_price', true ); // Assuming WooCommerce or similar
$pricing_data['display_price'] = ! empty( $pricing_data['base_price'] ) ?
$pricing_data['base_price'] :
__( 'Contact for Price', 'b2b-pricing-api' );
}
// Ensure we have some data to return
if ( empty( $pricing_data ) ) {
return new WP_Error( 'no_pricing_data', __( 'No pricing data found for this product and user role.', 'b2b-pricing-api' ), array( 'status' => 404 ) );
}
// Add product title for context
$product_title = get_the_title( $product_id );
if ( ! $product_title ) {
return new WP_Error( 'invalid_product', __( 'Invalid product ID.', 'b2b-pricing-api' ), array( 'status' => 404 ) );
}
$pricing_data['product_name'] = $product_title;
$response = new WP_REST_Response( $pricing_data );
$response->set_status( 200 );
return $response;
}
/**
* Permission callback for the B2B pricing endpoint.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|bool False on failure, true on success.
*/
function get_b2b_product_pricing_permissions_check( WP_REST_Request $request ) {
// Check if the user is logged in. For B2B, this is usually a prerequisite.
if ( ! is_user_logged_in() ) {
return new WP_Error( 'rest_forbidden', __( 'You must be logged in to view pricing.', 'b2b-pricing-api' ), array( 'status' => 401 ) );
}
// Further role-based checks can be added here.
// For example, if you have a specific role for B2B access:
// if ( ! current_user_can( 'access_b2b_pricing' ) ) {
// return new WP_Error( 'rest_forbidden', __( 'You do not have permission to view B2B pricing.', 'b2b-pricing-api' ), array( 'status' => 403 ) );
// }
// If the user is logged in and has passed any other checks, allow access.
return true;
}
// Optional: Add custom capabilities for finer-grained control
function add_custom_capabilities() {
$roles = array( 'wholesale_customer', 'vip_customer' ); // Example roles
foreach ( $roles as $role_name ) {
$role = get_role( $role_name );
if ( $role ) {
// Add capability if it doesn't exist
if ( ! $role->has_cap( 'view_b2b_pricing' ) ) {
$role->add_cap( 'view_b2b_pricing' );
}
} else {
// If role doesn't exist, create it (e.g., for new user registration)
add_role( $role_name, ucfirst( $role_name ), array( 'read' => true, 'view_b2b_pricing' => true ) );
}
}
}
register_activation_hook( __FILE__, 'add_custom_capabilities' );
// Optional: Remove custom capabilities on deactivation
function remove_custom_capabilities() {
$roles = array( 'wholesale_customer', 'vip_customer' );
foreach ( $roles as $role_name ) {
$role = get_role( $role_name );
if ( $role ) {
$role->remove_cap( 'view_b2b_pricing' );
}
}
// Optionally remove the roles themselves if they were created by the plugin
// remove_role( 'wholesale_customer' );
// remove_role( 'vip_customer' );
}
register_deactivation_hook( __FILE__, 'remove_custom_capabilities' );
?>
In this code:
- We hook into
rest_api_initto register our route. - The route
/b2b-pricing/v1/product/(?P<id>\d+)is defined, accepting a product ID. WP_REST_Server::READABLEspecifies that this endpoint responds to GET requests.get_b2b_product_pricingis the callback function that fetches and formats the pricing data.get_b2b_product_pricing_permissions_checkhandles access control.- We use
get_post_metato retrieve custom pricing fields. In a real e-commerce setup (like WooCommerce), these would be product meta fields. - The example simulates different pricing tiers based on hypothetical roles like
wholesale_customerandvip_customer. - The
add_custom_capabilitiesandremove_custom_capabilitiesfunctions demonstrate how to programmatically add roles and capabilities upon plugin activation/deactivation, which is crucial for managing user permissions effectively.
Implementing Role-Based Access Control Overrides
The permission_callback is the gatekeeper for our API endpoint. It’s essential to implement robust checks here to ensure only authorized users can access sensitive pricing information. We’ll go beyond basic logged-in checks to incorporate role-specific permissions.
Advanced Permission Logic
The get_b2b_product_pricing_permissions_check function is where the magic happens. We can leverage WordPress’s built-in user roles and capabilities, or define custom ones.
Example Permission Check
/**
* Permission callback for the B2B pricing endpoint.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|bool False on failure, true on success.
*/
function get_b2b_product_pricing_permissions_check( WP_REST_Request $request ) {
// 1. Basic logged-in check
if ( ! is_user_logged_in() ) {
return new WP_Error( 'rest_forbidden', __( 'You must be logged in to view pricing.', 'b2b-pricing-api' ), array( 'status' => 401 ) );
}
$current_user_id = get_current_user_id();
// 2. Role-based checks (using hypothetical roles)
if ( user_can( $current_user_id, 'wholesale_customer' ) ) {
// User is a wholesale customer, grant access.
return true;
}
if ( user_can( $current_user_id, 'vip_customer' ) ) {
// User is a VIP customer, grant access.
return true;
}
// 3. Custom capability check (if you've defined a general B2B access capability)
// if ( current_user_can( 'access_b2b_pricing' ) ) {
// return true;
// }
// 4. Fallback: If none of the above conditions are met, deny access.
return new WP_Error( 'rest_forbidden', __( 'You do not have permission to view B2B pricing.', 'b2b-pricing-api' ), array( 'status' => 403 ) );
}
This enhanced permission check:
- First, verifies if the user is logged in.
- Then, it checks for specific roles (
wholesale_customer,vip_customer). If the user possesses any of these roles, access is granted. - A commented-out section shows how to implement a more general capability like
access_b2b_pricing, which could be assigned to multiple roles or specific users. - If none of the authorized conditions are met, a
403 Forbiddenerror is returned.
Integrating with Frontend for Dynamic Pricing Display
Once the API endpoint is functional, the next step is to consume it from the frontend. This typically involves JavaScript to fetch data and dynamically update pricing on product pages or custom pricing grid pages.
JavaScript Fetch Example
This example uses the native fetch API. Ensure you enqueue this script properly within your WordPress theme or plugin.
document.addEventListener('DOMContentLoaded', function() {
const productId = document.querySelector('[data-product-id]')?.dataset.productId; // Assuming product ID is in a data attribute
if (!productId) {
console.error('Product ID not found.');
return;
}
// Construct the API URL. Ensure your site URL is correctly handled.
const apiUrl = `${window.location.origin}/wp-json/b2b-pricing/v1/product/${productId}`;
fetch(apiUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': window.wpApiSettings ? window.wpApiSettings.nonce : '' // Important for authenticated requests if needed
}
})
.then(response => {
if (!response.ok) {
// Handle non-2xx responses
if (response.status === 401) {
return Promise.reject('Authentication required. Please log in.');
} else if (response.status === 403) {
return Promise.reject('You do not have permission to view this pricing.');
} else {
return response.json().then(err => Promise.reject(err.message || `HTTP error! status: ${response.status}`));
}
}
return response.json();
})
.then(data => {
console.log('Pricing data received:', data);
// Update the DOM with pricing information
updatePricingDisplay(data);
})
.catch(error => {
console.error('Error fetching pricing:', error);
// Display an error message to the user
displayPricingError(error);
});
});
function updatePricingDisplay(pricingData) {
const pricingContainer = document.getElementById('b2b-pricing-display'); // Assume an element with this ID exists
if (!pricingContainer) return;
let html = `${pricingData.product_name || 'Pricing Information'}
`;
if (pricingData.display_price) {
html += `Price: ${pricingData.display_price}
`;
}
if (pricingData.discount_percentage) {
html += `Wholesale Discount: ${pricingData.discount_percentage}%
`;
}
if (pricingData.special_offer) {
html += `Special Offer: ${pricingData.special_offer}
`;
}
// Add more fields as needed
pricingContainer.innerHTML = html;
}
function displayPricingError(errorMessage) {
const pricingContainer = document.getElementById('b2b-pricing-display');
if (!pricingContainer) return;
pricingContainer.innerHTML = `Error: ${errorMessage}
`;
}
Key considerations for the frontend integration:
- The script fetches data from the registered API endpoint using the product ID.
- It includes basic error handling for different HTTP status codes (401, 403).
- A placeholder
updatePricingDisplayfunction shows where you would inject the fetched pricing data into your HTML. - The
X-WP-Nonceheader is crucial if your API endpoint requires authentication beyond just being logged in (e.g., if you were performing actions, not just reading data). For read operations on public endpoints, it might be optional but good practice.
Security Best Practices and Considerations
Securing B2B pricing is paramount. Beyond the role checks, consider these points:
Nonce Verification
While our current endpoint is read-only and relies on user roles, for any endpoint that modifies data (e.g., updating pricing rules), nonces are absolutely essential. WordPress automatically generates nonces for logged-in users, and they can be passed via JavaScript. The REST API validates these nonces automatically for authenticated requests.
HTTPS Enforcement
Always ensure your WordPress site is served over HTTPS. This encrypts data in transit, protecting pricing information from being intercepted, especially on public networks.
Rate Limiting
For public-facing APIs, implementing rate limiting (e.g., using Nginx or a WordPress plugin) can prevent abuse and denial-of-service attacks. While B2B pricing might be less of a target, it’s a good general security practice.
Data Sanitization and Validation
Always sanitize any data received from the client (even if it’s just the product ID in the URL) and validate it rigorously. The example uses is_numeric, but for more complex data, use functions like sanitize_text_field, absint, etc., and ensure your args array in register_rest_route is properly configured.
Custom Roles and Capabilities Management
For a production environment, consider a more robust system for managing custom roles and capabilities. Plugins like “Members” or “User Role Editor” can simplify this, or you can build your own management interface. The activation/deactivation hooks provide a programmatic way to set these up initially.
Conclusion
By leveraging the WordPress REST API with custom endpoints and carefully crafted permission callbacks, you can build sophisticated, secure, and dynamic B2B pricing grids. This approach offers flexibility to cater to complex business requirements while maintaining a secure and manageable WordPress environment. Remember to tailor the pricing logic and permission checks precisely to your specific B2B sales strategy.