EthicsComputerCourses
Join training +91 90532 10052

The WordPress REST API

WordPress · Lesson 26 of 30 Advanced 7 min read Updated

By the end of this lesson you will fetch posts from any WordPress site as JSON, create a draft post from the command line using an Application Password, and build your own API endpoint that a mobile app or another website could use.

What is the REST API?

An API (Application Programming Interface) is a way for one program to ask another for data. The REST API in WordPress lets any program read and change site content over normal web addresses, and get answers as JSON, a simple text format for data.

Think of a restaurant. You do not walk into the kitchen; you tell the waiter what you want, and the waiter brings it on a plate. The REST API is the waiter between your app and WordPress's kitchen (the database).

The block editor itself uses the REST API to load and save your posts. So it is always on, and it powers:

  • mobile apps that show your site's content
  • "headless" sites, where WordPress stores content and a separate front end (for example built with React) displays it
  • scripts that create posts or update data automatically

Endpoints and routes

Every API address starts with /wp-json/. After it comes a namespace (core uses wp/v2) and a route. Each route plus a method (GET to read, POST to create or update, DELETE to remove) is an endpoint.

AddressReturns
/wp-json/wp/v2/postsPublished posts
/wp-json/wp/v2/posts/45The post with ID 45
/wp-json/wp/v2/pagesPublished pages
/wp-json/wp/v2/categoriesCategories
/wp-json/wp/v2/mediaMedia items
/wp-json/wp/v2/courseYour custom post type (if show_in_rest is true)

Open https://yoursite/wp-json/wp/v2/posts in a browser right now. You will see a block of JSON text. That is the API answering.

If your site uses Plain permalinks, use ?rest_route=/wp/v2/posts instead of /wp-json/wp/v2/posts.

Step 1: Read posts with curl

curl is a command-line tool for making web requests; it comes with Windows 10 and later, macOS and Linux. Ask for the two newest posts, and only three fields:

Terminal
curl "https://brightfuture.in/wp-json/wp/v2/posts?per_page=2&_fields=id,title,link"
Output
[{"id":45,"title":{"rendered":"Class 10 Board Exam Tips"},"link":"https:\/\/brightfuture.in\/class-10-board-exam-tips\/"},{"id":41,"title":{"rendered":"Diwali Holiday Notice"},"link":"https:\/\/brightfuture.in\/diwali-holiday-notice\/"}]

Two things to notice:

  • title is an object with rendered inside, because titles can be processed by filters before display.
  • Slashes appear as \/. That is normal escaped JSON; every JSON reader turns it back into /.

Useful query parameters:

ParameterExampleMeaning
per_pageper_page=5How many items (maximum 100)
pagepage=2Which page of results
searchsearch=jeeText search
categoriescategories=3Only posts in category ID 3
_fields_fields=id,titleReturn only these fields (smaller, faster)
_embed_embedInclude related data such as the featured image

The response headers X-WP-Total and X-WP-TotalPages tell you how many items and pages exist in total.

Step 2: Read posts with JavaScript

The same request from a web page uses fetch. This prints the latest course titles on any page, even on another website:

JavaScript
const url = "https://brightfuture.in/wp-json/wp/v2/course?per_page=3&_fields=title,link";

const response = await fetch(url);
const courses = await response.json();

for (const course of courses) {
  console.log(course.title.rendered, "->", course.link);
}
Output
JEE Main Maths Crash Course -> https://brightfuture.in/courses/jee-main-maths-crash-course/
Class 10 Science Foundation -> https://brightfuture.in/courses/class-10-science-foundation/
Class 12 Physics Revision -> https://brightfuture.in/courses/class-12-physics-revision/

If you insert these titles into a page, use textContent, not innerHTML, because API text comes from outside your script.

Step 3: Write data with an Application Password

Reading public posts needs no login. Creating, editing or deleting needs authentication: proof of who you are. For scripts and apps, use Application Passwords:

  1. Step 1: Go to Users > Profile and scroll to Application Passwords.
  2. Step 2: Type a name such as "Notice script" and click Add Application Password.
  3. Step 3: WordPress shows a password made of groups of four characters, once. Copy it into your password manager.
  4. Step 4: Store it in an environment variable on your computer, never inside a script you share.

Now create a draft post. The password is read from the variable, so it never appears in the command:

Terminal
curl -X POST "https://brightfuture.in/wp-json/wp/v2/posts?_fields=id,status,title" \
  --user "rohan.owner:$WP_APP_PASSWORD" \
  -H "Content-Type: application/json" \
  -d '{"title":"Holiday on 25 December","content":"The centre will stay closed.","status":"draft"}'
Output
{"id":58,"status":"draft","title":{"raw":"Holiday on 25 December","rendered":"Holiday on 25 December"}}

Open Posts in the dashboard: the draft is there. Each Application Password can be revoked on its own, without changing your main login password. Application Passwords need HTTPS on live sites.

The user's role still applies. A Contributor's Application Password cannot publish, just as the Contributor cannot publish in the dashboard.

Step 4: Your own endpoint

Plugins can add their own routes with register_rest_route() on the rest_api_init hook. This endpoint tells an app how many seats are left in each batch:

PHP
add_action( 'rest_api_init', function () {
    register_rest_route( 'brightfuture/v1', '/seats', [
        'methods'             => 'GET',
        'callback'            => 'bf_rest_seats',
        'permission_callback' => '__return_true', // public, read-only data
        'args'                => [
            'batch' => [
                'type'              => 'string',
                'sanitize_callback' => 'sanitize_key',
            ],
        ],
    ] );
} );

function bf_rest_seats( WP_REST_Request $request ) {
    $seats = [ 'class-10' => 5, 'class-12' => 3 ];
    $batch = $request->get_param( 'batch' );

    if ( ! $batch ) {
        return rest_ensure_response( $seats );
    }
    if ( ! isset( $seats[ $batch ] ) ) {
        return new WP_Error( 'bf_unknown_batch', 'No such batch.', [ 'status' => 404 ] );
    }
    return rest_ensure_response( [ 'batch' => $batch, 'seats' => $seats[ $batch ] ] );
}

Three requests and their answers:

Output
GET /wp-json/brightfuture/v1/seats                  {"class-10":5,"class-12":3}
GET /wp-json/brightfuture/v1/seats?batch=class-12   {"batch":"class-12","seats":3}
GET /wp-json/brightfuture/v1/seats?batch=class-9    {"code":"bf_unknown_batch","message":"No such batch.","data":{"status":404}}

The last one also returns HTTP status 404, so apps know it failed.

Logged-in JavaScript in the dashboard

When your own JavaScript runs on a page where the user is already logged in (for example a custom admin screen), it authenticates with a nonce instead of an Application Password. Create it in PHP with wp_create_nonce( 'wp_rest' ), pass it to your script, and send it in the X-WP-Nonce header. The official @wordpress/api-fetch package does this for you.

Common mistakes

  • Putting Application Passwords in front-end JavaScript. Anyone can read page source. Keep them on servers and in environment variables only.
  • Leaving out show_in_rest on custom post types and meta, then wondering why the endpoint is missing.
  • Forgetting pagination. per_page is capped at 100; loop through page=2, page=3 using X-WP-TotalPages.
  • Using __return_true on write endpoints. That lets anyone change your data.
  • Inserting API text with innerHTML. Use textContent or escape it.

Key points

  • The REST API serves WordPress content as JSON under /wp-json/; core routes use the wp/v2 namespace.
  • Use query parameters such as per_page, search and _fields to shape responses.
  • Reading public content needs no login; writing needs authentication, such as Application Passwords over HTTPS.
  • Custom post types appear in the API when registered with show_in_rest.
  • register_rest_route() adds your own endpoints; always write a proper permission_callback.
  • Return data with rest_ensure_response() and errors with WP_Error plus an HTTP status.

Exercise

Using curl, fetch the five most recent posts in the category with ID 3 from your local site, returning only id, date and title. Then add a custom endpoint brightfuture/v1/next-test that returns {"subject":"Physics","date":"2026-11-15"} to anyone, and another endpoint brightfuture/v1/enquiries that only Administrators can read.

Show solution
Terminal
curl "http://localhost/coaching/wp-json/wp/v2/posts?categories=3&per_page=5&_fields=id,date,title"
PHP
add_action( 'rest_api_init', function () {
    register_rest_route( 'brightfuture/v1', '/next-test', [
        'methods'             => 'GET',
        'callback'            => fn() => rest_ensure_response( [ 'subject' => 'Physics', 'date' => '2026-11-15' ] ),
        'permission_callback' => '__return_true',
    ] );
    register_rest_route( 'brightfuture/v1', '/enquiries', [
        'methods'             => 'GET',
        'callback'            => fn() => rest_ensure_response( [] ),
        'permission_callback' => fn() => current_user_can( 'manage_options' ),
    ] );
} );

Calling /enquiries without logging in returns a rest_forbidden error with status 401.

Quick Quiz

1. What does the REST API return?

2. Which parameter returns only selected fields?

3. How should a script authenticate to create posts on a live site?

4. What does permission_callback in register_rest_route() decide?

Learn WordPress with a mentor

Free lessons get you started. Our WordPress & Website Building training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call