All app-to-WordPress communication goes through the Apps API at /wp-json/apps/v1/. Apps never use /wp/v2/ directly.
Every request must include a Bearer token and app ID:
GET /wp-json/apps/v1/posts?status=publish
Host: example.com
Authorization: Bearer {access_token}
X-App-Id: com.example.my-seo-app
X-Request-Id: {uuid-v4}
Content-Type: application/json
Accept: application/jsonAccess tokens are short-lived (1 hour). When a request returns 401, the SDK automatically refreshes the token using the refresh token and retries the request once.
Requires posts:read for GET, posts:write for POST/PUT, posts:delete for DELETE.
GET /apps/v1/posts
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
status |
string | publish |
Post status: publish, draft, pending, private. |
post_type |
string | post |
Post type. |
per_page |
integer | 10 | Results per page. Max: 100. |
page |
integer | 1 | Page number. |
orderby |
string | date |
Sort field: date, title, modified. |
order |
string | desc |
Sort direction: asc, desc. |
meta_key |
string | -- | Filter by meta key. |
meta_value |
string | -- | Filter by meta value. |
meta_compare |
string | = |
Meta comparison: =, !=, >, >=, <, <=. |
after |
string | -- | Posts after this ISO 8601 date. |
categories |
string | -- | Comma-separated category IDs. |
Response:
[
{
"id": 42,
"title": { "rendered": "My Blog Post" },
"content": { "rendered": "<p>Post content...</p>" },
"excerpt": { "rendered": "" },
"status": "publish",
"type": "post",
"slug": "my-blog-post",
"date": "2026-04-10T12:00:00",
"modified": "2026-04-10T14:30:00",
"author": 1,
"link": "https://example.com/my-blog-post/"
}
]Pagination headers:
X-WP-Total: 42
X-WP-TotalPages: 3
Link: <https://example.com/wp-json/apps/v1/posts?page=2>; rel="next"Apps with posts:read:published scope are restricted to status=publish regardless of what they request.
GET /apps/v1/posts/{id}
Returns a single post object. Returns 404 if not found.
POST /apps/v1/posts
Request body:
{
"title": "My New Post",
"content": "<p>Post content here.</p>",
"status": "draft",
"post_type": "post"
}Returns the created post with HTTP 201.
PUT /apps/v1/posts/{id}
Request body (partial updates supported):
{
"title": "Updated Title",
"content": "<p>Updated content.</p>",
"status": "publish"
}DELETE /apps/v1/posts/{id}
Moves the post to trash. Returns:
{ "deleted": true, "id": 42 }Requires postmeta:read for GET, postmeta:write for PUT/DELETE.
All meta keys are automatically namespaced per app. Writing seo_score for app com.example.seo stores _com_example_seo_seo_score in WordPress. Apps can only read/write their own namespaced keys.
GET /apps/v1/posts/{id}/meta
Returns all meta keys belonging to this app for the given post:
{
"_com_example_seo_seo_score": 85,
"_com_example_seo_seo_title": "Optimized Title"
}PUT /apps/v1/posts/{id}/meta/{key}
Request body:
{ "value": 85 }The {key} is auto-prefixed if it does not already start with the app's namespace prefix. You can write either seo_score or the full prefixed key.
Returns:
{ "key": "_com_example_seo_seo_score", "value": 85 }DELETE /apps/v1/posts/{id}/meta/{key}
Requires site:read.
GET /apps/v1/site
Returns site settings:
{
"name": "My WordPress Site",
"description": "Just another WordPress site",
"url": "https://example.com",
"language": "en_US",
"timezone": "America/New_York"
}Requires users:read:basic or users:read:full.
GET /apps/v1/users
GET /apps/v1/users/{id}
GET /apps/v1/users/me
Fields returned depend on scope:
| Scope | Fields |
|---|---|
users:read:basic |
id, name, email, role |
users:read:full |
All profile fields |
Requires media:read for GET, media:write for POST.
GET /apps/v1/media
GET /apps/v1/media/{id}
POST /apps/v1/media (multipart upload)
Apps can invalidate their cached content.
POST /apps/v1/cache/purge
{
"scope": "block",
"block_name": "my-app/pricing-table",
"post_id": 42
}POST /apps/v1/cache/purge
{
"scope": "all"
}Response:
{ "status": "purged", "scope": "block" }Used during the OAuth installation flow. Not called directly by app code -- the SDK handles this.
POST /apps/v1/token
{
"app_id": "com.example.my-seo-app",
"code": "{auth_code}"
}Returns:
{
"access_token": "{token}",
"refresh_token": "{token}",
"expires_in": 3600,
"token_type": "Bearer",
"scopes": ["posts:read", "postmeta:write"]
}POST /apps/v1/token/refresh
{
"app_id": "com.example.my-seo-app",
"refresh_token": "{refresh_token}"
}Returns a new token pair. The old refresh token is invalidated (rotation).
Requires posts:read.
GET /apps/v1/posts/{id}/revisions
GET /apps/v1/posts/{id}/revisions/{rev_id}
Revision data includes app-written post meta values at that point in time.
POST /apps/v1/jobs
{
"name": "reindex_all_posts",
"endpoint": "/jobs/reindex",
"delay_seconds": 0,
"timeout_ms": 60000
}The runtime queues the job and calls the app's endpoint.
Every response includes rate limit headers:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1714003600
X-RateLimit-Scope: read| Operation | Limit | Window |
|---|---|---|
Read (GET) |
1,000 | per hour |
Write (POST, PUT) |
200 | per hour |
Delete (DELETE) |
50 | per hour |
| Bulk operations | 20 | per hour |
| Token refresh | 10 | per hour |
| Cache purge | 30 | per hour |
Emails (email:send) |
50/hour, 500/day | per app |
| Meta writes per post | 20 | per minute |
| Meta keys per app per post | 50 | total |
| Meta value size | 64 KB | per value |
| Event webhook deliveries | 1,000 | per hour |
When a limit is exceeded, the API returns 429 Too Many Requests:
{
"code": "rate_limited",
"message": "Read rate limit exceeded. Resets in 423 seconds.",
"data": {
"status": 429,
"limit": 1000,
"remaining": 0,
"reset": 1714003600
}
}All errors follow this format:
{
"code": "error_code",
"message": "Human-readable error message.",
"data": {
"status": 403,
"required_scope": "users:write",
"app_id": "com.example.my-seo-app"
}
}| Code | HTTP Status | Description |
|---|---|---|
missing_token |
401 | No Authorization: Bearer header provided. |
invalid_token |
401 | Access token is invalid. |
expired_token |
401 | Access token has expired. Use refresh token. |
invalid_refresh_token |
401 | Refresh token is invalid or expired. |
insufficient_scope |
403 | App does not have the required permission scope. |
rate_limited |
429 | Rate limit exceeded. Check X-RateLimit-Reset header. |
not_found |
404 | Requested resource does not exist. |
invalid_request |
400 | Malformed request body or parameters. |
invalid_code |
400 | Auth code is invalid, expired, or already used. |
payload_too_large |
413 | Response exceeds max_payload_bytes. |
timeout |
504 | App did not respond within timeout_ms. |
conflict |
409 | Resource conflict (e.g., duplicate app ID). |
internal_error |
500 | Unexpected server error. |
delete_failed |
500 | Could not delete the resource. |
The ApiClient in the SDK wraps all of these endpoints:
// In any handler:
$posts = $req->api->get('/apps/v1/posts', ['per_page' => 50]);
$post = $req->api->get('/apps/v1/posts/42');
$new = $req->api->post('/apps/v1/posts', ['title' => 'New Post', 'status' => 'draft']);
$req->api->put('/apps/v1/posts/42/meta/seo_score', ['value' => 92]);
$req->api->delete('/apps/v1/posts/42');
$site = $req->api->get('/apps/v1/site');
$req->api->post('/apps/v1/cache/purge', ['scope' => 'all']);Token refresh and request signing are handled automatically.