Atualizado em 19/06/2026
A REST API do WordPress expõe o conteúdo e as operações do site como recursos HTTP que retornam JSON. É o que permite o editor de blocos (Gutenberg) salvar posts, um app mobile listar artigos, ou um sistema externo criar pedidos — tudo sem renderizar HTML. Faz parte do núcleo desde o WordPress 4.7 (dezembro de 2016), então está disponível em qualquer instalação atual. Neste artigo você vai aprender a estendê-la com endpoints customizados, do registro da rota à autenticação, com a atenção a segurança que separa um especialista de quem só copia trechos prontos.
Anatomia: namespace, route e endpoint
Toda a API vive sob a base /wp-json/. A partir daí, três conceitos se encaixam um dentro do outro:
- Namespace: agrupa as suas rotas e versiona a API. Os endpoints do núcleo usam
wp/v2; o seu plugin deve usar algo único comomeu-plugin/v1. Versionar (o/v1) permite lançar um/v2no futuro sem quebrar quem já consome o/v1. - Route: o caminho dentro do namespace, ex.:
/livrosou/livros/(?P<id>\d+). A parte entre parênteses é uma expressão regular nomeada que captura um parâmetro da URL. - Endpoint: a combinação de uma route com um método HTTP (GET, POST, etc.) e o callback que a atende. Uma mesma route pode ter vários endpoints (um para GET, outro para POST).
Na prática, fazer um GET /wp-json/wp/v2/posts devolve os últimos posts em JSON; GET /wp-json/wp/v2/posts/42 devolve o post 42. Antes de criar qualquer rota, vale conhecer esses endpoints padrão — muitas vezes eles já resolvem o problema.
Para descobrir tudo que a API expõe em um site, faça um GET em /wp-json/ (o índice) ou em /wp-json/wp/v2/. A resposta é auto-documentada: lista namespaces, rotas, métodos e os argumentos aceitos por cada um.
Registrando uma rota com register_rest_route()
O registro deve acontecer no hook rest_api_init — é o momento em que a API está montando suas rotas. A função-chave é register_rest_route( $namespace, $route, $args ). O exemplo abaixo registra dois endpoints na mesma route: um GET para ler um livro pelo ID e um POST para criar um livro.
<?php
add_action( 'rest_api_init', 'meu_plugin_registrar_rotas' );
function meu_plugin_registrar_rotas() {
register_rest_route( 'meu-plugin/v1', '/livros/(?P<id>\d+)', array(
// Endpoint de LEITURA: GET /wp-json/meu-plugin/v1/livros/42
array(
'methods' => WP_REST_Server::READABLE, // 'GET'
'callback' => 'meu_plugin_obter_livro',
'permission_callback' => '__return_true', // leitura pública (consciente)
'args' => array(
'id' => array(
'description' => 'ID do livro (post).',
'type' => 'integer',
'required' => true,
'validate_callback' => function ( $valor ) {
return is_numeric( $valor ) && (int) $valor > 0;
},
'sanitize_callback' => 'absint',
),
),
),
// Endpoint de ESCRITA: POST /wp-json/meu-plugin/v1/livros/42
array(
'methods' => WP_REST_Server::CREATABLE, // 'POST'
'callback' => 'meu_plugin_atualizar_livro',
'permission_callback' => function () {
// Só quem pode editar posts grava aqui.
return current_user_can( 'edit_posts' );
},
'args' => array(
'titulo' => array(
'type' => 'string',
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
),
),
),
) );
}
Repare em três decisões de especialista. Primeiro, o parâmetro id é capturado pela regex (?P<id>\d+) — o \d+ garante que só dígitos cheguem à rota, e o nome id torna o valor acessível no callback. Segundo, cada argumento tem validate_callback (rejeita o que não serve) e sanitize_callback (limpa o que passou). Terceiro, os métodos usam as constantes de WP_REST_Server em vez de strings soltas — é mais legível e à prova de erro de digitação.
O permission_callback não é opcional
Desde o WordPress 5.5, registrar uma rota sem permission_callback dispara um aviso de doing it wrong. A razão é didática e de segurança: o desenvolvedor precisa decidir explicitamente quem pode acessar cada endpoint, em vez de deixar tudo aberto por descuido. O callback pode devolver:
true— autorizado;false— negado (a API responde 401/403);- um objeto
WP_Error— negado, com a mensagem e o status que você definir.
__return_true libera o endpoint para qualquer pessoa na internet, autenticada ou não. Use-o apenas em leituras de dados que já seriam públicos no site. Para qualquer escrita, exclusão, ou dado privado, verifique uma capability com current_user_can() — nunca confie só no fato de o usuário estar logado.
O callback: WP_REST_Request, WP_REST_Response e WP_Error
O callback recebe um único objeto WP_REST_Request, de onde você lê os parâmetros (de URL, query string ou corpo JSON) com sintaxe de array: $req['id']. Ele deve retornar dados (que viram JSON), um objeto WP_REST_Response (quando você quer controlar status e cabeçalhos) ou um WP_Error (para falhas com código HTTP apropriado).
<?php
function meu_plugin_obter_livro( WP_REST_Request $req ) {
$id = (int) $req['id']; // já sanitizado por absint
$post = get_post( $id );
// Recurso inexistente ou tipo errado -> 404 explícito.
if ( ! $post || 'livro' !== $post->post_type ) {
return new WP_Error(
'livro_nao_encontrado',
'Nenhum livro com esse ID.',
array( 'status' => 404 )
);
}
// Monte só o que pode ser exposto — não devolva o objeto WP_Post cru.
$dados = array(
'id' => $post->ID,
'titulo' => get_the_title( $post ),
'resumo' => get_the_excerpt( $post ),
'permalink' => get_permalink( $post ),
);
$resposta = new WP_REST_Response( $dados, 200 );
$resposta->header( 'Cache-Control', 'public, max-age=300' );
return $resposta;
}
Dois pontos cruciais. O WP_Error com array( 'status' => 404 ) faz a API responder com o código HTTP correto — clientes bem-feitos dependem disso para distinguir "não existe" de "erro do servidor". E jamais devolva o objeto WP_Post inteiro: ele carrega campos internos (como post_password ou conteúdo de rascunhos) que não deveriam vazar. Monte explicitamente o array de saída com apenas o que é público.
Métodos HTTP e as constantes de WP_REST_Server
Cada verbo HTTP tem uma constante de atalho. Usá-las evita erros sutis (a constante EDITABLE, por exemplo, cobre três verbos de uma vez):
| Intenção | Método(s) HTTP | Constante |
|---|---|---|
| Ler um recurso | GET | WP_REST_Server::READABLE |
| Criar um recurso | POST | WP_REST_Server::CREATABLE |
| Atualizar um recurso | POST, PUT, PATCH | WP_REST_Server::EDITABLE |
| Apagar um recurso | DELETE | WP_REST_Server::DELETABLE |
| Qualquer método | GET, POST, PUT, PATCH, DELETE | WP_REST_Server::ALLMETHODS |
Autenticação: escolha pelo contexto
A REST API só sabe quem está chamando se a requisição carregar uma forma de identidade. As leituras de dados públicos não exigem nada; tudo que é privado ou de escrita, sim. Há três caminhos principais.
1. Cookie + nonce (same-origin)
É o método para chamadas feitas de dentro do próprio site (tema, plugin, painel) por um usuário já logado. O cookie de sessão viaja automaticamente; o que falta é provar que a chamada não vem de outro domínio (proteção CSRF). Isso é feito com um nonce enviado no cabeçalho X-WP-Nonce. Gere o nonce com a ação wp_rest e entregue-o ao JavaScript:
<?php
add_action( 'wp_enqueue_scripts', 'meu_plugin_assets_rest' );
function meu_plugin_assets_rest() {
wp_enqueue_script(
'meu-app',
plugins_url( 'js/app.js', __FILE__ ),
array(),
'1.0.0',
true
);
// Entrega a raiz da API e um nonce válido ao front-end.
wp_localize_script( 'meu-app', 'MEU_APP', array(
'root' => esc_url_raw( rest_url( 'meu-plugin/v1/' ) ),
'nonce' => wp_create_nonce( 'wp_rest' ),
) );
}
// js/app.js — consumindo o endpoint com fetch()
async function atualizarLivro( id, titulo ) {
const resp = await fetch( MEU_APP.root + 'livros/' + id, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': MEU_APP.nonce, // prova same-origin + login
},
body: JSON.stringify( { titulo: titulo } ),
} );
if ( ! resp.ok ) {
// 4xx/5xx: leia o erro JSON que o WordPress devolve.
const erro = await resp.json();
throw new Error( erro.message || 'Falha na requisição.' );
}
return resp.json();
}
O nonce do WordPress tem vida curta: é aceito no tick atual e no anterior, e cada tick vale metade do nonce_life (24h por padrão), então a validade efetiva fica entre 12 e 24 horas. Em páginas que ficam abertas muito tempo, ele pode expirar e a API passa a responder 403 com o código rest_cookie_invalid_nonce — nesses casos, atualize a página ou renove o nonce.
2. Application Passwords (acesso externo)
Para clientes que rodam fora do site (um servidor, um script, um app), o nonce não se aplica. Desde o WordPress 5.6, o núcleo traz Application Passwords: cada usuário gera, no próprio perfil, senhas dedicadas por aplicação, enviadas via HTTP Basic Auth. A grande vantagem é que você pode revogar uma aplicação sem trocar a senha principal da conta.
# Exemplo com curl — sempre sobre HTTPS, nunca HTTP puro.
# O usuário e a senha de aplicação vão no Basic Auth.
curl --user "usuario:xxxx yyyy zzzz aaaa bbbb cccc" \
https://seusite.com.br/wp-json/wp/v2/posts \
-H "Content-Type: application/json" \
-d '{"title":"Post via API","status":"draft"}'
Application Passwords só funcionam sobre HTTPS (o WordPress as desabilita em conexões não seguras, exceto em ambiente local explícito). Basic Auth envia a credencial em toda requisição; sem TLS, ela trafega praticamente em texto claro. Trate cada senha de aplicação como uma chave: armazene em variável de ambiente, nunca no código versionado.
3. JWT e OAuth (via plugin)
Quando você precisa de tokens com expiração, escopos ou um fluxo de autorização de terceiros (um app que age em nome do usuário), o caminho é JWT ou OAuth 1.0a por plugin — não há implementação nativa no núcleo para esses fluxos. São indicados para APIs públicas consumidas por aplicações cliente que você não controla. Para integrações internas, cookie+nonce e Application Passwords quase sempre bastam.
Boas práticas e segurança
- Sempre defina o
permission_callback, mesmo que seja para devolvertruede propósito — a omissão é bug, não atalho. - Valide e sanitize todo argumento em
args. A entrada de uma API é hostil por definição; trate-a com a mesma desconfiança da segurança no código. - Pagine listas. Devolver "todos os registros" não escala. Aceite
per_pageepagee exponha o total em cabeçalhos comoX-WP-Total, como fazem os endpoints padrão. - Não vaze dados privados. Monte a resposta com campos explícitos; nunca jogue o objeto do banco direto no JSON.
- Devolva o status HTTP correto via
WP_Error(404, 403, 400, 422...). É a forma de o cliente reagir bem a falhas. - Permalinks ligados para servir a URL
/wp-json/bonita; sem eles, recorra a?rest_route=. - Cuidado com CORS. Por padrão, a API responde same-origin. Se um domínio externo precisa consumi-la pelo navegador, ajuste os cabeçalhos com cautela — abrir o CORS para tudo é um risco.
Armadilhas comuns (e como evitar)
| Sintoma | Causa provável | Correção |
|---|---|---|
| Aviso doing it wrong no log | permission_callback ausente | Defina-o sempre (ex.: current_user_can(...)) |
| Rota não aparece / 404 | Registro fora do hook rest_api_init | Registre dentro de rest_api_init |
/wp-json/ dá 404 | Links permanentes desativados | Ative permalinks ou use ?rest_route= |
403 rest_cookie_invalid_nonce | Nonce ausente ou expirado | Envie X-WP-Nonce; renove se a página ficou aberta |
| Parâmetro chega como string | Faltou sanitize_callback | Use absint / sanitize_text_field em args |
| Dados internos no JSON | Retornou o objeto cru do banco | Monte um array só com campos públicos |
Endpoints customizados costumam morar em um plugin próprio, e a alternativa clássica de comunicação assíncrona — útil em contextos onde a REST API é exagero — é o AJAX no WordPress. Explore também os demais temas de desenvolvimento.
Perguntas frequentes
Por que recebo o aviso "permission_callback was not provided"?
Desde o WordPress 5.5, todo endpoint precisa de um permission_callback. Se você omitir, o WordPress registra um doing it wrong no log e, em versões recentes, pode até recusar a rota. O callback deve devolver true, false ou um WP_Error. Se a rota for realmente pública (leitura de dados que já são públicos), use __return_true — mas de forma consciente, sabendo que qualquer pessoa poderá chamá-la. Para qualquer escrita ou dado sensível, verifique uma capability com current_user_can().
Qual a diferença entre sanitize_callback e validate_callback?
O validate_callback roda primeiro e responde "este valor é aceitável?" — retornando true/false ou um WP_Error; se reprovar, o WordPress devolve 400 antes mesmo de chamar seu callback. O sanitize_callback roda depois e transforma o valor para uma forma segura (ex.: absint, sanitize_text_field). Pense em validar como o porteiro e sanitizar como a faxina: um barra o que não serve, o outro limpa o que passou.
A URL /wp-json/ não funciona, só ?rest_route=. Por quê?
As URLs "bonitas" do tipo /wp-json/ dependem de links permanentes ativados (qualquer estrutura diferente de "Simples"). Em Configurações → Links permanentes, escolha por exemplo "Nome do post" e salve para regravar as regras de rewrite. Sem permalinks bonitos, a API ainda funciona pela forma alternativa ?rest_route=/wp/v2/posts, que não depende de mod_rewrite. Veja também o erro de 404 e links permanentes.
Como chamo a REST API a partir do próprio tema/plugin (same-origin)?
Use autenticação por cookie + nonce. Gere o nonce no PHP com wp_create_nonce('wp_rest'), passe-o ao JavaScript (via wp_localize_script) e envie-o no cabeçalho X-WP-Nonce de cada requisição. O cookie de login do usuário já viaja junto, e o nonce comprova que a chamada partiu do seu site, não de outro domínio. É o mesmo princípio usado no AJAX no WordPress.
E para acessar a API de fora do site (outro servidor, app)?
O nonce de cookie não serve para clientes externos. Para acesso de servidor a servidor ou apps, use Application Passwords (nativo desde o WordPress 5.6), que cria senhas específicas por aplicação enviadas via HTTP Basic Auth — sempre sobre HTTPS. Para fluxos mais robustos (tokens com expiração, escopos), considere plugins de JWT ou OAuth 1.0a. Nunca trafegue credenciais por HTTP puro.
Devo criar um endpoint customizado ou usar os endpoints padrão wp/v2?
Se você só precisa ler ou gravar posts, páginas, taxonomias ou usuários, os endpoints nativos wp/v2 já cobrem isso (inclusive campos via register_meta e register_rest_field). Crie uma rota própria quando a resposta tem um formato específico da sua aplicação, agrega dados de várias fontes, ou expõe uma ação de negócio (ex.: "calcular frete", "validar cupom"). Endpoint customizado é para a sua lógica; os padrões são para CRUD de conteúdo.