rest_pre_serve_request filter) to intercept the data right before it’s sent and convert the JSON object into XML or CSV.

An API is an Application Programming Interface. REST, which stands for “Representational State Transfer,” is a set of concepts for modeling and accessing your application’s data as interrelated objects and collections.

API is a set of code that allows one system to interact (or “interface”) with another.

REST – Representational State Transfer, or REST, provides standards that web systems can use to interface with each other.

ResourceEndpoint ExampleAction
All Posts/wp/v2/postsGet a list of recent blog posts
Single Post/wp/v2/posts/123Get details for post with ID 123
All Users/wp/v2/usersList all registered users on the site
Media Library/wp/v2/mediaGet a list of uploaded images
Search/wp/v2/search?search=pizzaSearch the site for “pizza”

wp scaffold

  • wp scaffold plugin my-new-plugin: Creates a new plugin folder with all the necessary files (index.php, readme.txt, etc.).

  • wp scaffold child-theme my-child --parent_theme=twentytwentyfive: Instantly sets up a child theme.

  • wp scaffold post-type movie: Generates the PHP code needed to register a “Movie” custom post type.

  • wp scaffold block my-block: Creates the files (PHP, JS, CSS) needed to build a custom Gutenberg block.

# Basic plugin generation
wp scaffold plugin my-awesome-feature --plugin_name="My Awesome Feature" --plugin_description="Does something cool." --plugin_author="Your Name"

# Basic plugin generation
wp scaffold plugin my-awesome-feature --plugin_name="My Awesome Feature" --plugin_description="Does something cool." --plugin_author="Your Name"

# Generate code for a 'Movie' post type
# you can paste anywhere
wp scaffold post-type movie --label="Movies" --textdomain=my-plugin

# Creates a controller class for a 'movies' endpoint
wp scaffold _rest-api movies --plugin=my-awesome-feature

  • Routes & Endpoints – A route is a URI that can be mapped to different HTTP methods. The mapping of an individual HTTP method to a route is known as an endpoint.
  • Requests – A request made in WordPress is an instance of the WP_REST_Request class, which is used to store and retrieve information for the current request.
  • Responses – Responses are the data you get back from the API. The WP_REST_Response class provides a way to interact with the response data returned by endpoints. 
  • Schema – API Schema is a data structure of input and output data of each endpoint**.**
  • Controller Classes – It manages the registration of routes & endpoints, handling requests, utilizing schema, and generating API responses.

class MY_Daily_Message_Controller extends WP_REST_Controller {

    public function register_routes() {
        register_rest_route( 'my-plugin/v1', '/message', array(
            methods  => 'GET',
            callback => array( $this, 'get_item' ),
            permission_callback => '__return_true', // Publicly accessible
        ) );
    }

    public function get_item( $request ) {
        $messages = ["Code is poetry", "Keep it simple", "Debug with logic"];
        $data = array( 'message' => $messages[array_rand($messages)] );

        // Standardize the response
        return new WP_REST_Response( $data, 200 );
    }
}

// Initialization
add_action( 'rest_api_init', function () {
    $controller = new MY_Daily_Message_Controller();
    $controller->register_routes();
} );
  • To get data from remote
$response = wp_remote_get( 'https://api.example.com/data' );

if ( ! is_wp_error( $response ) ) {
    $body = wp_remote_retrieve_body( $response );
    $data = json_decode( $body );
}
  • To post data to remote
$response = wp_remote_post( 'https://api.example.com/submit', array(
    'body' => array(
        'name'  => 'Gemini',
        'email' => 'ai@example.com',
    ),
) );

You should use this instead of standard PHP curl because it handles different server environments automatically.

Always use is_wp_error() when working with wp_remote_* functions. If the site has no internet connection or the DNS fails, they won’t return an HTTP error code; they’ll return a WP_Error object that will crash your site if you try to treat it like an array.

function get_crypto_price() {
    $url = 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd';
    
    $response = wp_remote_get( $url );

    // 1. Check for connection/DNS errors
    if ( is_wp_error( $response ) ) {
        return 'Could not reach API';
    }

    // 2. Check for HTTP success (200 OK)
    $code = wp_remote_retrieve_response_code( $response );
    if ( $code !== 200 ) {
        return 'API error code: ' . $code;
    }

    // 3. Parse the data
    $body = wp_remote_retrieve_body( $response );
    $data = json_decode( $body, true );

    return '$' . $data['bitcoin']['usd'];
}
function send_lead_to_external_crm( $user_name, $user_email ) {
    $url  = 'https://example-crm.com/api/leads';
    
    $args = array(
        'method'      => 'POST',
        'timeout'     => 5,
        'redirection' => 5,
        'headers'     => array(
            'Authorization' => 'Bearer your-api-token',
            'Content-Type'  => 'application/json',
        ),
        'body'        => json_encode( array(
            'full_name' => $user_name,
            'email'     => $user_email,
            'source'    => 'WordPress Site'
        ) ),
    );

    $response = wp_remote_post( $url, $args );

    if ( is_wp_error( $response ) ) {
        error_log( $response->get_error_message() );
        return false;
    }

    return true;
}
FunctionPurpose
rest_url()Returns the API URL for the current blog/site.
get_rest_url()Used in Multisite environments to get the API URL for a specific blog ID.
add_action( 'rest_api_init', function () {
    register_rest_route( 'my-utility/v1', '/clear-cache', array(
        'methods'  => 'POST',
        'callback' => 'my_simple_cache_callback',
        'permission_callback' => function() { return current_user_can('manage_options'); },
    ) );
} );
class My_Books_Controller extends WP_REST_Controller {

    public function __construct() {
        $this->namespace = 'my-library/v1';
        $this->rest_base = 'books';
    }

    // 1. Register the actual URLs
    public function register_routes() {
        register_rest_route( $this->namespace, '/' . $this->rest_base, array(
            array(
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => array( $this, 'get_items' ),
                'permission_callback' => array( $this, 'get_items_permissions_check' ),
            ),
            array(
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => array( $this, 'create_item' ),
                'permission_callback' => array( $this, 'create_item_permissions_check' ),
            ),
        ) );
    }

    // 2. Permission Checks (Security first!)
    public function get_items_permissions_check( $request ) {
        return true; // Anyone can view books
    }

    public function create_item_permissions_check( $request ) {
        return current_user_can( 'edit_posts' ); // Only staff can add books
    }

    // 3. The Logic: Get all books
    public function get_items( $request ) {
        $books = get_posts( array( 'post_type' => 'book' ) );
        $data  = array();

        foreach ( $books as $book ) {
            $data[] = $this->prepare_item_for_response( $book, $request );
        }

        return rest_ensure_response( $data );
    }

    // 4. Format the output
    public function prepare_item_for_response( $item, $request ) {
        return array(
            'id'    => $item->ID,
            'title' => $item->post_title,
            'link'  => get_permalink( $item->ID ),
        );
    }
}
add_action( 'rest_api_init', function () {
    $books_controller = new My_Books_Controller();
    $books_controller->register_routes();

    $persons_controller = new My_Persons_Controller();
    $persons_controller->register_routes();
} );
  • To restrict the response to only those properties with this fields query:
/wp/v2/posts?_fields=author,id,excerpt,title,link

?_fields=meta.one-of-many-keys
  • To embed resources to prevent unncessary later calls
/wp/v2/posts?_embed=author,wp:term 

will only embed the post’s author and the lists of terms associated with the post.
POST /wp-json/wp/v2/posts/42 HTTP/1.1
Host: example.com
X-HTTP-Method-Override: DELETE

Similarly to _method, some servers, clients, and proxies do not support accessing the full response data. The API supports passing an _envelope parameter, which sends all response data in the body, including headers and status code.


  • ?page=: specify the page of results to return.
    • For example, /wp/v2/posts?page=2 is the second page of posts results
    • By retrieving /wp/v2/posts, then /wp/v2/posts?page=2, and so on, you may access every available post through the API, one page at a time.
  • ?per_page=: specify the number of records to return in one request, specified as an integer from 1 to 100.
    • For example, /wp/v2/posts?per_page=1 will return only the first post in the collection
  • ?offset=: specify an arbitrary offset at which to start retrieving posts
    • For example, /wp/v2/posts?offset=6 will use the default number of posts per page, but start at the 6th post in the collection
    • ?per_page=5&page=4 is equivalent to ?per_page=5&offset=15

To determine how many pages of data are available, the API returns two header fields with every paginated response:

  • X-WP-Total: the total number of records in the collection
  • X-WP-TotalPages: the total number of pages encompassing all available records

  • ?order=: control whether results are returned in ascending or descending order
    • Valid values are ?order=asc (for ascending order) and ?order=desc (for descending order).
    • All native collections are returned in descending order by default.
  • ?orderby=: control the field by which the collection is sorted
    • The valid values for orderby will vary depending on the queried resource; for the /wp/v2/posts collection, the valid values are “date,” “relevance,” “id,” “include,” “title,” and “slug”
    • See the REST API reference for the values supported by other collections
    • All collections with dated resources default to orderby=date

  • WP rest api uses cookie authentcation + nonces technique. using built in js api, auto handles this api for us. In manual ajax requests, nonces need to be passed.

  • application passwords

curl --user "USERNAME:PASSWORD" https://HOSTNAME/wp-json/wp/v2/users?context=edit

add_action( 'rest_api_init', function () {
    register_rest_route( 'rt/v1', '/celebs', array(
        'methods'  => 'GET',
        'callback' => 'get_rt_celebs_posts',
        'permission_callback' => '__return_true', // Public access
        'args'     => array(
            'page'     => array( 'default' => 1, 'sanitize_callback' => 'absint' ),
            'per_page' => array( 'default' => 10, 'sanitize_callback' => 'absint' ),
        ),
    ) );
} );

function get_rt_celebs_posts( $data ) {
    $args = array(
        'post_type'      => 'rt-celebs',
        'posts_per_page' => $data['per_page'],
        'paged'          => $data['page'],
    );

    $query = new WP_Query( $args );
    $posts = $query->get_posts();

    $response = array();
    foreach ( $posts as $post ) {
        $response[] = array(
            'id'    => $post->ID,
            'title' => $post->post_title,
            'link'  => get_permalink( $post->ID ),
        );
    }

    // Wrap in WP_REST_Response to include pagination headers
    $result = new WP_REST_Response( $response, 200 );
    $result->header( 'X-WP-Total', $query->found_posts );
    $result->header( 'X-WP-TotalPages', $query->max_num_pages );

    return $result;
}
  • Restricting request to one hosts/url
add_action( 'rest_api_init', function () {
    register_rest_route( 'rt/v1', '/celebs', array(
        'methods'  => 'GET',
        'callback' => 'get_rt_celebs_posts',
        'permission_callback' => '__return_true', // Public access
        'args'     => array(
            'page'     => array( 'default' => 1, 'sanitize_callback' => 'absint' ),
            'per_page' => array( 'default' => 10, 'sanitize_callback' => 'absint' ),
        ),
    ) );
} );

function get_rt_celebs_posts( $data ) {
    $args = array(
        'post_type'      => 'rt-celebs',
        'posts_per_page' => $data['per_page'],
        'paged'          => $data['page'],
    );

    $query = new WP_Query( $args );
    $posts = $query->get_posts();

    $response = array();
    foreach ( $posts as $post ) {
        $response[] = array(
            'id'    => $post->ID,
            'title' => $post->post_title,
            'link'  => get_permalink( $post->ID ),
        );
    }

    // Wrap in WP_REST_Response to include pagination headers
    $result = new WP_REST_Response( $response, 200 );
    $result->header( 'X-WP-Total', $query->found_posts );
    $result->header( 'X-WP-TotalPages', $query->max_num_pages );

    return $result;
}
  • CORS
add_filter( 'rest_pre_serve_request', function( $value ) {
    header( 'Access-Control-Allow-Origin: https://your-frontend-app.com' );
    header( 'Access-Control-Allow-Methods: GET, POST, OPTIONS' );
    header( 'Access-Control-Allow-Credentials: true' );
    return $value;
});

Execution

rest_api_loaded() checks if it is rest route, if yest, it defines constant REST_REQUEST to true

rest_Get_server() initialises the server init action rest_api_init is executed here returns object wp_rest_server

serve_requst() function called

  • first checks current user is logged in or not

  • adds header

  • adds cors

  • CREATE WP REST REQUEST OBJECT from $_SERVER(‘REQUEST_METHOD’) $_GET $_POST variables

  • check_authentication() function

dispatch() function

rest_pere_dispatch filter is used to check sata bedofew dispatch

match routes and call it.


Execution - response

‘rest_request_before_callbacks’ filter is excuted befoer any clalbakcs if no errors, callbacks are executed

then

‘rest_request_after_callbacks’ executed

_envelope is chcked

json data is returned

die() is called


add_action('rest_api_init', function () {
    register_rest_route('my-namespace/v1', '/submit', [
        'methods'  => 'POST',
        'callback' => 'handle_my_post_request',
        'permission_callback' => '__return_true', // Ensure you add real auth here
    ]);
});

function handle_my_post_request(WP_REST_Request $request) {
    // Get the raw body from the request
    $body = $request->get_body();

    // Verify if it is valid JSON
    json_decode($body);
    
    if (json_last_error() !== JSON_ERROR_NONE) {
        return new WP_Error(
            'rest_invalid_json', 
            'The body provided is not a valid JSON string.', 
            ['status' => 405] // Your requested status code
        );
    }

    // Continue with your logic...
    return rest_ensure_response(['message' => 'Success!']);
}
  • Can you change the REST base from wp-json? Yes, via the rest_url_prefix filter:
add_filter( 'rest_url_prefix', function() {
    return 'api';
});
  • Do internal rest calls
$request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
$response = rest_do_request( $request );

How JSONP Works

Instead of making an AJAX request, JSONP:

  1. Creates a <script> tag dynamically.

  2. Points its src to the external API.

  3. Passes a callback function name in the URL.

  4. The server wraps the JSON data inside that callback function.

  5. The browser executes it as JavaScript.

to enable jsonp

// Warning: This opens up potential security vulnerabilities (Rosetta attacks)
add_filter( 'rest_jsonp_enabled', '__return_true' );

  • Unlike requests sent over admin-ajax.php, the REST API doesn’t load the WordPress admin section via /wp-admin/includes/admin.php, nor does it fire the admin_init action hook. Based on that, it would seem that any plugins or themes that don’t rely on admin-specific functionality—but are making asynchronous requests using admin-ajax.php—should see a slight performance boost by switching over to the REST API.