Back to Archive

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.

Published June 8, 2026Updated June 8, 20263 min read
WordPress admin-ajax.php Returns -1: Nonce Checks to Fix First preview image

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_nonce and _wpnonce by 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

  1. The nonce field name does not match. If JavaScript sends nonce but PHP calls check_ajax_referer( 'action' ) without the second argument, WordPress looks for _ajax_nonce or _wpnonce, not nonce.
  2. The nonce action string changed. wp_create_nonce( 'save_widget' ) will not verify against check_ajax_referer( 'vulnwp_save_widget', 'nonce' ).
  3. 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.
  4. 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.
  5. 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_name for logged-in users.
  • If guests are allowed, wp_ajax_nopriv_your_action_name is 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() and check_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.

References and further reading

Popular Guides

Popular WordPress guides to read next.

These articles connect recurring production concerns: implementation details, updates, troubleshooting, recovery paths, and operational cleanup.

Continue reading

More from the archive