• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Building secure B2B pricing grids with custom Metadata API (add_post_meta) endpoints and role overrides

Building secure B2B pricing grids with custom Metadata API (add_post_meta) endpoints and role overrides

Securing B2B Pricing Grids with Custom WordPress Metadata Endpoints

Building dynamic, role-specific pricing grids in WordPress for B2B clients presents a unique set of challenges, particularly around data security and granular access control. Standard WordPress user roles and capabilities often fall short when dealing with complex pricing tiers, volume discounts, and client-specific negotiated rates. This post details a robust solution leveraging custom WordPress REST API endpoints for managing post meta, combined with advanced role overrides, to create a secure and flexible B2B pricing system.

Designing the Metadata Schema for Pricing

The foundation of our B2B pricing grid lies in how we structure the pricing data. We’ll use custom post meta associated with a custom post type (e.g., ‘Product’ or ‘Service’) to store this information. For a B2B context, pricing isn’t monolithic. It often varies based on customer tier, volume, or specific contract terms. We’ll design our meta keys to reflect this complexity.

Consider a product with the following pricing dimensions:

  • Base Price (for general public/retail)
  • Tier 1 Price (e.g., for Bronze partners)
  • Tier 2 Price (e.g., for Silver partners)
  • Tier 3 Price (e.g., for Gold partners)
  • Volume Discount Threshold 1 (e.g., 100 units)
  • Volume Discount Rate 1 (e.g., 5% off Tier 3)
  • Volume Discount Threshold 2 (e.g., 500 units)
  • Volume Discount Rate 2 (e.g., 10% off Tier 3)
  • Contracted Price (for specific clients, overriding all others)

These would translate into meta keys like:

  • _base_price
  • _tier_1_price
  • _tier_2_price
  • _tier_3_price
  • _volume_discount_threshold_1
  • _volume_discount_rate_1
  • _volume_discount_threshold_2
  • _volume_discount_rate_2
  • _contracted_price_{client_id} (where {client_id} is a unique identifier for the client)

Implementing Custom REST API Endpoints for Metadata

WordPress’s built-in REST API provides endpoints for posts and their meta. However, for B2B pricing, we need more control. We’ll create custom endpoints to manage these specific pricing meta fields, ensuring that only authorized users can read or write them. This involves hooking into the rest_api_init action.

Let’s define an endpoint to retrieve pricing data for a specific product, allowing filtering by user role or client ID.

Endpoint for Retrieving Pricing Data

We’ll register a route under /myplugin/v1/pricing/product/{id}. This endpoint will fetch the product’s meta data and intelligently return the appropriate price based on the authenticated user’s role or associated client data.

add_action( 'rest_api_init', function () {
    register_rest_route( 'myplugin/v1', '/pricing/product/(?P<id>\d+)', array(
        'methods'  => 'GET',
        'callback' => 'myplugin_get_product_pricing',
        'permission_callback' => '__return_true', // We'll handle permissions within the callback
    ) );
} );

function myplugin_get_product_pricing( WP_REST_Request $request ) {
    $product_id = $request['id'];
    $user = wp_get_current_user();
    $pricing_data = array();

    // Basic product existence check
    $post = get_post( $product_id );
    if ( ! $post || $post->post_type !== 'product' ) { // Assuming 'product' is your CPT
        return new WP_Error( 'rest_not_found', 'Product not found', array( 'status' => 404 ) );
    }

    // Fetch all relevant pricing meta
    $meta_keys = array(
        '_base_price',
        '_tier_1_price',
        '_tier_2_price',
        '_tier_3_price',
        '_volume_discount_threshold_1',
        '_volume_discount_rate_1',
        '_volume_discount_threshold_2',
        '_volume_discount_rate_2',
    );

    foreach ( $meta_keys as $key ) {
        $pricing_data[$key] = get_post_meta( $product_id, $key, true );
    }

    // --- Role-based pricing logic ---
    $user_roles = (array) $user->roles;
    $effective_price = $pricing_data['_base_price']; // Default to base price

    if ( in_array( 'b2b_tier_3', $user_roles ) ) {
        $effective_price = $pricing_data['_tier_3_price'] ?: $effective_price;
    } elseif ( in_array( 'b2b_tier_2', $user_roles ) ) {
        $effective_price = $pricing_data['_tier_2_price'] ?: $effective_price;
    } elseif ( in_array( 'b2b_tier_1', $user_roles ) ) {
        $effective_price = $pricing_data['_tier_1_price'] ?: $effective_price;
    }

    // --- Contracted pricing logic ---
    // This assumes you have a way to map the current user to a client ID.
    // For simplicity, let's assume a meta field on the user object: 'client_id'
    $client_id = get_user_meta( $user->ID, 'client_id', true );
    if ( ! empty( $client_id ) ) {
        $contracted_price_key = '_contracted_price_' . sanitize_key( $client_id );
        $contracted_price = get_post_meta( $product_id, $contracted_price_key, true );
        if ( ! empty( $contracted_price ) ) {
            $effective_price = $contracted_price;
        }
    }

    // --- Volume discount application (example for Tier 3 or contracted price) ---
    // This logic would typically be applied client-side or in a more complex backend process
    // For this example, we'll just return the base pricing structure.
    // A full implementation would involve passing quantity to the API or handling it in the frontend.

    $response_data = array(
        'product_id' => $product_id,
        'base_price' => $pricing_data['_base_price'],
        'tier_1_price' => $pricing_data['_tier_1_price'],
        'tier_2_price' => $pricing_data['_tier_2_price'],
        'tier_3_price' => $pricing_data['_tier_3_price'],
        'contracted_price' => isset( $contracted_price ) ? $contracted_price : null,
        'effective_price' => $effective_price, // The price determined by role/contract
        'volume_discounts' => array(
            'threshold_1' => $pricing_data['_volume_discount_threshold_1'],
            'rate_1' => $pricing_data['_volume_discount_rate_1'],
            'threshold_2' => $pricing_data['_volume_discount_threshold_2'],
            'rate_2' => $pricing_data['_volume_discount_rate_2'],
        ),
        'user_roles' => $user_roles,
        'client_id' => $client_id,
    );

    $response = new WP_REST_Response( $response_data );
    $response->set_status( 200 );

    return $response;
}

Endpoint for Updating Pricing Data

Updating pricing data requires strict authorization. We’ll create a POST endpoint /myplugin/v1/pricing/product/{id}/update. This endpoint will only be accessible to users with a specific capability, such as manage_b2b_pricing.

add_action( 'rest_api_init', function () {
    register_rest_route( 'myplugin/v1', '/pricing/product/(?P<id>\d+)/update', array(
        'methods'  => 'POST',
        'callback' => 'myplugin_update_product_pricing',
        'permission_callback' => 'myplugin_update_pricing_permissions_check',
        'args' => array( // Define expected arguments for validation
            'base_price' => array(
                'required' => false,
                'type' => 'string', // Use string for potential currency symbols, or float/number
                'sanitize_callback' => 'sanitize_text_field',
            ),
            '_tier_1_price' => array(
                'required' => false,
                'type' => 'string',
                'sanitize_callback' => 'sanitize_text_field',
            ),
            // ... other price fields ...
            '_contracted_prices' => array( // Expect an array of { client_id: price }
                'required' => false,
                'type' => 'array',
                'sanitize_callback' => array( $this, 'sanitize_contracted_prices' ), // Custom sanitizer
            ),
        ),
    ) );
} );

function myplugin_update_pricing_permissions_check( WP_REST_Request $request ) {
    // Check if the current user has the capability to manage B2B pricing
    if ( ! current_user_can( 'manage_b2b_pricing' ) ) {
        return new WP_Error( 'rest_forbidden', esc_html__( 'You do not have permission to update pricing.', 'myplugin' ), array( 'status' => 403 ) );
    }
    return true;
}

function myplugin_update_product_pricing( WP_REST_Request $request ) {
    $product_id = $request['id'];
    $params = $request->get_params();

    // Basic product existence check
    $post = get_post( $product_id );
    if ( ! $post || $post->post_type !== 'product' ) {
        return new WP_Error( 'rest_not_found', 'Product not found', array( 'status' => 404 ) );
    }

    // Update standard meta fields
    $meta_to_update = array(
        '_base_price',
        '_tier_1_price',
        '_tier_2_price',
        '_tier_3_price',
        '_volume_discount_threshold_1',
        '_volume_discount_rate_1',
        '_volume_discount_threshold_2',
        '_volume_discount_rate_2',
    );

    foreach ( $meta_to_update as $key ) {
        if ( isset( $params[$key] ) ) {
            update_post_meta( $product_id, $key, $params[$key] );
        }
    }

    // Handle contracted prices separately
    if ( isset( $params['_contracted_prices'] ) && is_array( $params['_contracted_prices'] ) ) {
        // First, remove any existing contracted prices for this product to ensure clean updates
        // This requires a more complex query or a known set of client IDs.
        // For simplicity, let's assume we're only ADDING/OVERWRITING.
        // A more robust solution would involve fetching existing contracted prices and diffing.

        foreach ( $params['_contracted_prices'] as $client_id => $price ) {
            if ( ! empty( $client_id ) && ! empty( $price ) ) {
                $contracted_price_key = '_contracted_price_' . sanitize_key( $client_id );
                update_post_meta( $product_id, $contracted_price_key, sanitize_text_field( $price ) );
            }
        }
    }

    return new WP_REST_Response( array( 'success' => true, 'message' => 'Pricing updated successfully.' ), 200 );
}

// Custom sanitizer for contracted prices
function sanitize_contracted_prices( $value, $request, $param ) {
    if ( ! is_array( $value ) ) {
        return new WP_Error( 'rest_invalid_param', esc_html__( 'Contracted prices must be an array.', 'myplugin' ), array( 'status' => 400 ) );
    }
    $sanitized = array();
    foreach ( $value as $client_id => $price ) {
        $sanitized_client_id = sanitize_key( $client_id );
        if ( ! empty( $sanitized_client_id ) ) {
            $sanitized[$sanitized_client_id] = sanitize_text_field( $price );
        }
    }
    return $sanitized;
}

Implementing Role Overrides and Capabilities

To manage access effectively, we need custom user roles and capabilities. This ensures that only designated personnel can view or modify B2B pricing information.

Defining Custom Roles and Capabilities

We’ll use the add_role() function, typically hooked into plugin activation, to create roles like ‘B2B Manager’ and ‘B2B Sales Rep’. These roles will be granted specific capabilities.

// Hook into plugin activation
register_activation_hook( __FILE__, 'myplugin_activate' );

function myplugin_activate() {
    // Add B2B Manager role with pricing management capabilities
    add_role(
        'b2b_manager',
        __( 'B2B Manager', 'myplugin' ),
        array(
            'read' => true,  // Basic read access
            'edit_posts' => true, // Can edit products/services
            'upload_files' => true,
            'manage_b2b_pricing' => true, // Custom capability to manage B2B pricing
            'read_private_products' => true, // If products are private
        )
    );

    // Add B2B Sales Rep role with pricing viewing capabilities
    add_role(
        'b2b_sales_rep',
        __( 'B2B Sales Rep', 'myplugin' ),
        array(
            'read' => true,
            'edit_posts' => false, // Cannot edit products
            'manage_b2b_pricing' => false, // Cannot manage pricing
            'read_private_products' => true,
        )
    );

    // Add B2B Tiers roles for pricing tiers
    add_role(
        'b2b_tier_1',
        __( 'B2B Tier 1 Client', 'myplugin' ),
        array( 'read' => true )
    );
    add_role(
        'b2b_tier_2',
        __( 'B2B Tier 2 Client', 'myplugin' ),
        array( 'read' => true )
    );
    add_role(
        'b2b_tier_3',
        __( 'B2B Tier 3 Client', 'myplugin' ),
        array( 'read' => true )
    );
}

// Hook into plugin deactivation to remove roles
register_deactivation_hook( __FILE__, 'myplugin_deactivate' );

function myplugin_deactivate() {
    remove_role( 'b2b_manager' );
    remove_role( 'b2b_sales_rep' );
    remove_role( 'b2b_tier_1' );
    remove_role( 'b2b_tier_2' );
    remove_role( 'b2b_tier_3' );
}

The manage_b2b_pricing capability is crucial. It’s checked in our permission_callback for the update endpoint. For the GET endpoint, we initially set 'permission_callback' => '__return_true', but we implement the actual access control logic within the callback function itself, allowing for more dynamic checks (e.g., checking if the user is logged in, or if they are a specific client type).

Client-Specific Pricing and User Mapping

For true B2B functionality, we need to associate users with specific client accounts, which then dictates their contracted pricing. This can be achieved by:

  • Adding a client_id meta field to the user profile.
  • Creating a custom mapping table or using a plugin like “ACF Extended” or “Meta Box” to manage user-client relationships.
  • Leveraging WordPress’s built-in user roles to represent client tiers (as shown in the activation hook).

In the myplugin_get_product_pricing function, we retrieve the user’s client_id and check for a corresponding _contracted_price_{client_id} meta key on the product. This provides the highest level of pricing specificity.

Frontend Integration and Security Considerations

On the frontend, JavaScript will be responsible for fetching pricing data from the /myplugin/v1/pricing/product/{id} endpoint. It’s critical to ensure that the API requests are made with appropriate authentication (e.g., using nonce verification for logged-in users).

// Example of how to enqueue a script that uses the REST API
add_action( 'wp_enqueue_scripts', function() {
    wp_enqueue_script( 'myplugin-pricing', get_template_directory_uri() . '/js/pricing.js', array( 'wp-api' ), '1.0', true );

    // Pass data to the script, including the REST API URL and nonce
    wp_localize_script( 'myplugin-pricing', 'myplugin_ajax_object', array(
        'ajax_url' => rest_url( 'myplugin/v1/pricing/product/' ),
        'nonce'    => wp_create_nonce( 'wp_rest' ),
    ) );
} );

The JavaScript code would then look something like this:

jQuery(document).ready(function($) {
    var productId = 123; // Example product ID

    $.ajax({
        url: myplugin_ajax_object.ajax_url + productId,
        method: 'GET',
        beforeSend: function ( xhr ) {
            // Add nonce for authenticated requests
            xhr.setRequestHeader( 'X-WP-Nonce', myplugin_ajax_object.nonce );
        }
    })
    .done(function(response) {
        console.log('Pricing data:', response);
        // Logic to display pricing based on response.effective_price, response.volume_discounts, etc.
        if (response.effective_price) {
            $('#product-price').text('Your Price: $' + response.effective_price);
        }
        // Further logic for volume discounts, etc.
    })
    .fail(function(jqXHR, textStatus, errorThrown) {
        console.error('Error fetching pricing:', textStatus, errorThrown);
        // Handle errors, e.g., display a message to the user
    });
});

Security Best Practices:

  • Nonce Verification: Always use nonces for any requests that modify data or require authentication. The wp_create_nonce( 'wp_rest' ) and X-WP-Nonce header are standard for REST API interactions.
  • Capability Checks: Rigorously check capabilities for any write operations. For read operations, implement checks within the callback if the data is sensitive.
  • Input Sanitization: Sanitize all data received via API requests (as demonstrated with sanitize_text_field and custom sanitizers) and all data before saving it to the database.
  • HTTPS: Ensure your WordPress site uses HTTPS to protect data in transit.
  • Role Management: Regularly audit user roles and capabilities to ensure least privilege.

Conclusion

By combining custom REST API endpoints for granular metadata management with robust role and capability-based access control, you can build a secure and highly adaptable B2B pricing grid system within WordPress. This approach provides the flexibility needed for complex pricing structures while maintaining the integrity and security of your business data.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Step-by-Step Guide to building a custom automatic translation switcher block for Gutenberg using Alpine.js lightweight states
  • How to securely integrate Google Analytics v4 REST endpoints into WordPress custom plugins using Block Patterns API
  • How to securely integrate Slack Webhooks integration endpoints into WordPress custom plugins using WP HTTP API
  • WordPress Development Recipe: Efficient binary storage and retrieval in custom tables using Readonly classes
  • How to securely integrate Salesforce CRM endpoints into WordPress custom plugins using Heartbeat API

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (647)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (855)
  • PHP (5)
  • PHP Development (38)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (627)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (290)
  • WordPress Theme Development (357)

Recent Posts

  • Step-by-Step Guide to building a custom automatic translation switcher block for Gutenberg using Alpine.js lightweight states
  • How to securely integrate Google Analytics v4 REST endpoints into WordPress custom plugins using Block Patterns API
  • How to securely integrate Slack Webhooks integration endpoints into WordPress custom plugins using WP HTTP API

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (855)
  • Debugging & Troubleshooting (647)
  • Security & Compliance (627)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala