Atualizado em 19/06/2026
AJAX é o que permite a uma página trocar dados com o servidor sem recarregar — curtir um post, filtrar produtos, carregar mais itens, validar um formulário em tempo real. No WordPress há dois caminhos oficiais para isso: o veterano admin-ajax.php, que existe desde os primórdios do painel, e a REST API, incorporada ao núcleo na versão 4.7. Este artigo mostra os dois na prática, com a segurança que eles exigem, e explica em que cenário cada um brilha. Spoiler: a resposta quase nunca é "sempre use X" — é "depende do contexto".
Os dois caminhos, lado a lado
Antes do código, fixe o modelo mental. Ambos recebem uma requisição do navegador e devolvem dados (em geral JSON), mas têm naturezas diferentes:
- admin-ajax.php: um único arquivo que despacha todas as ações AJAX por meio de um parâmetro
action. Carrega o ambiente do WordPress a cada chamada e responde sempre via POST/GET para a mesma URL. Simples e direto, ligado à sessão do usuário. - REST API: cada recurso tem sua própria rota semântica (
/wp-json/...), com método HTTP explícito e respostas potencialmente cacheáveis. Mais estruturada, melhor para integrações e leitura em escala.
Caminho 1: admin-ajax.php (o clássico)
O fluxo tem três peças: (1) enfileirar o script e passar a ele a URL do admin-ajax.php e um nonce; (2) o JavaScript que faz a requisição; (3) o handler PHP que valida e responde.
Os hooks wp_ajax_ e wp_ajax_nopriv_
Cada ação é identificada por um nome (ex.: curtir_post) e atendida por hooks dinâmicos:
wp_ajax_curtir_post— dispara para usuários logados;wp_ajax_nopriv_curtir_post— dispara para visitantes deslogados.
Se a funcionalidade for pública, registre os dois apontando para o mesmo handler. Esquecer o nopriv é a causa mais comum de "funciona quando estou logado, mas no site some" — o WordPress simplesmente devolve 0 para o visitante anônimo, porque nenhum hook respondeu àquela ação.
Passo 1 — Enfileirar o JS e passar ajaxurl + nonce
No painel a variável global ajaxurl já existe; no front-end, não. Por isso passamos a URL (sempre via admin_url()) e um nonce ao script com wp_localize_script():
<?php
add_action( 'wp_enqueue_scripts', 'meu_plugin_enfileirar_ajax' );
function meu_plugin_enfileirar_ajax() {
wp_enqueue_script(
'meu-ajax',
plugins_url( 'js/ajax.js', __FILE__ ),
array(), // sem dependência de jQuery: usaremos fetch()
'1.0.0',
true // no rodapé
);
// Entrega a URL do endpoint e um nonce ao JavaScript.
wp_localize_script( 'meu-ajax', 'MEU_AJAX', array(
'url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'curtir_post_nonce' ),
) );
}
Em código mais novo, wp_add_inline_script() é uma alternativa a wp_localize_script() para injetar dados — este último, apesar do nome, é a forma historicamente usada para passar variáveis ao JS, não só traduções. Ambos dependem de o script já ter sido registrado/enfileirado antes. Veja o artigo sobre enfileirar scripts e estilos.
Passo 2 — O JavaScript (fetch)
O front-end faz um POST para a URL recebida, mandando o parâmetro obrigatório action (que casa com o nome do hook, sem o prefixo) e o nonce:
// js/ajax.js
document.querySelector( '.btn-curtir' )?.addEventListener( 'click', async ( ev ) => {
const botao = ev.currentTarget;
const postId = botao.dataset.postId;
// admin-ajax espera dados de formulário (não JSON cru).
const corpo = new URLSearchParams( {
action: 'curtir_post', // = wp_ajax_curtir_post
post_id: postId,
_ajax_nonce: MEU_AJAX.nonce, // nome que o check_ajax_referer lê
} );
const resp = await fetch( MEU_AJAX.url, {
method: 'POST',
credentials: 'same-origin', // envia o cookie de login
body: corpo,
} );
const json = await resp.json();
if ( json.success ) {
botao.textContent = 'Curtido (' + json.data.total + ')';
} else {
console.error( json.data?.mensagem || 'Erro ao curtir.' );
}
} );
Repare em credentials: 'same-origin': sem isso, o navegador pode não enviar o cookie de sessão, e o WordPress trataria a chamada como deslogada (caindo no hook nopriv). O nome _ajax_nonce é a chave padrão que o check_ajax_referer() procura automaticamente.
Muito código WordPress ainda usa jQuery, que acompanha o núcleo. A mesma requisição com jQuery.post() fica assim — declare jquery como dependência no wp_enqueue_script para garantir que ele já esteja carregado:
// js/ajax.js — versão com jQuery
jQuery( function ( $ ) {
$( '.btn-curtir' ).on( 'click', function () {
const botao = $( this );
$.post( MEU_AJAX.url, {
action: 'curtir_post', // = wp_ajax_curtir_post
post_id: botao.data( 'postId' ),
_ajax_nonce: MEU_AJAX.nonce,
} ).done( function ( res ) {
if ( res.success ) {
botao.text( 'Curtido (' + res.data.total + ')' );
} else {
console.error( res.data && res.data.mensagem );
}
} ).fail( function () {
console.error( 'Falha de rede no AJAX.' );
} );
} );
} );
O jQuery.post() já envia o cookie e serializa os dados como formulário automaticamente, o que torna o código mais enxuto; em projetos novos, porém, fetch() dispensa a dependência. Escolha conforme o que o tema/plugin já carrega — não inclua jQuery só para isso.
Passo 3 — O handler PHP
O handler verifica o nonce, checa permissão quando aplicável, processa e responde com wp_send_json_success() ou wp_send_json_error():
<?php
add_action( 'wp_ajax_curtir_post', 'meu_plugin_curtir_post' );
add_action( 'wp_ajax_nopriv_curtir_post', 'meu_plugin_curtir_post' );
function meu_plugin_curtir_post() {
// 1) Nonce: barra requisições forjadas (CSRF). Encerra sozinho se falhar.
check_ajax_referer( 'curtir_post_nonce' );
// 2) Valide/sanitize a entrada — nunca confie no que chega.
$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
if ( ! $post_id || 'publish' !== get_post_status( $post_id ) ) {
wp_send_json_error(
array( 'mensagem' => 'Post inválido.' ),
400 // status HTTP
);
}
// 3) Lógica de negócio (exemplo: incrementar um contador em post meta).
$total = (int) get_post_meta( $post_id, 'curtidas', true ) + 1;
update_post_meta( $post_id, 'curtidas', $total );
// 4) Resposta. wp_send_json_success() imprime o JSON e encerra (wp_die).
wp_send_json_success( array( 'total' => $total ) );
}
Nunca dê echo, print ou return depois de wp_send_json_success()/wp_send_json_error() — elas já encerram a execução com wp_die(). Qualquer saída extra corromperia o JSON. E lembre: verificar o nonce não é verificar permissão. O nonce só prova que a requisição veio do seu site; se a ação for sensível, adicione current_user_can() para confirmar que aquele usuário pode executá-la.
Caminho 2: REST API (o moderno)
Para o mesmo objetivo, a REST API oferece uma rota dedicada em vez de um action genérico. A estrutura, a validação de args e o permission_callback são o tema completo do artigo de REST API do WordPress; aqui fica o contraste essencial — uma rota semântica, com método HTTP explícito:
<?php
add_action( 'rest_api_init', function () {
register_rest_route( 'meu-plugin/v1', '/curtir/(?P<id>\d+)', array(
'methods' => WP_REST_Server::CREATABLE, // POST
'callback' => 'meu_plugin_rest_curtir',
'permission_callback' => '__return_true', // leitura/escrita pública (consciente)
'args' => array(
'id' => array(
'required' => true,
'sanitize_callback' => 'absint',
),
),
) );
} );
No navegador, a chamada usa o cabeçalho X-WP-Nonce (em vez de um campo _ajax_nonce no corpo) e uma URL limpa como /wp-json/meu-plugin/v1/curtir/42. A resposta pode trazer cabeçalhos de cache, algo que o admin-ajax.php não oferece de fábrica.
Quando usar cada um
| Aspecto | admin-ajax.php | REST API |
|---|---|---|
| Overhead por chamada | Carrega o ambiente WP inteiro a cada requisição | Também carrega o WP, mas com fluxo dedicado a APIs |
| Cache HTTP | Não cacheável (POST para um único arquivo) | GET cacheável; aceita Cache-Control/CDN |
| URL | Genérica: admin-ajax.php?action=... | Semântica: /wp-json/ns/v1/recurso |
| Autenticação | Cookie + nonce (_ajax_nonce) | Cookie+nonce, Application Passwords, JWT/OAuth |
| Validação de entrada | Manual no handler | Declarativa via args (validate/sanitize) |
| Quando preferir | Ações do painel, código legado, operações simples ligadas à sessão | Leitura pública em escala, integrações externas, apps, headless |
Por que isso importa para a performance: o admin-ajax.php sobe todo o ambiente do WordPress a cada requisição e, por ser um POST, não passa por cache de página nem por CDN. Em uma operação de leitura chamada milhares de vezes (um filtro de catálogo, por exemplo), isso vira gargalo. A REST API, com endpoints de leitura via GET, permite cache e alivia o servidor — por isso costuma vencer em escala. Para uma ação ocasional do administrador, a diferença é irrelevante.
Segurança: as regras que valem para os dois
- Nonce sempre. No admin-ajax,
check_ajax_referer(); na REST API, oX-WP-Nonce. Sem isso, qualquer site pode forjar requisições em nome do seu usuário (CSRF). Entenda o mecanismo em segurança no código. - Nonce não é autorização. Ele prova a origem, não o direito. Para ações sensíveis, some a verificação com
current_user_can(). - Nunca confie na entrada. Todo
$_POST/$_GETé hostil até prova em contrário: sanitize comabsint,sanitize_text_field,sanitize_emailconforme o tipo. - Escape na saída. Se o handler devolve HTML, escape com
esc_html/esc_attr; se devolve JSON, use as funçõeswp_send_json_*, que cuidam doContent-Type. - Trate o erro com status HTTP (400, 403...), para o JavaScript reagir corretamente em vez de exibir "sucesso vazio".
Armadilhas comuns (e como evitar)
| Sintoma | Causa provável | Correção |
|---|---|---|
| Funciona logado, falha deslogado | Faltou o hook wp_ajax_nopriv_ | Registre as duas ações para uso público |
A resposta vem só 0 | Nenhum hook atende àquela action | Confira o nome da action e o hook correspondente |
ajaxurl is not defined | Variável só existe no admin | Passe a URL via wp_localize_script no front-end |
| JSON corrompido / lixo no fim | echo ou saída após wp_send_json_* | Não imprima nada depois delas (já chamam wp_die) |
| 403 / "nonce inválido" | Nonce ausente, expirado ou de outra ação | Gere e envie o nonce certo; renove se a página ficou aberta |
| Cookie não enviado | fetch sem credentials | Use credentials: 'same-origin' |
Esse handler de AJAX normalmente vive em um plugin próprio e se conecta ao enfileiramento correto de assets. Para decidir entre os dois caminhos com mais profundidade, leia a REST API do WordPress e explore os demais temas de desenvolvimento.
Perguntas frequentes
Preciso mesmo dos dois hooks, wp_ajax_ e wp_ajax_nopriv_?
Depende de quem usa a funcionalidade. O hook wp_ajax_{action} só atende usuários logados; o wp_ajax_nopriv_{action} atende visitantes deslogados. Se o recurso é só do painel (admin), basta o primeiro. Se um visitante anônimo precisa usá-lo (um formulário público, um filtro de catálogo), você precisa registrar os dois — é o erro nº 1 de quem vê o AJAX "funcionar logado e quebrar deslogado".
Por que o ajaxurl funciona no admin mas é undefined no front-end?
No painel (wp-admin), o WordPress já declara a variável global JavaScript ajaxurl apontando para o admin-ajax.php. No front-end ela não existe — você precisa passá-la ao seu script, normalmente com wp_localize_script() ou wp_add_inline_script(), junto de um nonce. Tentar usar ajaxurl no site público sem isso resulta em ReferenceError.
Onde a URL do admin-ajax.php é obtida no PHP?
Sempre com admin_url('admin-ajax.php'), nunca com uma string fixa. Essa função respeita o domínio, o protocolo (HTTP/HTTPS) e instalações em subdiretório, gerando a URL correta em qualquer ambiente. Cravar a URL na mão quebra em multisite, em HTTPS misto e ao migrar o site.
O que o check_ajax_referer faz exatamente?
Ele verifica o nonce enviado pela requisição contra uma ação. Se o nonce for inválido ou tiver expirado, por padrão a função encerra a execução com uma resposta de erro — protegendo o handler contra CSRF (requisições forjadas por outro site). Você o chama logo no início do handler, antes de qualquer processamento. É o equivalente, no admin-ajax, ao papel do cabeçalho X-WP-Nonce na REST API.
Por que wp_send_json_success encerra o script sozinho?
As funções wp_send_json_success() e wp_send_json_error() imprimem a resposta JSON (com a estrutura {success, data}), enviam o cabeçalho Content-Type correto e chamam wp_die() internamente. Por isso você não deve dar echo nem return manual depois delas — qualquer saída extra contaminaria o JSON. Esse encerramento automático evita o clássico "0" ou lixo no final da resposta.
AJAX clássico ou REST API: qual usar em 2024+?
Para leitura pública e em escala, a REST API tende a ser melhor: respostas cacheáveis, URLs semânticas e autenticação padronizada. O admin-ajax continua ótimo para ações pontuais do painel, para quem já tem código legado nele, ou para operações simples ligadas à sessão do usuário. Não é "antigo contra moderno" — são ferramentas para contextos diferentes, e ambas exigem nonce e validação de entrada.