# admin-ajax.php returns 0 (or -1): the five causes

An AJAX request to wp-admin/admin-ajax.php comes back with nothing but 0. That 0 means WordPress found no handler for the action — or the handler forgot…

Source: https://webkodingtheme.com/blog/admin-ajax-php-returns-0/

---

WordPress

# admin-ajax.php returns 0 (or -1): the five causes

AÖ Arif Özelci · Published 30 Oct 2026 · 6 min read
An AJAX request to wp-admin/admin-ajax.php comes back with nothing but 0. That 0 means WordPress found no handler for the action — or the handler forgot to stop. Here are the five reasons, in the order to check them, and what a -1 or a 400 means instead.

## What the 0 actually means

admin-ajax.php reads one parameter, action, and fires the hook wp_ajax_{action} for a logged-in user or wp_ajax_nopriv_{action} for a visitor. Whatever your handler prints is the response. When the script finishes, it calls wp_die( &#x27;0&#x27; ). So a bare 0 means one of two things: no function was hooked to that action for this kind of user, or a function ran but did not stop the script itself, and WordPress appended its 0 to whatever it printed.
In current WordPress, a missing action, or one with no handler for this kind of user, returns 0 with HTTP status 400 — which is why the browser’s network tab may show “400 Bad Request” alongside the 0.

## The five causes

- Only wp_ajax_ is registered, and the request comes from a logged-out visitor. Front-end features need both hooks: add_action( &#x27;wp_ajax_my_action&#x27;, ... ) and add_action( &#x27;wp_ajax_nopriv_my_action&#x27;, ... ). Testing while logged in hides this completely.
- The action parameter never arrived. admin-ajax.php reads $_REQUEST[&#x27;action&#x27;]. A fetch() call with a JSON body (Content-Type: application/json) sends no form fields at all, so PHP sees no action. Send FormData or application/x-www-form-urlencoded, or put action in the query string.
- The action name does not match. The hook is wp_ajax_ plus exactly the action value: action=save_cart needs wp_ajax_save_cart. A prefix added in one place and not the other, or a typo, is enough.
- The add_action call never runs during the AJAX request. If it sits inside a shortcode, a template file, or behind a check that is false in admin-ajax — ! is_admin(), for instance, which is false there because admin-ajax counts as admin — the hook is never registered. Register AJAX hooks at plugin load or on init, unconditionally.
- The handler does not end with wp_die(). It works — the data is saved — but the response is your output followed by 0, which breaks JSON.parse. End every handler with wp_send_json_success() / wp_send_json_error(), which send JSON and stop, or with wp_die().

## A handler that works for everyone
add_action( &#x27;wp_ajax_wk_save_note&#x27;, &#x27;wk_save_note&#x27; );
add_action( &#x27;wp_ajax_nopriv_wk_save_note&#x27;, &#x27;wk_save_note&#x27; );

function wk_save_note() {
check_ajax_referer( &#x27;wk_save_note&#x27;, &#x27;nonce&#x27; ); // dies with -1 on a bad nonce
$note = sanitize_text_field( wp_unslash( $_POST[&#x27;note&#x27;] ?? &#x27;&#x27; ) );
// ... save it ...
wp_send_json_success( array( &#x27;saved&#x27; => $note ) ); // sends JSON and stops
} const body = new FormData();
body.append(&#x27;action&#x27;, &#x27;wk_save_note&#x27;);
body.append(&#x27;nonce&#x27;, wkAjax.nonce);
body.append(&#x27;note&#x27;, &#x27;Hello&#x27;);

const res = await fetch(wkAjax.url, { method: &#x27;POST&#x27;, body });
const json = await res.json(); // { success: true, data: { saved: "Hello" } }
wkAjax comes from wp_localize_script() or wp_add_inline_script(), with url set to admin_url( &#x27;admin-ajax.php&#x27; ) and nonce to wp_create_nonce( &#x27;wk_save_note&#x27; ).

## If it returns -1 instead

-1 (usually with status 403) is a failed nonce check: check_ajax_referer() could not verify the nonce. The page was cached with an old nonce, the nonce was created for a different action name, or it was created for a logged-out visitor and checked after they logged in. Exclude the page from full-page cache, or fetch a fresh nonce before the request.

## Seeing what the server got

- Browser dev tools → Network → the admin-ajax.php request → Payload: is action there, with the value you expect?
- Response tab: anything before the 0 is output from your handler that ran without stopping.
- Temporarily log the request at the top of the handler with error_log( print_r( $_REQUEST, true ) ); with WP_DEBUG_LOG on — if nothing is logged, the hook is not registered.
- Consider the REST API for new code: register_rest_route() gives you routes, methods, permission callbacks and JSON responses without these pitfalls.
Our plugins follow these rules — nonces on every request, JSON responses, hooks registered at load — and ship their full source. [See the plugins](/plugins). Browse the plugins →

AÖ
Arif Özelci Builds the WebKoding plugins. Answers support himself.
Share on X LinkedIn Copy link
In this article What the 0 actually means The five causes A handler that works for everyone If it returns -1 instead Seeing what the server got
Related Your WooCommerce webhook is not firing. Here is the order to check things in. “There has been a critical error on this website”: the fix
