WordPress AJAX Hooks

WordPress uses dynamic action hooks to route AJAX requests to handler functions.

Primary Action Hooks

wp_ajax_{action}

Fires for authenticated (logged-in) users.

File: wp-admin/admin-ajax.php
Since: 2.1.0

do_action( "wp_ajax_{$action}" );

The {action} portion is the value of $_REQUEST['action'].

Example:

// Handler for action: my_custom_action
add_action('wp_ajax_my_custom_action', 'handle_my_custom_action');

function handle_my_custom_action() {
    check_ajax_referer('my_nonce', 'security');
    
    // Only logged-in users reach this code
    $user_id = get_current_user_id();
    
    wp_send_json_success(array('user_id' => $user_id));
}

JavaScript:

jQuery.post(ajaxurl, {
    action: 'my_custom_action',  // Maps to wp_ajax_my_custom_action
    security: nonce
});

wp_ajax_nopriv_{action}

Fires for unauthenticated (logged-out) users.

File: wp-admin/admin-ajax.php
Since: 2.8.0

do_action( "wp_ajax_nopriv_{$action}" );

Example:

// Public action - both logged-in and logged-out users
add_action('wp_ajax_get_posts', 'handle_get_posts');
add_action('wp_ajax_nopriv_get_posts', 'handle_get_posts');

function handle_get_posts() {
    $posts = get_posts(array('numberposts' => 5));
    wp_send_json_success($posts);
}

// Private action - only logged-in users
add_action('wp_ajax_delete_post', 'handle_delete_post');
// No nopriv hook = logged-out users get 400 Bad Request

Common Pattern:

// Same handler for both
add_action('wp_ajax_search', 'handle_search');
add_action('wp_ajax_nopriv_search', 'handle_search');

// Or different handlers
add_action('wp_ajax_save_draft', 'handle_save_draft_auth');
add_action('wp_ajax_nopriv_save_draft', 'handle_save_draft_noauth');

function handle_save_draft_noauth() {
    wp_send_json_error('Please log in to save drafts', 401);
}

Security Hook

check_ajax_referer

Fires after nonce verification in check_ajax_referer().

File: wp-includes/pluggable.php
Since: 2.1.0

do_action( 'check_ajax_referer', string $action, false|int $result );

Parameters:

Parameter Type Description
$action string The nonce action
$result false|int false if invalid, 1 if 0-12h old, 2 if 12-24h old

Example:

add_action('check_ajax_referer', 'log_ajax_security', 10, 2);

function log_ajax_security($action, $result) {
    if ($result === false) {
        error_log(sprintf(
            'AJAX security failure: action=%s, IP=%s, user=%d',
            $action,
            $_SERVER['REMOTE_ADDR'],
            get_current_user_id()
        ));
    }
}

Heartbeat Hooks

The Heartbeat API uses AJAX for periodic server communication.

heartbeat_received

Filters the Heartbeat response for logged-in users.

File: wp-admin/includes/ajax-actions.php
Since: 3.6.0

$response = apply_filters( 'heartbeat_received', array $response, array $data, string $screen_id );

Parameters:

Parameter Type Description
$response array Response data to send back
$data array Data received from client
$screen_id string Current admin screen ID

Example:

add_filter('heartbeat_received', 'my_heartbeat_received', 10, 3);

function my_heartbeat_received($response, $data, $screen_id) {
    // Check if client sent our data
    if (isset($data['my_plugin_check'])) {
        $response['my_plugin_status'] = array(
            'pending_tasks' => get_pending_task_count(),
            'last_update'   => get_last_update_time(),
        );
    }
    return $response;
}

heartbeat_send

Filters the Heartbeat response before sending (logged-in users).

File: wp-admin/includes/ajax-actions.php
Since: 3.6.0

$response = apply_filters( 'heartbeat_send', array $response, string $screen_id );

Example:

add_filter('heartbeat_send', 'my_heartbeat_send', 10, 2);

function my_heartbeat_send($response, $screen_id) {
    // Always send notification count
    $response['notification_count'] = get_user_notification_count();
    return $response;
}

heartbeat_tick

Fires when Heartbeat ticks (logged-in users).

File: wp-admin/includes/ajax-actions.php
Since: 3.6.0

do_action( 'heartbeat_tick', array $response, string $screen_id );

Example:

add_action('heartbeat_tick', 'on_heartbeat_tick', 10, 2);

function on_heartbeat_tick($response, $screen_id) {
    // Update user's last activity timestamp
    update_user_meta(get_current_user_id(), 'last_activity', time());
}

heartbeat_nopriv_received

Filters Heartbeat response for logged-out users.

File: wp-admin/includes/ajax-actions.php
Since: 3.6.0

$response = apply_filters( 'heartbeat_nopriv_received', array $response, array $data, string $screen_id );

heartbeat_nopriv_send

Filters Heartbeat response before sending (logged-out users).

File: wp-admin/includes/ajax-actions.php
Since: 3.6.0

$response = apply_filters( 'heartbeat_nopriv_send', array $response, string $screen_id );

heartbeat_nopriv_tick

Fires when Heartbeat ticks (logged-out users).

File: wp-admin/includes/ajax-actions.php
Since: 3.6.0

do_action( 'heartbeat_nopriv_tick', array $response, string $screen_id );

Detection Filter

wp_doing_ajax

Filters whether the current request is an AJAX request.

File: wp-includes/load.php
Since: 4.7.0

$is_ajax = apply_filters( 'wp_doing_ajax', bool $wp_doing_ajax );

Example:

add_filter('wp_doing_ajax', 'force_ajax_mode');

function force_ajax_mode($is_ajax) {
    // Treat custom endpoint as AJAX
    if (isset($_GET['custom_ajax'])) {
        return true;
    }
    return $is_ajax;
}

Core AJAX Actions Reference

WordPress registers these core actions in admin-ajax.php:

GET Actions

Action Handler Description
fetch-list wp_ajax_fetch_list Fetch list table data
ajax-tag-search wp_ajax_ajax_tag_search Search tags/terms
wp-compression-test wp_ajax_wp_compression_test Test compression
imgedit-preview wp_ajax_imgedit_preview Image editor preview
oembed-cache wp_ajax_oembed_cache Cache oEmbed response
autocomplete-user wp_ajax_autocomplete_user User autocomplete
dashboard-widgets wp_ajax_dashboard_widgets Dashboard widgets
logged-in wp_ajax_logged_in Check login status
rest-nonce wp_ajax_rest_nonce Get REST API nonce

POST Actions (Selection)

Action Handler Description
heartbeat wp_ajax_heartbeat Heartbeat API
save-widget wp_ajax_save_widget Save widget settings
delete-comment wp_ajax_delete_comment Delete comment
delete-post wp_ajax_delete_post Delete post
trash-post wp_ajax_trash_post Trash post
inline-save wp_ajax_inline_save Quick edit post
upload-attachment wp_ajax_upload_attachment Upload media
save-attachment wp_ajax_save_attachment Save attachment data
install-plugin wp_ajax_install_plugin Install plugin
update-plugin wp_ajax_update_plugin Update plugin
delete-plugin wp_ajax_delete_plugin Delete plugin

Nopriv Actions

Action Handler Description
heartbeat wp_ajax_nopriv_heartbeat Heartbeat (logged out)
generate-password wp_ajax_nopriv_generate_password Generate password

Complete Hook Usage Example

<?php
/**
 * Plugin Name: AJAX Example
 */

// Enqueue scripts and localize
add_action('wp_enqueue_scripts', 'my_ajax_enqueue');
function my_ajax_enqueue() {
    wp_enqueue_script(
        'my-ajax',
        plugin_dir_url(__FILE__) . 'ajax.js',
        array('jquery', 'heartbeat'),
        '1.0.0',
        true
    );
    
    wp_localize_script('my-ajax', 'MyAjax', array(
        'url'   => admin_url('admin-ajax.php'),
        'nonce' => wp_create_nonce('my_ajax_nonce'),
    ));
}

// Register AJAX handlers
add_action('wp_ajax_my_action', 'handle_my_action');
add_action('wp_ajax_nopriv_my_action', 'handle_my_action_public');

function handle_my_action() {
    check_ajax_referer('my_ajax_nonce', 'security');
    
    if (!current_user_can('edit_posts')) {
        wp_send_json_error('Unauthorized', 403);
    }
    
    // Process request...
    wp_send_json_success(array('message' => 'Done'));
}

function handle_my_action_public() {
    wp_send_json_error('Login required', 401);
}

// Hook into Heartbeat
add_filter('heartbeat_received', 'my_heartbeat_received', 10, 3);
function my_heartbeat_received($response, $data, $screen_id) {
    if (!empty($data['my_plugin_tick'])) {
        $response['my_plugin_data'] = get_plugin_status();
    }
    return $response;
}

// Log security failures
add_action('check_ajax_referer', 'log_ajax_failures', 10, 2);
function log_ajax_failures($action, $result) {
    if ($result === false && strpos($action, 'my_') === 0) {
        // Log our plugin's nonce failures
        error_log("My Plugin: Nonce failure for {$action}");
    }
}

Hook Priority

Default priority is 10. For core actions registered in admin-ajax.php, priority is 1:

// Core uses priority 1
add_action('wp_ajax_' . $_POST['action'], 'wp_ajax_' . $function, 1);

// Your handlers default to priority 10
add_action('wp_ajax_my_action', 'my_handler');  // Priority 10

// Override core behavior with lower priority
add_action('wp_ajax_heartbeat', 'my_heartbeat_override', 0);

Debugging AJAX Hooks

// Log all AJAX actions
add_action('admin_init', function() {
    if (wp_doing_ajax()) {
        $action = isset($_REQUEST['action']) ? $_REQUEST['action'] : 'unknown';
        error_log("AJAX Request: {$action}");
        error_log("User: " . (is_user_logged_in() ? get_current_user_id() : 'guest'));
    }
});

// Check if hook exists
add_action('wp_ajax_my_action', function() {
    error_log('Hooks on wp_ajax_my_action: ' . print_r(
        $GLOBALS['wp_filter']['wp_ajax_my_action'] ?? [],
        true
    ));
});