By Devport Team | Last updated: 2026-10-04 | 9 min read

Adding a Custom Endpoint to the WooCommerce REST API

Sometimes the built-in wc/v3 routes are not enough: you need an action such as "mark this order as synced", a combined response for a mobile app, or an extra field on the product response. WordPress gives you two tools for this – registering your own route, or extending WooCommerce's existing responses. This guide shows both, including how to make your route accept WooCommerce API keys.

Table of Contents

  1. Custom route or extended response?
  2. Registering a custom route
  3. Accepting WooCommerce API keys on your route
  4. Adding fields to existing responses
  5. Restricting access to REST endpoints
  6. Testing

Custom Route or Extended Response?

Use your own namespace (e.g. myshop/v1) instead of registering inside wc/v3, so WooCommerce updates cannot collide with your routes.

Registering a Custom Route

<?php
/**
 * Plugin Name: MyShop REST extensions
 */

add_action( 'rest_api_init', function () {
    register_rest_route( 'myshop/v1', '/orders/(?P<id>\d+)/sync', array(
        'methods'             => WP_REST_Server::CREATABLE, // POST
        'callback'            => 'myshop_mark_order_synced',
        'permission_callback' => function () {
            return current_user_can( 'edit_shop_orders' );
        },
        'args'                => array(
            'id'        => array(
                'validate_callback' => function ( $value ) {
                    return is_numeric( $value );
                },
            ),
            'reference' => array(
                'type'              => 'string',
                'required'          => true,
                'sanitize_callback' => 'sanitize_text_field',
            ),
        ),
    ) );
} );

function myshop_mark_order_synced( WP_REST_Request $request ) {
    $order = wc_get_order( (int) $request['id'] );
    if ( ! $order ) {
        return new WP_Error( 'myshop_order_not_found', 'Order not found.', array( 'status' => 404 ) );
    }

    $order->update_meta_data( '_myshop_erp_reference', $request['reference'] );
    $order->add_order_note( 'Synced to ERP: ' . $request['reference'] );
    $order->save();

    return rest_ensure_response( array(
        'id'        => $order->get_id(),
        'status'    => $order->get_status(),
        'reference' => $order->get_meta( '_myshop_erp_reference' ),
    ) );
}

Key points:

Accepting WooCommerce API Keys on Your Route

WooCommerce only checks consumer keys on requests whose route starts with wc/ or wc-. On a custom namespace, a request with -u ck_xxx:cs_xxx arrives unauthenticated and your permission_callback returns false (401). Tell WooCommerce to authenticate your namespace too:

add_filter( 'woocommerce_rest_is_request_to_rest_api', function ( $is_wc_request ) {
    if ( $is_wc_request || empty( $_SERVER['REQUEST_URI'] ) ) {
        return $is_wc_request;
    }
    $uri    = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
    $prefix = trailingslashit( rest_get_url_prefix() ); // usually "wp-json/"
    return false !== strpos( $uri, $prefix . 'myshop/' );
} );

With that filter, the key's Read/Write permission is enforced for your route as well (GET needs read, POST needs write), and the request runs as the key's user so current_user_can() works. Application Passwords work without any filter, because WordPress core handles them for every route.

Adding Fields to Existing Responses

WooCommerce passes every object through a woocommerce_rest_prepare_{type}_object filter before returning it. For orders the type is shop_order; for products it is product:

add_filter( 'woocommerce_rest_prepare_shop_order_object', function ( $response, $order, $request ) {
    $response->data['erp_reference'] = $order->get_meta( '_myshop_erp_reference' ) ?: null;
    return $response;
}, 10, 3 );

add_filter( 'woocommerce_rest_prepare_product_object', function ( $response, $product, $request ) {
    $response->data['warehouse_bin'] = $product->get_meta( '_warehouse_bin' ) ?: null;
    return $response;
}, 10, 3 );

If you also need the field in the schema and writable through the API, use register_rest_field() on the post type (for products) with get_callback, update_callback and schema. Order meta is also exposed in the standard meta_data array, unless the key starts with an underscore (protected meta).

Restricting Access to REST Endpoints

To stop anonymous access to a route, rely on permission_callback. To block whole namespaces for unauthenticated users (for example on a store that is only accessed by back-office tools), filter rest_authentication_errors:

add_filter( 'rest_authentication_errors', function ( $result ) {
    if ( ! empty( $result ) ) {
        return $result; // another auth method already decided
    }
    $route = $GLOBALS['wp']->query_vars['rest_route'] ?? '';
    if ( str_starts_with( $route, '/myshop/' ) && ! is_user_logged_in() ) {
        return new WP_Error( 'rest_forbidden', 'Authentication required.', array( 'status' => 401 ) );
    }
    return $result;
} );

Do not blanket-disable the REST API: the block editor, WooCommerce admin screens and the Store API used by block-based checkout all depend on it.

Testing

curl -X POST "https://example.com/wp-json/myshop/v1/orders/727/sync" \
  -u ck_xxx:cs_xxx -H "Content-Type: application/json" \
  -d '{ "reference": "ERP-10045" }'

Check the route is registered with GET /wp-json/myshop/v1, and remember that WooCommerce must be active before you call wc_get_order() – ship the code as a small plugin rather than in a theme's functions.php.

Related: authentication and key permissions, webhooks, and the WordPress handbook on custom endpoints.