How to use AJAX in WordPress with admin-ajax.php in PHP and JS in WordPress

Contents

Introduction

This article is a comprehensive, practical tutorial for using AJAX in WordPress with admin-ajax.php using PHP and JavaScript. It covers everything you need to know: how admin-ajax.php works, how to register AJAX handlers in PHP, how to enqueue and localize scripts, how to send requests from JavaScript (both jQuery and vanilla JS/fetch), security (nonces and capability checks), sanitization, debugging, common pitfalls, and alternatives (REST API).

How admin-ajax.php works (overview)

WordPress exposes admin-ajax.php as a centralized AJAX endpoint. When a client sends an HTTP request (usually POST or GET) to admin-ajax.php, one mandatory parameter is action which tells WordPress which PHP hook to execute. Handlers are registered in PHP using action hooks named wp_ajax_{action} (for authenticated requests) and wp_ajax_nopriv_{action} (for unauthenticated requests).

Request flow

  1. Client JS sends a request to admin-ajax.php with data that includes action and any other parameters.
  2. WordPress checks the action parameter and executes the registered handler(s): wp_ajax_{action} and/or wp_ajax_nopriv_{action}.
  3. The handler processes input (sanitization and security checks), outputs data (JSON, HTML, text), and ends the response (wp_send_json, echo wp_die(), etc.).

Core hooks reference

Hook When to use
wp_ajax_myaction Use when only logged-in users can call the handler
wp_ajax_nopriv_myaction Use when guests (not logged-in) should be able to call the handler

Best-practice checklist

  • Never trust client input: sanitize and validate all incoming values.
  • Use nonces to prevent CSRF for actions that modify state.
  • Check capabilities for privileged operations (current_user_can()).
  • Return structured JSON (use wp_send_json_success() / wp_send_json_error()).
  • Avoid heavy computation inside admin-ajax.php consider background processing if needed.
  • Prefer REST API for new projects or public APIs. admin-ajax.php remains useful for admin-related or legacy integrations.

Step-by-step example (practical)

Well implement a small example: a front-end button that requests a random quote from the server and shows it. Well provide two JavaScript examples: jQuery and vanilla fetch.

1) PHP: register and implement the AJAX handler (functions.php or plugin file)

Add this to your themes functions.php or a plugin file. It registers handlers for both authenticated and unauthenticated requests, verifies nonce, sanitizes input, and returns JSON.

 Insufficient permissions ), 403 )
    // }

    // Example: read a parameter (optional)
    category = isset( _POST[category] ) ? sanitize_text_field( wp_unslash( _POST[category] ) ) : 

    // Generate or fetch data. Keep processing quick.
    quotes = array(
        Life is what happens when you’re busy making other plans.,
        The only way to do great work is to love what you do.,
        Do one thing every day that scares you.,
    )

    if ( category === short ) {
        quotes = array_slice( quotes, 0, 2 )
    }

    quote = quotes[ array_rand( quotes ) ]

    // Prepare response data (escape on output where appropriate)
    data = array(
        quote => wp_kses_post( quote ), // or esc_html() when embedding in text nodes
    )

    // Send JSON success response (this calls wp_die for you)
    wp_send_json_success( data )
}
?>

2) PHP: enqueue script and localize data (functions.php or plugin)

Enqueue your front-end JS and pass the AJAX URL and nonce to the script using wp_localize_script (or wp_add_inline_script). Using admin_url(admin-ajax.php) provides the correct endpoint URL.

 admin_url( admin-ajax.php ),
            nonce    => wp_create_nonce( my_quote_nonce )
        )
    )
}
?>

3) JavaScript: jQuery example

If your site loads jQuery, heres a concise jQuery-based request. It sends action and security (nonce) fields, handles success and error, and updates the DOM safely.

jQuery(document).ready(function(){
    (#get-quote-btn).on(click, function(e){
        e.preventDefault()

        // Optional data to send
        var data = {
            action: get_random_quote,
            security: MyAjax.nonce,
            category: short // optional
        }

        // Show loading state
        (#quote-output).text(Loading...)

        .post(MyAjax.ajax_url, data)
            .done(function(response){
                // WordPress wp_send_json_success returns object with success and data
                if ( response  response.success ) {
                    // Use text() to avoid injection if you dont trust content
                    (#quote-output).text(response.data.quote)
                } else {
                    // The handler used wp_send_json_error or something unexpected happened
                    var msg = (response  response.data  response.data.message) ? response.data.message : Server error
                    (#quote-output).text(Error:    msg)
                }
            })
            .fail(function(jqXHR, textStatus, errorThrown){
                (#quote-output).text(AJAX failed:    textStatus)
                console.error(AJAX error, textStatus, errorThrown)
            })
    })
})

4) JavaScript: vanilla fetch example (application/x-www-form-urlencoded)

This example uses fetch with URLSearchParams. Note: when using fetch with a non-FormData body, set the content-type appropriately. Using FormData is also common and lets the browser set the content-type boundary automatically.

document.getElementById(get-quote-btn).addEventListener(click, function(e){
    e.preventDefault()

    var data = new URLSearchParams()
    data.append(action, get_random_quote)
    data.append(security, MyAjax.nonce)
    data.append(category, short)

    // Show loading
    document.getElementById(quote-output).textContent = Loading...

    fetch(MyAjax.ajax_url, {
        method: POST,
        headers: {
            Content-Type: application/x-www-form-urlencoded charset=UTF-8
        },
        body: data.toString()
    })
    .then(function(response){
        if (!response.ok) {
            throw new Error(Network response was not ok:    response.status)
        }
        return response.json()
    })
    .then(function(json){
        if (json  json.success) {
            document.getElementById(quote-output).textContent = json.data.quote
        } else {
            var msg = (json  json.data  json.data.message) ? json.data.message : Server error
            document.getElementById(quote-output).textContent = Error:    msg
        }
    })
    .catch(function(err){
        document.getElementById(quote-output).textContent = Fetch error:    err.message
        console.error(Fetch error:, err)
    })
})

5) JavaScript: vanilla fetch with FormData (multipart/form-data)

If you need to send files, use FormData. Do not set Content-Type manually the browser will do it so boundaries are correct.

var form = document.getElementById(my-upload-form)
form.addEventListener(submit, function(e){
    e.preventDefault()

    var formData = new FormData(form)
    formData.append(action, my_upload_action)
    formData.append(security, MyAjax.nonce)

    fetch(MyAjax.ajax_url, {
        method: POST,
        body: formData
    })
    .then(resp => resp.json())
    .then(json => {
        console.log(json)
    })
    .catch(err => console.error(err))
})

Security details

Nonces

Use nonces for state-changing requests (any action that creates, updates, deletes, or performs privileged reads). Create nonces with wp_create_nonce(your_action_name) and verify with check_ajax_referer(your_action_name,security). The second parameter is the field name you expect in POST/GET (commonly security).

Capability checks

Even with nonces, you should verify that the current user has the capability required for the action:

if ( ! current_user_can( edit_posts ) ) {
    wp_send_json_error( array( message => Not allowed ), 403 )
}

Sanitization and escaping

– Sanitize inputs as soon as you read them (sanitize_text_field, intval, wp_kses_post for HTML inputs).
– Escape outputs at the point of rendering in HTML (esc_html, esc_attr, wp_kses_post).
– When returning raw JSON via wp_send_json, escape user content properly to avoid including malicious HTML in server-provided strings unless explicitly allowed and sanitized.

Response patterns

  • Use wp_send_json_success(data): returns { success: true, data: data } and dies.
  • Use wp_send_json_error(data, status_code): returns { success: false, data: data } and dies. Optionally send HTTP status codes with wp_send_json_error(…, status_code) by setting status header before. Note: wp_send_json_error signature doesnt accept status—use status header wp_send_json_error for custom codes.
  • Return HTML: return HTML in the data object (eg. array(html => html)) and in JS insert using innerHTML only if you trust/clean it otherwise insert as text.

Debugging tips

  • In browser devtools, inspect the network tab for the admin-ajax.php request: POST payload, headers, response body and status code.
  • Make sure action parameter is present and spelled exactly like the hook suffix.
  • If you get a 0 response, WordPress likely died (often a missing action handler or failing nonce). Check server error logs and enable WP_DEBUG for local debugging.
  • Use error_log() or error_log(print_r(var, true)) for quick server-side logging (in development).

Common pitfalls

  1. Forgetting to include action in request (WordPress returns 0).
  2. Forgetting to localize or set the correct ajax_url hardcoding urls can break on multisite or non-standard admin paths.
  3. Not using wp_send_json_ and echoing JSON manually without proper headers—use the built-in helpers.
  4. Not verifying nonces or capabilities—leads to CSRF or privilege escalation.
  5. Heavy synchronous processing inside the handler slows down admin-ajax calls for all visitors. Consider offloading to background jobs.

Performance and scaling considerations

  • admin-ajax.php loads the entire WP stack which is heavier than REST endpoints defined with lower overhead for simple needs. If you need many public AJAX endpoints, the REST API may be more efficient and cache-friendly.
  • Use caching where possible (transients, object caching) for repeated expensive operations.
  • Minimize operations and DB queries inside AJAX handlers return precomputed content when possible.
  • Rate-limit endpoints or implement nonce-checks capability checks for heavy operations to reduce abuse.

Alternative: WordPress REST API

For new projects, consider the REST API (wp-json). The REST API provides better standards, HTTP verbs, built-in JSON handling, and is easier to cache and secure for public APIs. However, admin-ajax.php remains useful for quick admin-side AJAX or when migrating legacy code.

Advanced: Return status codes from admin-ajax.php

To return a custom HTTP status code, set the status header before sending JSON:

status_header( 403 )
wp_send_json_error( array( message => Forbidden ) )

Complete minimal example recap

  1. PHP handler: add_action hooks, check_ajax_referer, validate, sanitize, use wp_send_json_success/error.
  2. Enqueue and localize script: pass admin_url(admin-ajax.php) and nonce via wp_localize_script.
  3. JavaScript: send action security other params handle JSON response and update DOM.
  4. Security: nonces and capability checks sanitize inputs and escape outputs.

Additional resources

Final notes

This article provided a full, practical guide to using admin-ajax.php for AJAX calls in WordPress with examples in PHP and JavaScript. Follow the security and performance recommendations, test in development with WP_DEBUG on, and prefer the REST API for public-facing APIs when appropriate.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *