The WordPress REST API
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.
| Address | Returns |
|---|---|
/wp-json/wp/v2/posts | Published posts |
/wp-json/wp/v2/posts/45 | The post with ID 45 |
/wp-json/wp/v2/pages | Published pages |
/wp-json/wp/v2/categories | Categories |
/wp-json/wp/v2/media | Media items |
/wp-json/wp/v2/course | Your 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:
curl "https://brightfuture.in/wp-json/wp/v2/posts?per_page=2&_fields=id,title,link"[{"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:
titleis an object withrenderedinside, 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:
| Parameter | Example | Meaning |
|---|---|---|
per_page | per_page=5 | How many items (maximum 100) |
page | page=2 | Which page of results |
search | search=jee | Text search |
categories | categories=3 | Only posts in category ID 3 |
_fields | _fields=id,title | Return only these fields (smaller, faster) |
_embed | _embed | Include 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:
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);
}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:
- Step 1: Go to Users > Profile and scroll to Application Passwords.
- Step 2: Type a name such as "Notice script" and click Add Application Password.
- Step 3: WordPress shows a password made of groups of four characters, once. Copy it into your password manager.
- 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:
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"}'{"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:
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:
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_reston custom post types and meta, then wondering why the endpoint is missing. - Forgetting pagination.
per_pageis capped at 100; loop throughpage=2,page=3usingX-WP-TotalPages. - Using
__return_trueon write endpoints. That lets anyone change your data. - Inserting API text with
innerHTML. UsetextContentor escape it.
Key points
- The REST API serves WordPress content as JSON under
/wp-json/; core routes use thewp/v2namespace. - Use query parameters such as
per_page,searchand_fieldsto 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 properpermission_callback.- Return data with
rest_ensure_response()and errors withWP_Errorplus 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
curl "http://localhost/coaching/wp-json/wp/v2/posts?categories=3&per_page=5&_fields=id,date,title"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?
REST API responses are JSON, which any language can read.
2. Which parameter returns only selected fields?
_fields=id,title trims the response to those fields.
3. How should a script authenticate to create posts on a live site?
Application Passwords are separate, revocable credentials for apps and scripts.
4. What does permission_callback in register_rest_route() decide?
It returns true or false to allow or deny the request.
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
