
TL;DR: WordPress REST API custom endpoints let you expose any server-side data or action to external apps using register_rest_route(). Build your route in a plugin or theme file, secure it with a permission callback, and your endpoint is immediately testable with Postman or any HTTP client.
Last Updated: October 2026. Code tested against WordPress 6.7 running PHP 8.3.
Why Custom REST API Endpoints Matter in 2026
Try GigaPress AI Free →
WordPress ships with a powerful built-in REST API that covers posts, pages, users, taxonomies, and most core objects. But the moment you need to expose custom business logic, aggregate data from multiple tables, or serve a mobile app without leaking your WordPress internals, the built-in routes fall short. That is where WordPress REST API custom endpoints close the gap.
Custom endpoints give you full control: you decide the route, the HTTP methods it accepts, what data it returns, and who can call it. Whether you are building a headless front-end in Next.js, powering a React Native mobile app, or wiring up a third-party CRM, a well-designed custom route is the cleanest solution available inside WordPress.
This tutorial walks you through everything from namespace conventions to live testing in Postman, including a complete, copy-ready working example.
For a broad overview of how the REST API works before diving into custom routes, start with the WordPress REST API: A Beginner’s Guide (2026). For enterprise-scale architecture patterns, see the WordPress REST API Integration Guide for Enterprise Developers 2026.
Understanding register_rest_route()
Every custom endpoint in WordPress is registered through register_rest_route(). This function is the single entry point for telling WordPress: “When a request arrives for this path, run this callback.” It accepts three arguments: a namespace string, a route pattern, and an array of arguments.
register_rest_route(
string $namespace,
string $route,
array $args = [],
bool $override = false
);
You must call this function inside the rest_api_init action hook. Calling it any earlier or later means WordPress either has not set up the API infrastructure yet or has already processed the request.
add_action( 'rest_api_init', 'myplugin_register_routes' );
function myplugin_register_routes() {
register_rest_route( 'myplugin/v1', '/data', [
'methods' => 'GET',
'callback' => 'myplugin_get_data',
'permission_callback' => '__return_true',
] );
}
Namespace Conventions
The namespace is a vendor prefix combined with a version number, separated by a forward slash. WordPress core uses wp/v2. For your own code, the convention is plugin-slug/v1, for example myplugin/v1 or acme-crm/v2.
Versioning matters. If you ship a breaking change to your endpoint schema, you bump to v2 while keeping v1 running. External consumers can migrate on their own timeline rather than breaking the moment you deploy.
The full URL for the example above resolves to:
https://yoursite.com/wp-json/myplugin/v1/data
Step-by-Step: Build a Complete Custom Endpoint
The example below creates a read-only endpoint that returns a list of published products stored in a custom post type called product. It covers route registration, the callback, permission handling, and proper response formatting.
Step 1: Register the Route
Add this to your plugin file or to a custom plugin you create specifically for API routes. Avoid placing API logic in functions.php — themes change, and your API should not disappear when you switch themes.
<?php
/**
* Plugin Name: My Products API
* Description: Exposes product data via the WordPress REST API.
* Version: 1.0.0
*/
add_action( 'rest_api_init', 'myproducts_register_routes' );
function myproducts_register_routes() {
// GET all products
register_rest_route( 'myproducts/v1', '/products', [
'methods' => WP_REST_Server::READABLE, // GET
'callback' => 'myproducts_get_products',
'permission_callback' => '__return_true',
'args' => [
'per_page' => [
'default' => 10,
'sanitize_callback' => 'absint',
],
],
] );
// GET single product by ID
register_rest_route( 'myproducts/v1', '/products/(?P<id>d+)', [
'methods' => WP_REST_Server::READABLE,
'callback' => 'myproducts_get_single_product',
'permission_callback' => '__return_true',
'args' => [
'id' => [
'validate_callback' => function( $param ) {
return is_numeric( $param );
},
],
],
] );
// POST — create a product (authenticated)
register_rest_route( 'myproducts/v1', '/products', [
'methods' => WP_REST_Server::CREATABLE, // POST
'callback' => 'myproducts_create_product',
'permission_callback' => 'myproducts_is_admin',
'args' => [
'title' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
'price' => [
'required' => true,
'sanitize_callback' => 'floatval',
],
],
] );
}
Notice the use of WP_REST_Server constants (READABLE, CREATABLE, EDITABLE, DELETABLE, ALLMETHODS) instead of raw HTTP method strings. This is the recommended approach per the WordPress Developer Resources.
Step 2: Write the Callback Functions
Each callback receives a WP_REST_Request object and must return either a WP_REST_Response, a WP_Error, or any value that WordPress can JSON-encode. Returning raw arrays is valid but using rest_ensure_response() is safer because it guarantees a properly typed response object.
function myproducts_get_products( WP_REST_Request $request ) {
$per_page = $request->get_param( 'per_page' );
$query = new WP_Query( [
'post_type' => 'product',
'posts_per_page' => $per_page,
'post_status' => 'publish',
] );
if ( ! $query->have_posts() ) {
return new WP_Error(
'no_products',
'No products found.',
[ 'status' => 404 ]
);
}
$products = [];
foreach ( $query->posts as $post ) {
$products[] = [
'id' => $post->ID,
'title' => $post->post_title,
'slug' => $post->post_name,
'price' => get_post_meta( $post->ID, '_price', true ),
'link' => get_permalink( $post->ID ),
];
}
return rest_ensure_response( $products );
}
function myproducts_get_single_product( WP_REST_Request $request ) {
$id = $request->get_param( 'id' );
$post = get_post( $id );
if ( ! $post || $post->post_type !== 'product' || $post->post_status !== 'publish' ) {
return new WP_Error(
'product_not_found',
'Product not found.',
[ 'status' => 404 ]
);
}
$data = [
'id' => $post->ID,
'title' => $post->post_title,
'slug' => $post->post_name,
'price' => get_post_meta( $post->ID, '_price', true ),
'link' => get_permalink( $post->ID ),
];
return rest_ensure_response( $data );
}
function myproducts_create_product( WP_REST_Request $request ) {
$title = $request->get_param( 'title' );
$price = $request->get_param( 'price' );
$post_id = wp_insert_post( [
'post_title' => $title,
'post_type' => 'product',
'post_status' => 'publish',
] );
if ( is_wp_error( $post_id ) ) {
return $post_id;
}
update_post_meta( $post_id, '_price', $price );
$response = rest_ensure_response( [
'id' => $post_id,
'title' => $title,
'price' => $price,
] );
$response->set_status( 201 );
return $response;
}
Step 3: Implement the Permission Callback
The permission callback is the gatekeeper for your endpoint. It runs before the main callback. If it returns false or a WP_Error, WordPress sends a 401 or 403 response and never executes your callback. This is where you check authentication and authorization.
Never use __return_true on a write endpoint. That constant makes the route publicly accessible with no authentication. It is appropriate only for read-only public data.
// Restrict to logged-in administrators only
function myproducts_is_admin( WP_REST_Request $request ) {
if ( ! is_user_logged_in() ) {
return new WP_Error(
'rest_not_logged_in',
'You must be logged in to create products.',
[ 'status' => 401 ]
);
}
if ( ! current_user_can( 'manage_options' ) ) {
return new WP_Error(
'rest_forbidden',
'You do not have permission to create products.',
[ 'status' => 403 ]
);
}
return true;
}
For API key authentication rather than cookie-based auth, you would read a custom header, validate it against a stored hash, and return true or a WP_Error accordingly. Application Passwords (built into WordPress since version 5.6) are a cleaner alternative for machine-to-machine calls without a full OAuth implementation.
Request and Response Handling
The WP_REST_Request class surfaces everything about the incoming request through a clean interface. Understanding the key methods saves debugging time.
| Method | What It Returns | When to Use |
|---|---|---|
get_param( 'key' ) |
Single parameter (URL, query, or body) | Most common case |
get_params() |
All merged parameters as an array | Bulk processing |
get_json_params() |
Decoded JSON request body | POST/PUT with JSON body |
get_header( 'name' ) |
A specific request header value | API key authentication |
get_method() |
HTTP method string (GET, POST, etc.) | Multi-method routes |
get_url_params() |
Regex-captured URL segments | Route wildcards like (?P<id>d+) |
On the response side, set custom HTTP status codes using $response->set_status( 201 ), add headers with $response->header( 'X-Custom', 'value' ), and add pagination headers with $response->header( 'X-WP-Total', $total ) to match the pattern WordPress core uses on its own collection endpoints.
Argument Validation and Sanitization
The args array in your route registration is not just documentation. WordPress processes it before your callback runs, which means you get free input validation without any manual checks inside the callback itself.
'args' => [
'status' => [
'required' => false,
'default' => 'publish',
'enum' => [ 'publish', 'draft', 'pending' ],
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => function( $value ) {
return in_array( $value, [ 'publish', 'draft', 'pending' ], true );
},
'description' => 'Filter products by post status.',
],
],
If a required argument is missing or a validate_callback returns false, WordPress automatically returns a 400 Bad Request error before your callback is ever called. Use sanitize_callback to clean the data after validation passes.
Testing WordPress REST API Custom Endpoints with Postman
Postman (postman.com) is the most widely used tool for testing REST endpoints during development. Here is how to configure it for WordPress custom endpoints.
Test a Public GET Endpoint
1. Open Postman and create a new request.
2. Set the method to GET.
3. Enter your endpoint URL, for example https://yoursite.com/wp-json/myproducts/v1/products?per_page=5.
4. Click Send. You should receive a 200 response with a JSON array.
Test an Authenticated POST Endpoint Using Application Passwords
1. In your WordPress dashboard, go to Users, then your user profile, and scroll to Application Passwords. Generate a new password and copy it.
2. In Postman, go to the Authorization tab. Select Basic Auth. Enter your WordPress username and the generated application password (with spaces removed).
3. Set the method to POST and the URL to https://yoursite.com/wp-json/myproducts/v1/products.
4. In the Body tab, select raw and JSON, then add:
{
"title": "Widget Pro",
"price": 49.99
}
5. Click Send. A successful response returns a 201 status and the new product’s data.
Debugging Common Issues
If your endpoint returns a 404, the most common causes are: pretty permalinks not enabled (go to Settings, then Permalinks, and save once), a typo in the namespace or route string, or the registration hook not firing. If you get a 401 or 403, the permission callback is blocking access, often because authentication credentials were not sent or are invalid. For a full breakdown of REST API error codes, see the WordPress REST API Troubleshooting Guide 2026.
Common Use Cases for Custom Endpoints
Headless WordPress Front-Ends
When you decouple your WordPress back-end from the presentation layer, a JavaScript framework (Next.js, Nuxt, SvelteKit) fetches data from your REST API and renders it on its own. Custom endpoints let you shape the payload precisely for what the front-end needs, rather than forcing it to parse the verbose default post schema. A single custom route can aggregate post data, ACF fields, menu items, and taxonomy terms in one request, cutting round trips and load time significantly.
For a thorough look at headless architecture on WordPress, see WordPress REST API vs GraphQL for Headless Builds.
Mobile Apps
iOS and Android apps consume data over HTTP. Rather than exposing the full WordPress REST API surface to a mobile client, a custom endpoint returns only the fields the app needs, strips HTML markup the app cannot render, and handles authentication in a way suited to mobile token flows. This also lets you version the mobile API independently of your web API, so a legacy app version keeps working while you iterate.
Third-Party Integrations
CRMs, marketing automation platforms, payment processors, and ERP systems often support outgoing webhooks and inbound REST calls. A custom WordPress endpoint can receive a Stripe webhook, a HubSpot contact sync, or a Zapier trigger and write the data directly into WordPress custom post types or meta fields. This keeps data inside WordPress without requiring a separate middleware layer.
Internal Admin Tooling
Internal Admin Tooling
Custom Gutenberg blocks and React-powered admin pages use apiFetch, WordPress’s built-in wrapper around the Fetch API, to communicate with custom endpoints. If you are building a block that displays live inventory counts or a dashboard widget that pulls from an external database, a registered REST route is the correct data transport.
Security Best Practices for Custom Endpoints
Every public endpoint is a potential attack surface. These practices reduce risk without adding excessive complexity.
Always sanitize input and escape output. Use the sanitize_callback in your args definition and escape any data you insert into the database with $wpdb->prepare() or the standard WordPress data functions.
Use nonces for cookie-authenticated requests. When a logged-in user’s browser calls your endpoint (for example, from a Gutenberg block), WordPress checks the X-WP-Nonce header automatically. Generate nonces with wp_create_nonce( 'wp_rest' ) and pass them in the nonce key of the localized script data.
Rate-limit sensitive endpoints. WordPress does not rate-limit REST routes out of the box. For write endpoints or endpoints that trigger external API calls, add rate-limiting at the server level (nginx limit_req, Cloudflare rate limiting) or use a plugin that hooks into rest_pre_dispatch.
Do not expose internal data in error messages. Return generic error messages to unauthenticated users, not stack traces or database schema details. Pass only a readable string and an HTTP status code in your WP_Error data array.
Registering Multiple Methods on One Route
You can register GET and POST on the same route path by passing an array of argument arrays. This is the standard pattern for a collection endpoint that supports both reading and creating.
register_rest_route( 'myproducts/v1', '/products', [
[
'methods' => WP_REST_Server::READABLE,
'callback' => 'myproducts_get_products',
'permission_callback' => '__return_true',
],
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => 'myproducts_create_product',
'permission_callback' => 'myproducts_is_admin',
],
] );
Each method gets its own callback and permission callback. WordPress routes the request to the correct pair based on the HTTP method of the incoming request.
Supplementary Reading
The official WordPress Developer Resources site documents every class, hook, and function referenced in this tutorial. Cross-reference it whenever you need to verify argument names or class method signatures.
If your project uses custom post types as the data source for your endpoints, the WordPress Custom Post Types Explained: A Beginner’s Guide (2026) covers everything you need to set those up correctly first.
For teams evaluating whether REST or GraphQL is the right query layer for a headless build, the REST API vs GraphQL comparison on this site provides a side-by-side analysis.
Want a Pro WordPress Site in Minutes?
GigaPress AI builds you a full WordPress site in about 15 minutes — AI handles layout, styling, content, and images. Free to design, only pay when you’re ready to go live. If you are building a headless front-end or custom API integration, starting with a well-structured WordPress back-end from GigaPress AI means your REST API endpoints have clean data to serve from day one.
Frequently Asked Questions
What is the difference between a custom endpoint and the default WordPress REST API routes?
Default WordPress REST API routes expose core objects: posts, pages, users, taxonomies, and settings. Custom endpoints, registered with register_rest_route(), expose whatever data or logic you define. They are the right choice when you need to aggregate data from multiple sources, run custom queries, trigger server-side actions, or expose data from custom post types and plugin-specific tables that the default routes do not cover.
Can I use custom REST endpoints with the Gutenberg block editor?
Yes. Gutenberg uses @wordpress/api-fetch, which is a thin wrapper around the browser Fetch API that automatically handles nonces and the REST API base URL. In your block’s JavaScript, call apiFetch({ path: '/myproducts/v1/products' }) to fetch data from your custom route. For write operations from a block, pass a method and data option. Nonce verification happens automatically as long as the nonce is present in the page.
How do I authenticate REST API requests from a mobile app or external service?
Application Passwords (built into WordPress 5.6 and later) are the simplest option for machine-to-machine authentication. Generate a password per application in the user profile screen, then send requests using HTTP Basic Auth with the WordPress username and the application password. For public-facing apps where users log in with their own accounts, JWT authentication via a plugin such as JWT Authentication for WP REST API is a common pattern. Cookie-based authentication with nonces works only for requests originating from the same WordPress installation.
Why does my custom endpoint return a 404 error even though I registered it?
The most common cause is that WordPress pretty permalinks are not active. Go to Settings, then Permalinks, and click Save Changes without altering anything. This flushes the rewrite rules and makes the REST API URL structure work. A second common cause is registering the route outside the rest_api_init hook. Make sure your add_action( 'rest_api_init', ... ) call is in code that loads on every request, not only on admin pages. For a full list of causes and fixes, see the WordPress REST API Troubleshooting Guide.
Should I put custom endpoint code in a plugin or in functions.php?
Always use a plugin. If the code lives in functions.php and you switch or update the active theme, your endpoints disappear and any external app consuming them breaks immediately. A dedicated plugin, or a must-use plugin placed in wp-content/mu-plugins/, loads regardless of the active theme and is the correct long-term home for REST API route registrations.





