# WordPress REST API: read and write data from outside your site

- آدرس: https://di9.ir/blog/en/wordpress-rest-api-tutorial
- نویسنده: danidiba
- انتشار: 2026-09-29T08:14:00+03:30
- به‌روزرسانی: 2026-09-29T08:14:00+03:30
- دسته: WordPress Development
- برچسب‌ها: API، Application Passwords، REST API، توسعه وردپرس، وردپرس

> The built-in WordPress REST API lets another app read, create or edit posts. This article covers reading without login, writing with an application password, building a custom route with validation and permission checks, and security tips.

Sometimes you need to read a WordPress site's data from outside it: showing the latest posts on another site, feeding a mobile app, or filling products automatically from an inventory system. WordPress has a REST API for this, built into core for years, with no plugin needed. It sends and receives data as JSON.

If you want to build your own custom route, start with [building a WordPress plugin from scratch](https://di9.ir/blog/%D8%B3%D8%A7%D8%AE%D8%AA-%D8%A7%D9%81%D8%B2%D9%88%D9%86%D9%87-%D9%88%D8%B1%D8%AF%D9%BE%D8%B1%D8%B3-%D8%A7%D8%B2-%D8%B5%D9%81%D8%B1), because this article's code belongs in a plugin.

## Reading: no login

Public content needs no authentication:

```bash
curl "https://example.com/wp-json/wp/v2/posts?per_page=3&_fields=id,title,link"
```

- `per_page` limits the count.
- `_fields` returns only the fields you need and keeps the response light.
- `search`, `categories` and `page` handle filtering and pagination.

Post types registered with `show_in_rest`, such as a [custom post type](https://di9.ir/blog/%D9%BE%D8%B3%D8%AA-%D8%AA%D8%A7%DB%8C%D9%BE-%D8%B3%D9%81%D8%A7%D8%B1%D8%B4%DB%8C-%D9%88%D8%B1%D8%AF%D9%BE%D8%B1%D8%B3), are available at their own routes. The list of routes is visible at `/wp-json/`.

## Writing: application passwords

To create or edit content, you must prove the identity of a user with enough permissions. A user's profile has an Application Passwords section; create a separate password for each app so that if one leaks, you revoke only that one. This feature is usually enabled only on HTTPS connections.

```bash
curl -X POST "https://example.com/wp-json/wp/v2/posts" \
  -u "USERNAME:APPLICATION-PASSWORD" \
  -H "Content-Type: application/json" \
  -d '{"title":"Test post","content":"Body text","status":"draft"}'
```

Notes:

- Set the status to `draft` so you can review before publishing.
- Don't put the password in source code or a public repository; keep it in an environment variable or a config file outside the web root.
- Give the API user only the role it needs (for example "Author"), not Administrator.

The same request in PHP, using WordPress's own function (from another WordPress site):

```php
<?php
$response = wp_remote_post( 'https://example.com/wp-json/wp/v2/posts', array(
	'timeout' => 15,
	'headers' => array(
		'Authorization' => 'Basic ' . base64_encode( $user . ':' . $app_password ),
		'Content-Type'  => 'application/json',
	),
	'body'    => wp_json_encode( array(
		'title'  => 'Test post',
		'status' => 'draft',
	) ),
) );

if ( is_wp_error( $response ) ) {
	error_log( $response->get_error_message() );
} elseif ( 201 !== wp_remote_retrieve_response_code( $response ) ) {
	error_log( 'REST error: ' . wp_remote_retrieve_body( $response ) );
}
```

## Building a custom route

If your data isn't in a standard format, register your own route:

```php
<?php
add_action( 'rest_api_init', function () {
	register_rest_route( 'demo/v1', '/stats', array(
		'methods'             => WP_REST_Server::READABLE,
		'callback'            => 'demo_stats',
		'permission_callback' => function () {
			return current_user_can( 'manage_options' );
		},
		'args'                => array(
			'days' => array(
				'default'           => 7,
				'sanitize_callback' => 'absint',
				'validate_callback' => function ( $value ) {
					return is_numeric( $value ) && $value > 0 && $value <= 90;
				},
			),
		),
	) );
} );

function demo_stats( WP_REST_Request $request ): WP_REST_Response {
	$days = (int) $request->get_param( 'days' );
	return new WP_REST_Response( array(
		'days'  => $days,
		'posts' => (int) wp_count_posts()->publish,
	), 200 );
}
```

Keep these three parts separate in your mind:

| Part | Job |
| --- | --- |
| `permission_callback` | Who is allowed? Without it, the route is unsafe |
| `validate_callback` | Is the input valid? |
| `sanitize_callback` | Clean the input |

If a route really must be public, use `__return_true`, but only for reading public data; never for writing or private information.

The resulting URL is `https://example.com/wp-json/demo/v1/stats?days=30`, and it must be called as an administrator (for example with an application password).

## Calling it from JavaScript inside your own site

When JavaScript runs on pages of the same site, the user is logged in with a cookie, and you must send a nonce instead of an application password. Pass the base URL and the nonce from PHP to the script:

```php
<?php
add_action( 'wp_enqueue_scripts', function () {
	wp_enqueue_script( 'demo-app', plugins_url( 'app.js', __FILE__ ), array(), '1.0', true );
	wp_add_inline_script(
		'demo-app',
		'window.demoApi = ' . wp_json_encode( array(
			'root'  => esc_url_raw( rest_url( 'demo/v1/' ) ),
			'nonce' => wp_create_nonce( 'wp_rest' ),
		) ) . ';',
		'before'
	);
} );
```

And in `app.js`:

```js
fetch(window.demoApi.root + 'stats?days=30', {
  headers: { 'X-WP-Nonce': window.demoApi.nonce }
}).then(r => r.json()).then(console.log);
```

Without the nonce, WordPress treats the request as logged out, and a route with a `permission_callback` answers 401 or 403.

## Common errors

| Symptom | Likely cause |
| --- | --- |
| 404 on `/wp-json/` | Broken permalinks or a server rule; re-save permalinks |
| 401 when writing | Wrong application password, or no HTTPS |
| 403 with the right password | The user lacks the capability for that action |
| Response contains HTML | A plugin printed a PHP error or warning; check the log |
| 400 with an invalid parameter message | `validate_callback` rejected the value |

If you hit a 500 error, the troubleshooting steps are in [common WordPress errors](https://di9.ir/blog/%D8%AE%D8%B7%D8%A7%D9%87%D8%A7%DB%8C-%D8%B1%D8%A7%DB%8C%D8%AC-%D9%88%D8%B1%D8%AF%D9%BE%D8%B1%D8%B3).

## Security tips

- The user list (`/wp/v2/users`) reveals usernames. If you don't need it, restrict it; this is also part of the [WordPress security checklist](https://di9.ir/blog/%DA%86%DA%A9-%D9%84%DB%8C%D8%B3%D8%AA-%D8%A7%D9%85%D9%86%DB%8C%D8%AA-%D9%88%D8%B1%D8%AF%D9%BE%D8%B1%D8%B3).
- Always use HTTPS when writing; the application password is sent with every request.
- Use `permission_callback` correctly, and always validate and sanitize input.
- Keep heavy responses small with `_fields` and a small `per_page`.
- If you call from a browser with a cookie, you need a nonce named `wp_rest` (the `X-WP-Nonce` header).

## Summary

- The WordPress REST API is built into core and lives at `/wp-json/`.
- Reading public content needs no login; `_fields` and `per_page` keep responses light.
- For writing, use an application password over HTTPS and create a low-privilege user.
- Build custom routes with `register_rest_route` and never forget `permission_callback`.
- Control input with `validate_callback` and `sanitize_callback`.
- Restrict sensitive routes such as the user list.

## پرسش‌های متداول

### What is the WordPress REST API URL?

Usually your site address plus /wp-json/, with the post list at /wp-json/wp/v2/posts. If the site uses plain permalinks, try the same route as the rest_route query parameter instead.

### How do I authenticate to write through the API?

The simplest way is an Application Password, created on the user's profile page. Send the username and that password with Basic Auth, over HTTPS only. Never store the account's main password in another app.

### Why is permission_callback required?

Without it, a route may be open to everyone by accident. WordPress shows a warning for routes that lack this function. In it, you decide who may use the route, for example with current_user_can.

### Can I disable the REST API?

Disabling it completely can break the block editor and some plugins. It's better to restrict sensitive routes such as the user list, and to require authentication for any writing.
