Subtle diagonal pinstripe pattern in light grey and cream, giving an impression of printed editorial paper

How to create a custom REST API endpoint in WordPress with authentication

WordPress includes a flexible REST API for exposing posts, products, orders, settings, and custom data as JSON. A custom route lets your theme, plugin, mobile application, or external service request precisely the information it needs instead of relying on a broad collection of default endpoints.

Authentication is the part that determines whether the endpoint is safe to use. A well-designed route checks the caller’s identity, verifies their capability, validates every parameter, and returns only the data that account is allowed to see. The examples below use modern WordPress APIs and are suitable for developers maintaining sites in Australia, from a small Brisbane business site to a WooCommerce store serving customers across Sydney and Melbourne.

Authentication method Best use Main requirement Important limitation
Cookie plus REST nonce Logged-in WordPress users in the dashboard X-WP-Nonce header Intended for browser sessions, not public mobile apps
Application Passwords Server-to-server integrations and admin tools HTTPS and a dedicated WordPress user Protect credentials like passwords
OAuth or JWT plugin Mobile apps and external clients A reputable, maintained authentication solution Adds configuration and token management
Public endpoint with capability checks Truly public, non-sensitive data Strict validation and rate limiting Never expose private records or write operations

Decide what the endpoint should expose

Start by defining the resource, HTTP method, response shape, and access rule. A read-only route might return store locations, while a protected route could return a customer’s orders or update a booking. Use GET for retrieval, POST for creation, PUT or PATCH for changes, and DELETE for removal.

Give the route a versioned namespace such as acme/v1. The namespace prevents collisions with WordPress core, another plugin, or a future rewrite of your code. A URL such as /wp-json/acme/v1/orders is easier to maintain than a vague route like /wp-json/data.

The permission rule should describe a capability rather than a hard-coded user ID. For example, manage_woocommerce may be suitable for an internal order report, while a custom capability such as read_customer_orders provides more precise control. A café in Perth might allow staff to read today’s bookings without granting them permission to change payment settings.

For data-heavy resources, establish pagination before writing the callback. WordPress’s page and per_page conventions are familiar to API consumers, and careful query ordering prevents records from appearing twice or moving between pages. This related WP_Query pagination guide covers the ordering issue in greater detail.

Register a namespaced route

Register routes on the rest_api_init hook. The permission_callback is required in current WordPress versions; omitting it can produce warnings and may accidentally create an endpoint with unclear access behaviour.

add_action( 'rest_api_init', function () {
    register_rest_route(
        'acme/v1',
        '/orders',
        array(
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => 'acme_get_orders',
            'permission_callback' => 'acme_can_read_orders',
            'args'                => array(
                'page' => array(
                    'default'           => 1,
                    'sanitize_callback' => 'absint',
                    'validate_callback' => function ( $value ) {
                        return (int) $value > 0;
                    },
                ),
            ),
        )
    );
} );

The route is now available at /wp-json/acme/v1/orders, but it is protected by the permission callback. WordPress runs its authentication mechanisms before this callback, so the callback can use functions such as is_user_logged_in(), get_current_user_id(), and current_user_can().

function acme_can_read_orders( WP_REST_Request $request ) {
    if ( ! is_user_logged_in() ) {
        return new WP_Error(
            'rest_not_logged_in',
            'Authentication is required.',
            array( 'status' => 401 )
        );
    }

    if ( ! current_user_can( 'manage_woocommerce' ) ) {
        return new WP_Error(
            'rest_forbidden',
            'You do not have permission to view these orders.',
            array( 'status' => 403 )
        );
    }

    return true;
}

Returning true allows the callback to run. Returning false creates a generic forbidden response, while a WP_Error lets you define a useful error code and HTTP status. Use 401 when no valid authentication is present and 403 when the user is authenticated but lacks the required capability.

Build a safe callback and response

The callback receives a WP_REST_Request object. Read values with $request->get_param(), $request->get_param() for general parameters, or more specific methods such as $request->get_json_params() for a JSON body. Validate values before using them in queries or business logic.

function acme_get_orders( WP_REST_Request $request ) {
    $page     = max( 1, (int) $request->get_param( 'page' ) );
    $per_page = 20;

    $orders = wc_get_orders(
        array(
            'limit'   => $per_page,
            'paged'   => $page,
            'return'  => 'objects',
            'orderby' => 'date',
            'order'   => 'DESC',
        )
    );

    $data = array();

    foreach ( $orders as $order ) {
        $data[] = array(
            'id'     => $order->get_id(),
            'status' => $order->get_status(),
            'total'  => $order->get_total(),
            'date'   => $order->get_date_created()
                ? $order->get_date_created()->date( DATE_ATOM )
                : null,
        );
    }

    return rest_ensure_response(
        array(
            'page'    => $page,
            'per_page'=> $per_page,
            'results' => $data,
        )
    );
}

Do not return complete database rows, user objects, payment details, or internal metadata by default. Construct a deliberate response with a stable schema. For Australian WooCommerce sites, consider whether the consumer needs prices in Australian dollars, GST-inclusive totals, or a localised date representation. ISO 8601 dates are usually safest for applications, even when the storefront displays dates in an Australian format.

When a route uses WP_Query, WC_Order_Query, or custom SQL, keep the query narrowly scoped. Use prepared statements for SQL and whitelist sortable fields rather than accepting arbitrary column names. A request such as ?orderby=customer_email should be checked against a fixed list before it affects a query.

Authenticate server-to-server requests

Application Passwords are a practical built-in option for a trusted integration. Create a dedicated WordPress user with the smallest useful role, then generate an application password under that user’s profile. The client sends HTTP Basic Authentication over HTTPS using the WordPress username and application password.

A command-line request can look like this:

curl --user "api-reader:xxxx xxxx xxxx xxxx xxxx xxxx" \
  "https://example.com/wp-json/acme/v1/orders?page=1"

The application password is separate from the user’s normal login password and can be revoked without changing the account password. Create one per integration so that a compromised credential can be disabled without taking down every connected service. Never commit it to a Git repository, JavaScript bundle, public Docker image, or shared screenshot.

HTTPS is essential because Basic Authentication encodes credentials rather than encrypting them itself. Check that the site redirects HTTP to HTTPS and that the certificate is valid. This matters particularly for Australian businesses using managed hosting in Sydney or Melbourne, where staging and production environments may have different domain names and SSL settings.

A mobile application usually needs a token-based design rather than an application password embedded in the app. JWT and OAuth plugins can provide that layer, but inspect their maintenance history, token expiry behaviour, refresh flow, and compatibility with your WordPress version. Do not write a custom token system unless you understand secure token storage, revocation, replay protection, and key rotation.

Use a REST nonce for browser sessions

Cookie authentication is appropriate when JavaScript running inside a logged-in WordPress dashboard calls your endpoint. WordPress uses the X-WP-Nonce header to protect against cross-site request forgery. Generate the nonce in PHP and pass it to the script with wp_localize_script() or wp_add_inline_script().

wp_enqueue_script(
    'acme-admin',
    plugin_dir_url( __FILE__ ) . 'admin.js',
    array(),
    '1.0.0',
    true
);

wp_add_inline_script(
    'acme-admin',
    'window.acmeApi = ' . wp_json_encode(
        array(
            'root'  => esc_url_raw( rest_url( 'acme/v1/' ) ),
            'nonce' => wp_create_nonce( 'wp_rest' ),
        )
    ) . ';',
    'before'
);

The browser can then include the nonce in a fetch() request:

fetch(acmeApi.root + 'orders?page=1', {
    method: 'GET',
    headers: {
        'X-WP-Nonce': acmeApi.nonce
    },
    credentials: 'same-origin'
})
.then(response => response.json())
.then(data => console.log(data));

A nonce confirms that the request is associated with the user’s WordPress session; it is not a replacement for authentication or a capability check. Keep permission_callback in place because a valid nonce does not automatically grant access to every route.

For write operations, include the nonce and verify the user’s capability before processing the body. Avoid accepting raw HTML, file paths, role names, or arbitrary option keys. If the endpoint is used by a public-facing form rather than a logged-in dashboard, use a separate anti-abuse strategy and do not assume a browser nonce makes anonymous submissions safe.

Validate errors and test the endpoint

A reliable endpoint returns predictable status codes and JSON errors. Use 400 for malformed input, 401 for missing or invalid authentication, 403 for insufficient permissions, 404 for a missing resource, and 500 only for an unexpected server-side failure. Avoid exposing stack traces, SQL statements, filesystem paths, or secret configuration values.

Test the route as an unauthenticated visitor, a logged-in user with the correct capability, a logged-in user without it, and an administrator. Also test invalid page numbers, oversized per_page values, missing records, expired credentials, and malformed JSON. WordPress debug logs can help during development, but sensitive request headers should never be written to logs.

For a route that accepts a JSON body, declare arguments with validate_callback and sanitize_callback. Sanitisation changes a value into a safer form; validation decides whether the value is acceptable. An email address, post ID, date range, and enum status each need different validation rules.

Rate limiting is useful even when authentication is required. A compromised account or leaked application password can otherwise generate a large number of requests. Limits can be applied at the reverse proxy, web server, hosting platform, or application layer. Review access logs using Australian Eastern Standard Time or daylight-saving time consistently so an incident spanning Sydney, Brisbane, and Perth is easier to reconstruct.

Protect personal and commercial data

Authentication proves who is calling; authorisation decides what that caller can see. A support employee might need order numbers and fulfilment status but not a customer’s full address. A store manager may need sales totals while an external shipping service needs only delivery details for one order.

Filter records by the authenticated user when the endpoint is customer-facing. Do not use a request parameter such as ?user_id=18 as the sole protection for private data. Obtain the current user with get_current_user_id() and apply that identity to the query, or check a narrowly defined capability before allowing administrative access.

Consider Australian privacy obligations when designing responses. Personal information can include names, email addresses, phone numbers, delivery addresses, IP addresses, and purchase history. The Australian Privacy Principles and guidance from the Office of the Australian Information Commissioner are relevant considerations for many organisations, especially when data is sent to overseas hosting or analytics services.

External integrations deserve the same scrutiny as your own route. If an endpoint imports catalogue or reward information, validate its response and limit the fields forwarded elsewhere; even a third-party coin shop should never receive WordPress credentials, customer records, or an unrestricted admin token. Use separate service credentials and document exactly what data leaves the site.

Version, document, and maintain the API

Once another system consumes /acme/v1/orders, changing field names or meanings can break that system without warning. Add new fields in a backward-compatible way where possible. For breaking changes, register /acme/v2/orders and keep the first version available for a planned migration period.

Document the endpoint’s URL, methods, authentication method, required capabilities, parameters, response examples, error codes, and rate limits. Include whether monetary values are decimal strings or integers in cents, whether dates are UTC, and whether pagination is zero-based or one-based. Clear documentation saves time when a developer in Adelaide has to troubleshoot a production integration created by a contractor in Canberra.

Keep the route in a plugin rather than a theme when it represents business functionality. Add automated tests for permission checks, parameter validation, response fields, and ownership filtering. Review WordPress, WooCommerce, PHP, and authentication plugin updates before deploying them to production, and revoke unused application passwords during regular security maintenance.

A custom REST API endpoint is secure when each layer has a defined job: the authentication mechanism identifies the caller, the permission callback checks capability, the callback validates and limits data, and the server protects transport and credentials. That structure makes the endpoint easier to test, safer to extend, and more dependable for WordPress applications operating across Australia.