Implementation Notes / Recovery Playbooks
WordPress admin-ajax.php Returns -1: Nonce Checks to Fix First
Debug admin-ajax.php -1 responses by checking nonce field names, action strings, AJAX hooks, stale cached scripts, and capability checks.
Last reviewed: June 8, 2026
A WordPress AJAX request that returns -1 is usually not a mysterious server failure. In many custom plugins, it means nonce verification failed and check_ajax_referer() stopped the request before your handler reached the useful code. That is why the symptom often appears as a blank response, a 403, or a JavaScript flow that simply stops.
This guide is for plugin developers and WordPress operators debugging custom admin-ajax.php actions. It focuses on the nonce path first because that is the fastest way to distinguish a security check failure from a missing action hook, capability problem, cache issue, or JavaScript request bug.
Key takeaways
check_ajax_referer()checks request fields named_ajax_nonceand_wpnonceby default, unless you pass a custom field name.- The action string used when creating the nonce must match the action string used when verifying it.
- Nonce checks do not replace capability checks. A valid nonce proves request intent, not user authorization.
Start with the minimal working shape
Localize the AJAX URL and nonce when enqueueing the script:
add_action( 'wp_enqueue_scripts', 'vulnwp_enqueue_ajax_example' );
function vulnwp_enqueue_ajax_example() {
wp_enqueue_script(
'vulnwp-ajax-example',
plugin_dir_url( __FILE__ ) . 'assets/ajax-example.js',
array(),
'1.0.0',
true
);
wp_localize_script(
'vulnwp-ajax-example',
'vulnwpAjax',
array(
'url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'vulnwp_save_widget' ),
)
);
}
Send both the WordPress AJAX action and the nonce field from JavaScript:
const body = new FormData();
body.append('action', 'vulnwp_save_widget');
body.append('nonce', vulnwpAjax.nonce);
body.append('title', titleInput.value);
const response = await fetch(vulnwpAjax.url, {
method: 'POST',
credentials: 'same-origin',
body,
});
Then verify the same nonce action and the same request field name in PHP:
add_action( 'wp_ajax_vulnwp_save_widget', 'vulnwp_save_widget' );
function vulnwp_save_widget() {
check_ajax_referer( 'vulnwp_save_widget', 'nonce' );
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array( 'message' => 'Forbidden.' ), 403 );
}
$title = isset( $_POST['title'] )
? sanitize_text_field( wp_unslash( $_POST['title'] ) )
: '';
wp_send_json_success( array( 'title' => $title ) );
}
Why -1 happens
- The nonce field name does not match. If JavaScript sends
noncebut PHP callscheck_ajax_referer( 'action' )without the second argument, WordPress looks for_ajax_nonceor_wpnonce, notnonce. - The nonce action string changed.
wp_create_nonce( 'save_widget' )will not verify againstcheck_ajax_referer( 'vulnwp_save_widget', 'nonce' ). - The request is going to the wrong endpoint. Custom AJAX actions should post to
wp-admin/admin-ajax.php, not directly to a plugin PHP file. - The user state changed. Logged-in nonces are tied to the user session. Logging out, switching accounts, or restoring an old browser tab can invalidate the request.
- A cache serves stale JavaScript data. If the page or inline script containing the nonce is cached too aggressively, users can submit an old nonce.
Debug without weakening the check
Do not remove nonce verification just to make the request work. Temporarily set $stop to false so you can return a structured error during debugging:
$nonce_result = check_ajax_referer( 'vulnwp_save_widget', 'nonce', false );
if ( false === $nonce_result ) {
wp_send_json_error(
array(
'message' => 'Invalid or expired nonce.',
'expected_field' => 'nonce',
),
403
);
}
That preserves the security boundary while making the client-side failure visible. Once the request is fixed, you can keep this clearer response pattern or return to the default stopping behavior.
Request checklist
- The request includes
action=your_action_name. - The PHP hook is registered as
wp_ajax_your_action_namefor logged-in users. - If guests are allowed,
wp_ajax_nopriv_your_action_nameis registered intentionally. - The nonce field name sent by JavaScript matches the second argument to
check_ajax_referer(). - The nonce action string matches between
wp_create_nonce()andcheck_ajax_referer(). - The handler performs
current_user_can()before changing data.
Related reading
For a broader form and AJAX protection pattern, read the WordPress nonce example. If the request also writes custom SQL, pair this with the wpdb::prepare guide.
