agentleFS
AnyOrgNameAnyOrgName/php-skills/skills/wordpress/rest-api/SKILL.md

rest-api

Skill4 starsChanged 4 months agoMITView on GitHub

Build custom WordPress REST API endpoints — register_rest_route, controllers, a mandatory permission_callback, argument sanitization and validation, responses and errors, and authentication with cookie plus nonce or application passwords. Use when the user wants to expose a custom REST API route in WordPress with register_rest_route to read or write data; the user needs to authenticate a WordPress REST request or restrict a route to certain capabilities; the user needs to sanitize and validate incoming arguments on a WordPress REST endpoint. Keywords: wordpress, rest-api, register-rest-route, permission-callback, authentication, sanitization, validation, input, auth, api.

7 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

7 rules. 0 are common, 6 are distinctive, and 1 is very specific to this project.

  • 0

    Common

    Most of the 6 edited versions we read keep these.

  • 6

    Distinctive

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Do not build a REST route for a single internal admin-only action that fits admin-ajax or an admin-post handler.

    line 76
  2. Distinctive:

    Do not hand-roll a route when a core endpoint (posts, users, media) or a post type with show_in_rest already exposes the resource.

    line 77
  3. Distinctive:

    Verify: The localized settings object exposes a nonce string in the page.

    line 125
  4. Distinctive:

    Verify: The logged-in request authenticates and no longer returns 401 rest_cookie_invalid_nonce.

    line 128
  5. Very specific:

    Verify: GET /wp-json/acme/v1/books resolves to the callback.

    line 144
  6. Distinctive:

    Verify: Anonymous callers get 401/403 and invalid arguments return a WP_Error.

    line 147
  7. Distinctive:

    Verify: A valid request returns JSON with status 200 and no stray output.

    line 150

“Common” is measured: kept by at least 6 in 10 of the 6 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md
<!-- generated: do not edit — source: knowledge/atoms/wordpress/rest-api/ --><!-- schema: atomVersion=3 skillVersion=2 graphVersion=1 --><!-- compile: @php-skills/compiler v1.0.0 (deterministic build) -->

WordPress REST API

When to use

  • The user wants to expose a custom REST API route in WordPress with register_rest_route to read or write data.
  • The user needs to authenticate a WordPress REST request or restrict a route to certain capabilities.
  • The user needs to sanitize and validate incoming arguments on a WordPress REST endpoint.

When NOT to use

  • Do not build a REST route for a single internal admin-only action that fits admin-ajax or an admin-post handler.
  • Do not hand-roll a route when a core endpoint (posts, users, media) or a post type with show_in_rest already exposes the resource.

Core guidance

  • Always set a real permission_callback — Give every route a permission_callback that performs a real capability or ownership check; use __return_true only for a deliberately public read and never omit it.
  • Register routes on rest_api_init with a versioned namespace — Call register_rest_route() from a rest_api_init callback using a vendor, versioned namespace such as acme/v1 and a resource-style route.

Decision Engine

Walk this section before choosing an implementation path.

Who calls this route — anonymous readers, your own logged-in pages, or an
external machine client?

├── External server or integration, no browser session: → application-passwords — the client sends Basic auth with a per-application,
revocable credential over HTTPS; still enforce capabilities per route.

│   └── Client needs stateless bearer tokens, not Basic auth: → jwt — add a JWT authentication layer when clients need short-lived stateless
bearer tokens; keep the same per-route capability checks.

├── Your own theme or admin pages, logged-in user: → cookie-nonce — rely on the auth cookie plus the wp_rest nonce in the
X-WP-Nonce header, with a capability check in permission_callback.

└── Anonymous read of already-public data: → public — permission_callback __return_true, read-only methods, and no private
fields; still sanitize and validate every argument.

Full engine: references/decision-engine.md — Decision Tree, Priority Matrix, Decision Table.

Workflows

Workflow: Authenticate Client

Progress:
- [ ] Enqueue your script and `wp_localize_script` a value from
`wp_create_nonce('wp_rest')` so front-end JavaScript can read the nonce
- [ ] In the fetch call set the `X-WP-Nonce` header to the localized nonce and send
credentials so the auth cookie accompanies the request
  1. Enqueue your script and wp_localize_script a value from wp_create_nonce('wp_rest') so front-end JavaScript can read the nonce.
    • Verify: The localized settings object exposes a nonce string in the page.
  2. In the fetch call set the X-WP-Nonce header to the localized nonce and send credentials so the auth cookie accompanies the request.
    • Verify: The logged-in request authenticates and no longer returns 401 rest_cookie_invalid_nonce.

Workflow: Create Endpoint

Progress:
- [ ] Hook `rest_api_init` and call `register_rest_route('acme/v1', '/books', [...])`
with the methods, callback, permission_callback, and args
- [ ] Set a `permission_callback` that checks the capability and define each argument
with type, required, sanitize_callback, and validate_callback
- [ ] In the callback read params with `$request->get_param()`, do the work, and
return `new WP_REST_Response($data, 200)` or a WP_Error on failure
  1. Hook rest_api_init and call register_rest_route('acme/v1', '/books', [...]) with the methods, callback, permission_callback, and args.
    • Verify: GET /wp-json/acme/v1/books resolves to the callback.
  2. Set a permission_callback that checks the capability and define each argument with type, required, sanitize_callback, and validate_callback.
    • Verify: Anonymous callers get 401/403 and invalid arguments return a WP_Error.
  3. In the callback read params with $request->get_param(), do the work, and return new WP_REST_Response($data, 200) or a WP_Error on failure.
    • Verify: A valid request returns JSON with status 200 and no stray output.

Best practices

  • Declare each parameter in the args array with required, type, sanitize_callback, and validate_callback so input is cleaned and rejected before the handler runs
  • For anything beyond a single route, extend WP_REST_Controller (or a class like it) with register_routes, permission methods, get_item_schema, and handlers, instead of many loose closures
  • For logged-in, same-origin (theme or admin) requests send the wp_rest nonce in the X-WP-Nonce header alongside the auth cookie; use application passwords for external, machine-to-machine clients
  • Return a WP_REST_Response (or a value REST will wrap) on success and a WP_Error with an HTTP status in its data on failure, rather than echoing or using wp_send_json

Anti-patterns

Anti-patternWhy it failsDo instead
Reading input from superglobals instead of the requestSuperglobals bypass the args schema, so values arrive unsanitized and unvalidated, and the route's self-documented schema no longer matches its behavior.Declare parameters in the args array and read them with $request->get_param('x') so sanitize and validate callbacks apply.
permission_callback set to __return_true on a write route__return_true grants access to every anonymous caller, turning the route into an unauthenticated write or data-leak endpoint.Replace it with a real check such as current_user_can('edit_posts') or an ownership test, reserving __return_true for genuinely public reads.
Registering routes under an unversioned or generic namespaceGeneric namespaces collide with other plugins and leave no room to evolve the API, forcing breaking changes on existing clients.Use a vendor-versioned namespace such as acme/v1 and bump to acme/v2 for incompatible changes.

See references/anti-patterns.md.

Debugging

  1. Logged-in requests get 401 rest_cookie_invalid_nonce — Cookie authentication only applies when the request carries a valid wp_rest nonce; without the X-WP-Nonce header WordPress treats the caller as anonymous. Fix: Localize the nonce with wp_create_nonce('wp_rest') (or wpApiSettings.nonce) and send it in the X-WP-Nonce request header.
  2. doing_it_wrong notice for a missing permission_callback — Since WordPress 5.5 the permission_callback argument is required; omitting it is a security and correctness bug, not a harmless default. Fix: Add an explicit permission_callback — a capability check, an ownership check, or __return_true only for an intentionally public read.
  3. Echoing output corrupts the JSON response — REST serializes the returned value into the response body; anything echoed is written first and breaks the JSON envelope. Fix: Return a WP_REST_Response or WP_Error instead of echoing, and remove stray debug output before shipping.

Production checklist

  • Every accepted parameter is in the args schema with type, sanitize_callback, and validate_callback.
  • Same-origin clients send the X-WP-Nonce header; external clients use application passwords over HTTPS.
  • Request values are sanitized on input and escaped or bound before reaching SQL, the filesystem, or output.
  • Each register_rest_route supplies a permission_callback with a genuine capability or ownership check.
  • Routes are registered under a vendor-versioned namespace such as acme/v1.
  • Handlers return WP_REST_Response on success and WP_Error with a status on failure, never echo.

Version notes

  • Application passwords built in since WordPress 5.6 (PHP >=7.4) — Application passwords ship in core from WordPress 5.6 and require HTTPS by default; before that, external clients needed a third-party auth plugin such as Basic Auth or JWT.
  • permission_callback required since WordPress 5.5 (PHP >=7.4) — Since WordPress 5.5 a route registered without a permission_callback triggers a doing_it_wrong notice; earlier versions silently allowed it, which produced accidentally public endpoints.

Security

  • permission_callback is mandatory on every route — Treat permission_callback as required security, not optional plumbing: every register_rest_route MUST supply one that enforces authentication and the right capability, and it must never be left off or set to __return_true on a route that reads private data or mutates state.
  • Treat every REST argument as untrusted — Sanitize and validate every request argument through the args schema and never pass raw parameters into SQL, the filesystem, shell, or output; escape on output as well.

Testing

  • Activation and decision behavior are verified in eval/scenarios/wordpress/rest-api.yaml.

Additional resources

  • references/anti-patterns.md
  • references/core.md
  • references/decision-engine.md
  • references/examples.md
  • references/pitfalls.md
  • references/recipes.md
  • references/version-differences.md

Knowledge graph

Prerequisites: wordpress/plugin-structure
Related: security/jwt-php

<!-- graph-fragment: graph/fragments/wordpress/rest-api.yaml -->

Skill contract

Produces: wp.rest-route
Requires: wp.plugin-scaffolded
Risk: medium · Confidence: 0.9

<!-- contract-fragment: contracts/fragments/wordpress/rest-api.yaml -->

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.