Learn how to register and implement secure WordPress AJAX handlers in a UTB Product Builder flow, using the helpers provided by AbstractFlow.
Flows can expose WordPress AJAX endpoints for real-time UI interactions: calculating discounts, fetching dependent field options, validating documents, and so on. AbstractFlow takes care of registration and provides helpers for nonces, sanitization, and JSON responses.
Use these inherited methods to clean user input before processing or storing it:
/** Sanitize a generic text field (strips tags, trims whitespace, unslashes). */protected function sanitize_text($value): string{ return sanitize_text_field(wp_unslash((string) $value));}/** Sanitize an email address. */protected function sanitize_email_field($value): string{ return sanitize_email(wp_unslash((string) $value));}/** Cast to integer — safe for IDs and numeric values. */protected function sanitize_int($value): int{ return (int) $value;}
Real example: ajax_calculate_discount from CEPFlow
The following is the actual AJAX handler in CEPFlow that validates a student’s identity and returns a discount tier. It illustrates all the patterns above.
/** * AJAX: Validate document and calculate discount in one step. * Action: cep_calculate_discount */public function ajax_calculate_discount(): void{ // 1. Nonce verification — CEPFlow uses check_ajax_referer() directly here. if (!check_ajax_referer('utb_cep_nonce', 'nonce', false)) { $this->ajax_error( 'La sesión ha expirado o la petición no es válida.', 'security_check_failed', 403 ); return; } // 2. Input validation. $programa_codigo = isset($_POST['programa_codigo']) ? sanitize_text_field($_POST['programa_codigo']) : ''; $documento = isset($_POST['documento']) ? absint($_POST['documento']) : 0; if (empty($programa_codigo) || empty($documento)) { $this->ajax_error('Datos incompletos', 'missing_data', 400); return; } $primer_nombre = isset($_POST['primer_nombre']) ? sanitize_text_field($_POST['primer_nombre']) : ''; $primer_apellido = isset($_POST['primer_apellido']) ? sanitize_text_field($_POST['primer_apellido']) : ''; // 3. Transient cache to avoid redundant API calls. $cache_key = 'cep_disc_' . md5($documento . '|' . $programa_codigo); $cached = get_transient($cache_key); if ($cached && is_array($cached)) { $cached['cached'] = true; $this->ajax_success($cached); return; } // 4. Business logic — external API call. $roles_data = $this->roles_validator->get_user_roles($documento); if (is_wp_error($roles_data)) { $this->ajax_error($roles_data->get_error_message(), 'user_validation_error', 400); return; } $roles_norm = (array) ($roles_data['roles_norm'] ?? []); if (empty($roles_norm)) { $this->ajax_error('No se encontraron roles activos para el documento.', 'no_roles', 400); return; } $result = $this->discount_calculator->get_discount_for_program($programa_codigo, $roles_norm); if (is_wp_error($result)) { $this->ajax_error($result->get_error_message(), $result->get_error_code(), 400); return; } // 5. Cache the result and respond. set_transient($cache_key, $result, 15 * MINUTE_IN_SECONDS); $this->ajax_success($result);}
Before shipping an AJAX handler, verify each item:
Verify the nonce
Always call $this->verify_nonce() or check_ajax_referer() as the first statement. Return a 403 error immediately if it fails.
Sanitize every input
Use $this->sanitize_text(), $this->sanitize_email_field(), $this->sanitize_int(), or the equivalent WordPress functions on every $_POST value before using it.
Validate before processing
Check that required fields are present and in range before running business logic or queries. Return a descriptive 400 error for invalid input.
Use capability checks where appropriate
If an action should be restricted to logged-in users or administrators, call is_user_logged_in() or current_user_can() and return a 403 error for unauthorized requests.
Cache external API calls
Use WordPress transients to cache results of slow or rate-limited API calls. CEPFlow caches discount lookups for 15 minutes.
Never trust $data for DB queries
Use $wpdb->prepare() for any database query that includes user-supplied values. Never interpolate $_POST values directly into SQL.