October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run ScanOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk10 min

How to Create a Custom REST API Endpoint in WordPress (Secure, Versioned Guide)

A practical, security-first guide to building custom WordPress REST API endpoints with register_rest_route(), capability checks, validation, JSON handling, errors, testing, and versioning.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Register a custom route on the rest_api_init hook with register_rest_route(). Give it a unique versioned namespace such as myplugin/v1, a URL pattern, an HTTP method, a callback, and a permission_callback. Define every parameter’s type, validation, and sanitization, then return data or a structured WP_Error. For a growing API, move the code into a WP_REST_Controller and describe the contract with JSON Schema.

This guide builds a working endpoint, explains authorization and input handling, and shows when a small function should become a controller-based API.

As an Amazon Associate I earn from qualifying purchases.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What a WordPress REST route actually contains

A REST route is a URL pattern, while an endpoint is the behavior attached to that pattern for a particular HTTP method. One route can therefore have separate read, create, update, and delete endpoints. WordPress exposes route registration through the REST API extension guide and the register_rest_route() reference.

A complete endpoint definition answers six questions:

  • Namespace: Who owns the route and which contract version is it?
  • Path: Which URL pattern identifies the resource?
  • Method: Is the operation read-only or does it change data?
  • Callback: What code performs the operation?
  • Permission callback: Who may invoke it?
  • Arguments: Which parameters are accepted, and how are they typed, validated, and sanitized?

A request such as GET /wp-json/myplugin/v1/books/42 combines the standard REST prefix, your namespace and version, and a route containing the book ID.

Plan the contract before writing PHP

Choose a namespace and version

Use a namespace that is unique to your plugin or project and include a version: vendor/v1. Keep the version stable for compatible changes; introduce v2 when you must make a breaking change, such as removing a field or changing its type. A namespace also reduces collisions with WordPress core and other plugins.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose a resource-shaped path

Use nouns for resources and put identifiers in the path. For example, /books can represent a collection and /books/(?P<id>d+) a single numeric book. The regular expression restricts the path before your callback runs. Query parameters such as ?page=2 are better for filtering or pagination than for identifying the resource itself.

Decide whether the operation is public

A public route is an explicit product decision, not a shortcut. It can expose data or trigger an action for anyone who can reach the URL. Protected routes should require authentication and check the capability that represents the requested operation on the requested resource.

Build a minimal read-only endpoint

Put route registration in a plugin or a namespaced project file that loads on every request. The registration itself belongs on rest_api_init; calling register_rest_route() earlier is outside the documented lifecycle. The following example reads a post of the custom book type by ID and requires the current user to have the read capability.

<?php

add_action( 'rest_api_init', function () {
    register_rest_route( 'myplugin/v1', '/books/(?P<id>\d+)', array(
        'methods'             => WP_REST_Server::READABLE,
        'callback'            => 'myplugin_get_book',
        'permission_callback' => function () {
            return current_user_can( 'read' );
        },
        'args'                => array(
            'id' => array(
                'required'          => true,
                'sanitize_callback' => 'absint',
                'validate_callback' => function ( $value ) {
                    return is_numeric( $value ) && (int) $value > 0;
                },
            ),
        ),
    ) );
} );

function myplugin_get_book( WP_REST_Request $request ) {
    $id   = (int) $request['id'];
    $book = get_post( $id );

    if ( ! $book ) {
        return new WP_Error(
            'book_not_found',
            'Book not found.',
            array( 'status' => 404 )
        );
    }

    return array(
        'id'    => $book->ID,
        'title' => get_the_title( $book ),
    );
}

WordPress converts the returned array to a JSON response. The callback receives a WP_REST_Request, so route variables, query values, headers, and body data are available through the request object. Returning a WP_Error gives clients a machine-readable code, a human-readable message, and the HTTP status supplied in the error data.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Register the route step by step

  1. Create the namespace. Replace myplugin with a unique vendor or plugin identifier and retain /v1 as the initial contract version.
  2. Attach registration to the lifecycle hook. Use add_action( 'rest_api_init', ... ); do not register the route at file load time.
  3. Define the path. Use a collection path such as /books or a constrained identifier path such as /books/(?P<id>d+).
  4. Select the method. WP_REST_Server::READABLE is suitable for a read operation. Use the corresponding REST server method constant for creating, editing, or deleting data.
  5. Write one focused callback. Read the request, load or change one resource, and return the representation, a WP_REST_Response, or a WP_Error.
  6. Add a permission callback. Every endpoint needs one. Use a capability check for protected work, or deliberately use __return_true only when the endpoint is intended to be public.
  7. Declare arguments. Mark required values, supply defaults where appropriate, and attach type, validation, and sanitization rules.
  8. Document the contract. Record methods, URL patterns, authentication expectations, parameters, success fields, and error codes for client developers.

The official guidance on methods, callbacks, permissions, and arguments is in Routes and Endpoints.

Validate and sanitize every input

Route variables, query parameters, and request-body values are untrusted. Validation answers “is this acceptable?” and should reject malformed input. Sanitization answers “how should an acceptable value be normalized?” For an integer ID, the example validates a positive numeric value and sanitizes it with absint. Do not use sanitization as a substitute for validation: converting an invalid value into zero can hide a bad request.

Useful argument metadata

Property Purpose Example
required Reject a request when the client omits the value. true
default Supply a value when omission is valid. 1
type Describe the expected JSON value type. string, integer, boolean, array
enum Restrict a value to a known set. array( 'draft', 'published' )
validate_callback Return true only when the value meets your rule. Positive integer or permitted status.
sanitize_callback Normalize an accepted value before business logic uses it. absint for an ID.

Keep validation close to the route definition so the API contract is visible. Check relationships and permissions in the callback as well—for example, an ID may be syntactically valid but refer to a resource the current user cannot access.

Read JSON request bodies correctly

For create and update operations, clients should send a JSON content type and a JSON body. In the callback, read the parsed associative array with $request->get_json_params(). Validate required fields, reject unknown or malformed values when your contract requires that, authorize the operation, then persist the change.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
function myplugin_create_book( WP_REST_Request $request ) {
    $params = $request->get_json_params();

    if ( ! is_array( $params ) || empty( $params['title'] ) ) {
        return new WP_Error(
            'missing_title',
            'A title is required.',
            array( 'status' => 400 )
        );
    }

    // Validate, sanitize, authorize, and save the book here.
}

Do not trust a field merely because it arrived in JSON. Normalize text for the storage API you use, constrain numeric and enum fields, and make sure the authenticated user can perform the requested write.

Authorization: login is not enough

permission_callback is authorization, not merely an authentication check. A logged-in user may still lack permission to edit a particular resource. Match the capability to the operation: reading private data, creating a record, editing someone else’s record, and deleting a record can all require different decisions.

Protected endpoint

'permission_callback' => function ( WP_REST_Request $request ) {
    $id = (int) $request['id'];

    return current_user_can( 'edit_post', $id );
},

For a collection endpoint, check the capability that governs the collection operation. For object-level access, load the target and check its capability. Return false or a WP_Error when access is denied; WordPress will produce the appropriate REST error response.

Deliberately public endpoint

WordPress documents __return_true for an endpoint intended to be public:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
'permission_callback' => '__return_true',

Use this only after deciding that the data or action may be reached without authentication. Public does not mean “safe by default”; rate limiting, response minimization, and careful input rules remain your responsibility.

Return useful responses and errors

A successful callback can return an array, a WP_REST_Response, or another supported value. Return the resource representation clients need rather than an internal database row. For writes, returning the created or changed resource lets a client update its local state without making an immediate second request.

When an operation cannot complete, return WP_Error with:

  • a stable machine-readable error code such as book_not_found;
  • a clear message suitable for logs and developers; and
  • an HTTP status in the error data, such as 400 for invalid input, 401 for missing authentication, 403 for insufficient permission, or 404 for a missing resource.

Keep error codes stable across a version so clients can branch on the code rather than scrape prose. Avoid leaking credentials, stack traces, SQL, or private fields in either success or error responses.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose an implementation style

Choice Best fit Trade-off
Small function route One simple, usually read-only endpoint. Fast to write, but shared behavior can become duplicated as the API grows.
WP_REST_Controller Several methods on one resource, shared permissions, or a growing API. More structure to implement, with clearer separation of registration and behavior.
Callback validation A few parameters with straightforward rules. Easy locally, but contracts are less reusable across endpoints.
JSON Schema Multiple endpoints, client generation, or a machine-readable contract. Requires designing and maintaining the schema.

When several routes share behavior, subclass WP_REST_Controller, implement route registration and permission methods, and derive endpoint arguments from a schema. The official Controller Classes guide explains that organization. The Schema guide describes how a schema structures data so applications can reason about their interactions with the API.

Signs that it is time for a controller

  • The same resource needs collection and item routes.
  • Read, create, update, and delete methods share permission logic.
  • Several responses expose the same fields and constraints.
  • Client teams need discoverable, reusable schema information.
  • Route registration, queries, and response formatting have become difficult to test in one function.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Test the route like a client

  1. Confirm registration. Visit the route under the site’s REST base URL, such as /wp-json/myplugin/v1/books/42, using the method you registered.
  2. Test an allowed request. Verify the status, JSON shape, data types, and headers expected by the client.
  3. Test authentication states. Try an anonymous request, an authenticated user without the capability, and an authorized user. The outcomes should match your policy.
  4. Test malformed input. Use a non-numeric ID, omit required fields, send an invalid enum, and provide an incorrect JSON type. Confirm that each is rejected rather than silently coerced.
  5. Test missing resources. Request a valid-looking ID that does not exist and check for your stable 404 error code.
  6. Test writes safely. Use a non-production environment, verify that only the intended record changes, and confirm the response represents the saved resource.
  7. Test compatibility. Keep clients on the versioned namespace and make additive changes there; reserve a new version for breaking changes.

Use an HTTP client that lets you select the method, send JSON, and inspect status codes. For authenticated routes, use the authentication mechanism already configured for your WordPress installation; never place real credentials in source code or logs.

Troubleshoot common failures

“The route is not found”

  • Check the exact namespace, version, and leading slash in the route pattern.
  • Ensure registration runs on rest_api_init and that the plugin containing it is active.
  • Confirm that the request uses the registered HTTP method; a route can exist for GET but not POST.
  • Check the site’s REST base URL and any server or security layer that blocks /wp-json/.

WordPress reports a missing permission callback

Add permission_callback to every endpoint. For a protected operation, return a capability decision; for a consciously public endpoint, use __return_true. Do not suppress the warning by leaving authorization undefined.

Valid-looking IDs still produce the wrong record

Constrain the route, validate the value, sanitize it before use, and verify that the loaded object is the expected post type or resource. A positive integer alone does not prove that the record belongs to your API.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

JSON fields are empty

Send the request with a JSON content type and read the body using get_json_params(). Log the received keys in a safe development environment, then validate the shape before accessing nested values.

Clients receive inconsistent errors

Centralize error codes and statuses, return WP_Error for expected failures, and keep success responses separate from error paths. A controller and shared schema become useful when multiple endpoints need the same contract.

Document and maintain the public contract

Documentation should state the full URL, accepted methods, authentication and capability expectations, path and query parameters, JSON body fields, success response shape, error codes, and versioning policy. Explain which fields are nullable, which are read-only, and whether omitted values receive defaults.

Keep route behavior focused: a read callback retrieves the resource; a write callback validates input, authorizes the action, saves the change, and returns the changed representation. Use namespaced functions or controller methods to avoid PHP global-name collisions. When a change would break an existing client, publish it under a new namespace version rather than silently changing the old response.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Practical decision checklist

  • Is the namespace unique and versioned?
  • Is registration attached to rest_api_init?
  • Does every endpoint declare a method, callback, and permission_callback?
  • Are route, query, and body values validated before business logic uses them?
  • Are values sanitized only when normalization is intended?
  • Does authorization match the operation and the specific resource?
  • Are success responses minimal, useful, and stable?
  • Do failures return a code, message, and HTTP status?
  • Are JSON bodies parsed with get_json_params() and checked for the expected shape?
  • Would a controller and schema reduce duplication for the next endpoint?
  • Have anonymous, unauthorized, malformed, missing-resource, and successful requests all been tested?
  • Is the method, payload, response, and version documented for client developers?

Frequently Asked Questions

Where should register_rest_route() be called?

Call it from a function attached to the rest_api_init hook, normally in your plugin or project code.

Can I make a custom endpoint public?

Yes. Set its permission_callback to __return_true only when unauthenticated access is an intentional product decision; otherwise enforce the capability required by the operation.

What is the difference between validation and sanitization?

Validation rejects values that do not meet the contract. Sanitization normalizes an accepted value before processing. Use both where appropriate, and do not let sanitization hide malformed input.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Leave a Reply

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

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Crashes, No Sound, or Screen Glitches?Free driver scan
Windows Errors? Fix Them Before They SpreadFree repair scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.