Skip to content

Every route WB Member Blog and WB Member Blog Pro register, who may call it, what it takes and what it returns. Written against version 4.3.0.

Sources: src/Rest/*Controller.php and the two image routes in public/class-buddypress-member-blog-public.php (Free), and includes/class-buddypress-member-blog-pro-rest.php (Pro).

There is one namespace: wbcom-member-blog/v1. Pro registers its routes in the same namespace by extending Free’s RestController, so both plugins share one error shape, one set of permission callbacks and one pagination contract.

https://example.com/wp-json/wbcom-member-blog/v1

The constant is Wbcom\MemberBlog\Rest\RestController::NAMESPACE_V1.

The old bpmb/v1 alias was removed in 4.0.1. Requests to /wp-json/bpmb/v1/* return 404.

Every write route and every private read route uses the WordPress cookie plus a REST nonce, sent in the X-WP-Nonce header.

fetch( wpApiSettings.root + 'wbcom-member-blog/v1/bookmarks', {
method: 'POST',
credentials: 'same-origin',
headers: {
'X-WP-Nonce': wpApiSettings.nonce,
'Content-Type': 'application/json'
},
body: JSON.stringify( { post_id: 123 } )
} );

Any script that declares bpmb-ui-helpers as a dependency gets window.bpmbRestConfig ({ root, nonce }) and the window.bpmbRest( method, path, data ) helper. It attaches the nonce, returns the payload on success, and rejects with an Error carrying .code, .status and .data on failure. Pro’s own scripts use it for every call.

Application Passwords cannot write by default

Section titled “Application Passwords cannot write by default”

The shared write check, RestController::can_mutate(), refuses a request authenticated with an Application Password with 403 bpmb_app_password_write_denied. Every member-facing write here is something a member does on a page they are looking at, so a long-lived token is refused unless the site opts in:

add_filter( 'bpmb_rest_allow_app_passwords', '__return_true' );

This applies to every route that uses can_mutate() or a check built on it, in Free and in Pro.

Every route returns core’s error envelope. Codes are prefixed bpmb_.

{
"code": "bpmb_cannot_edit",
"message": "You cannot edit this post.",
"data": { "status": 403 }
}

Common codes from the shared checks:

Code Status Meaning
bpmb_not_logged_in 401 The route needs a signed-in user.
bpmb_app_password_write_denied 403 A write was attempted with an Application Password.
bpmb_feature_disabled 403 The site owner switched the feature off on the Features screen.
bpmb_forbidden 403 Signed in, but not allowed to act on this object.
bpmb_not_found 404 The object does not exist, or is not published where it must be.

Collection routes that use the shared parameters accept page (default 1) and per_page (default 10, maximum 50) and send X-WP-Total and X-WP-TotalPages. Values outside the bounds are rejected with a 400, not silently clamped.

The Features screen (Member Blog settings, Features tab) is enforced on the server, not only in the page. With a feature off, its routes answer 403 bpmb_feature_disabled:

Feature Routes it closes
Claps POST /reactions
Reading list POST /bookmarks, GET /bookmarks, DELETE /bookmarks/{post_id}
Follow writers POST /follows, GET /follows, DELETE /follows/{object_type}/{object_id}, and GET /feed?source=following (501)

In Pro, each module registers its routes only while the module is on (see Pro routes). A route for a module that is off does not exist and returns 404.

Route Method Access
/dashboard GET Signed in. Own dashboard, or any with edit_others_posts.
/dashboard/counts GET Same as /dashboard.
/posts/render GET Public for published posts. Drafts and pending: owner or edit_others_posts.
/posts/{id}/related GET Public.
/feed GET Public, except following and reading-list, which need a signed-in user.
/popular GET Public.
/authors GET Public.
/featured GET Public.
/posts/{id}/featured POST, DELETE Capability from bpmb_feature_capability (default edit_others_posts).
/bookmarks GET, POST Signed in. Reading list feature on.
/bookmarks/{post_id} DELETE Signed in. Reading list feature on.
/follows GET, POST Signed in. Follow writers feature on.
/follows/{object_type}/{object_id} DELETE Signed in. Follow writers feature on.
/reactions POST Signed in. Claps feature on.
/autosave POST Signed in. Edit rights on the post, or the create gate for a new one.
/draft GET, DELETE Signed in. Edit rights on the post.
/posts/{id}/featured-image DELETE Signed in. Edit rights on the post.
/categories POST Signed in. The Allow custom categories rule (bpmb_access()->can_manage_categories()).
/upload-image POST Signed in, with upload_files or the create gate.
/attach-image POST Signed in, with upload_files or the create gate, plus edit rights on the post and the attachment.
Route Method Module
/posts/{id} DELETE Always registered.
/posts/{id}/status POST Always registered.
/series POST Series
/series/{id} PATCH, DELETE Series
/series/{id}/order PATCH Series
/series/{id}/progress POST Series
/series/{id}/guest-progress POST Series
/coauthors/search GET Co-Authors
/posts/{id}/coauthors POST, PATCH Co-Authors
/posts/{id}/coauthors/{user_id} DELETE Co-Authors
/posts/{id}/coauthors/invitation/accept POST Co-Authors
/posts/{id}/coauthors/invitation/decline POST Co-Authors
/posts/{id}/lock POST, DELETE Co-Authors
/posts/{id}/review POST Co-Authors, with the editorial workflow setting on
/posts/{id}/schedule POST, DELETE Scheduling
/analytics GET Analytics
/analytics/export GET Analytics
/groups/{group_id}/posts/{id}/featured POST Group Blogs
/me/notifications PATCH Author Dashboard

src/Rest/DashboardController.php

A member’s dashboard lists the posts they can edit, which includes posts they co-author. Core’s author parameter cannot express that. Both routes go through DashboardService, which applies the bp_member_blog_dashboard_query_args filter Pro uses to fold in co-authored posts.

Permission: signed in (401 otherwise). Passing another member’s user_id needs edit_others_posts (403 bpmb_forbidden otherwise).

Param Type Default Notes
user_id integer 0 0 is the current user.
tab string published drafts or pending; anything else lists published posts.
search string ''
page integer 1
per_page integer 10 Maximum 50.
[
{
"id": 412,
"title": "Draft about indexes",
"status": "draft",
"date": "2026-07-02T09:14:00",
"link": "https://example.com/?p=412",
"edit_link": "https://example.com/write/?post_id=412",
"views": 0,
"is_owner": true
}
]

Headers: X-WP-Total, X-WP-TotalPages.

Same permission. Accepts user_id.

{ "published": 12, "drafts": 3, "pending": 1, "total": 16, "views": 4821 }

Each count comes from the same service call as the list, so a tab badge cannot disagree with the list under it.

src/Rest/FrontendController.php. Returns one page of a member’s post list as rendered HTML, the same cards and empty state the first page was drawn with. The member home uses it for paging.

Permission: public. The drafts and pending tabs return 403 bpmb_forbidden unless the caller is the owner or has edit_others_posts.

Param Type Default
user_id integer 0 (the current user; 400 bpmb_invalid_user when signed out)
tab string published
page integer 1
search string ''
{
"html": "<div class=\"bpmb-blog-post\">...</div>",
"page": 2,
"max_pages": 4,
"total": 31,
"pagination": "<nav class=\"bpmb-pagination\">...</nav>"
}

Permission: public. Wraps PostQuery::related(), the same method the related-posts shortcode and block use.

Param Type Default Notes
id integer required Must be published, otherwise 404 bpmb_not_found.
per_page integer 4 1 to 12.
[
{
"id": 77,
"title": "Indexing follows",
"link": "https://example.com/indexing-follows/",
"date": "2026-06-21T10:00:00",
"thumbnail": "",
"author": { "id": 4, "name": "Jane Doe" }
}
]

src/Rest/FeedController.php. The route behind Load more on every listing shortcode. One route with a source parameter, where each source calls the same service method its shortcode used for page 1, so page 2 always continues page 1. It returns rendered card HTML.

Permission: public. following and reading-list need a signed-in user (401) and always read the current user from the session; a user_id in the request is ignored for them. Only published posts are returned; post_status is not a parameter.

Public sources are cached for 60 seconds, for signed-out visitors only (the cards carry each member’s own saved, clapped and following state, so a signed-in response is never cached). The cache key uses the registered parameters below; any other query parameter is ignored.

Param Type Default Notes
source string required recent, author, popular, topic, following, reading-list, authors.
page integer 2
per_page integer 10 1 to 50.
user_id integer 0 Required for source=author (400 bpmb_missing_author).
category, tag, search string ''
timeframe string all all, week, month, year (for popular).
orderby string date date, title, comment_count, rand; for authors also post_count, total_views, name, recent.
order string DESC ASC or DESC.
exclude_users string '' Comma-separated user IDs.
start_rank, variant 0, recent Card layout options passed through from the shortcode.
show_image, show_excerpt, show_date, show_author, show_avatar, show_views boolean true Card options.
role, columns (1-6, default 4), avatar_size (32-200, default 80), show_count, show_bio Author directory options (source=authors).
{ "html": "<article ...>", "page": 2, "max_pages": 5, "count": 10, "has_more": true }

source=authors also returns announce, a sentence for the screen-reader status region. Responses for public sources are cached for 60 seconds per parameter set; personal sources are never cached.

src/Rest/DiscoveryController.php. Permission: public. Collection parameters plus:

Param Type Default Notes
category integer 0 Term ID.
timeframe string all all, week, month, year.

Each item: id, title, permalink, date (UTC), thumbnail, views, and author (id, name, url). Headers: X-WP-Total, X-WP-TotalPages.

Permission: public. Collection parameters plus orderby (post_count or total_views, default post_count) and order (asc or desc, default desc).

Each item: id, name, slug, post_count, total_views, description, avatar, url.

src/Rest/FeaturedController.php. Every route wraps FeaturedService, which the Featured row action in wp-admin also calls.

/posts/{id}/featured is the editor’s-pick flag. /posts/{id}/featured-image is the post thumbnail. They are different things.

Permission: public. Collection parameters. Returns a plain array (not an envelope) and the paging headers.

[
{
"id": 101,
"title": "The member blog rebuild",
"permalink": "https://example.com/the-member-blog-rebuild/",
"author": { "id": 4, "name": "Jane Doe", "url": "https://example.com/author/jane/" },
"date": "2026-07-01T08:00:00"
}
]

Permission: FeaturedService::user_can_feature(), which checks the capability returned by bpmb_feature_capability (default edit_others_posts). 401 when signed out, 403 bpmb_cannot_feature when signed in without it.

Param Type Default Notes
order integer 0 0 to 9999. Lower sorts first.

Only a published post can be featured (400 bpmb_not_published). A missing post is 404 bpmb_post_not_found.

{ "post_id": 101, "featured": true, "total": 7 }

Same permission. Returns { "post_id": 101, "featured": false, "total": 6 }.

src/Rest/EngagementController.php. Bookmarks (the reading list), follows and claps. Each route is closed when its feature is switched off (see above).

Param Type Required
post_id integer yes

The post must be published, otherwise 404. Returns { "bookmarked": true, "count": 38 }.

No existence check, so a member can remove a saved post that has since been deleted. Returns { "bookmarked": false, "count": 37 }.

A reading list is private. Collection parameters plus user_id (default the current user); another member’s list needs edit_others_posts. Each item: id, title, link, date, thumbnail, author (id, name). Paging headers.

Param Type Required Notes
object_type string yes user, series or term.
object_id integer yes A user ID, a series term ID, or (for term) a term taxonomy ID.

The target must exist (404). A term must belong to a followable taxonomy, category by default (bpmb_followable_taxonomies), otherwise 400 bpmb_not_followable.

Returns { "following": true, "followers": 129 }.

No existence check. Returns { "following": false, "followers": 128 }.

Returns the current member’s own list only.

Param Type Default
object_type string user
{ "object_type": "user", "ids": [ 4, 19, 22 ] }

Claps.

Param Type Default Notes
post_id integer required Must be published.
claps integer 1 1 to 50 per request.

The client sends one request per burst of taps. The server adds them atomically and caps each member’s total per post at bpmb_max_claps (default 50). Clapping your own post returns 403 bpmb_own_post.

{ "claps": 7, "total": 214, "maxed": false }

claps is the member’s own total for the post, total is the post’s. Fires bpmb_post_clapped ($post_id, $user_id, $mine, $total).

src/Rest/FrontendController.php. The routes the post form uses.

Permission: signed in; bpmb_access()->can_edit() on an existing post, or bpmb_access()->can_create() for a new draft. Returns 403 bpmb_autosave_disabled when the site owner has switched autosave off.

Param Type Default Notes
post_id integer 0 0 creates a new draft.
title string ''
content string '' Sanitized by PostSubmissionService::sanitize_content().
categories integer[] [] Excluded categories are removed.
tags string[] []
editor_data string '' Editor.js block JSON, stored in _bpmb_editorjs_blocks when it decodes.

Autosave never changes a post’s status. The one exception is auto-draft, which becomes draft on the first save with content.

It also never writes over a post readers can see. For a post that is publish, private or future, the content goes to the member’s own autosave revision and the live post is untouched; categories and tags are not written on that path. draft and pending posts are saved in place.

{ "saved": true, "post_id": 42, "stored": "post", "time": "2026-08-12 09:47:11" }
{ "saved": true, "post_id": 42, "stored": "revision", "revision_id": 43, "time": "2026-08-12 09:47:11" }

post_id is always the post, never the revision. An empty title with empty content saves nothing and returns { "saved": false, "post_id": 0 }. time is site-local time.

Asks whether there are unsaved changes on a post the member is reopening, so the editor can offer them back.

Permission: signed in (can_mutate()).

Param Type Required
post_id integer yes

post_id is required. In 4.3.0 the call without an id, which offered the member’s latest stray draft to the blank new-post form, was removed; a new post is never prompted.

The answer reads only this member’s own autosave revision of the post, needs edit rights on the post, and ignores a revision older than the post itself or one the member has dismissed.

{ "has_draft": false }
{
"has_draft": true,
"post_id": 412,
"title": "Draft about indexes",
"content": "<p>...</p>",
"modified": "2026-07-11T16:49:00Z",
"editor_data": "{\"blocks\":[...]}",
"context": "unpublished_changes"
}

modified is the autosave time in UTC (ISO 8601 with Z). Format it on the client, in the member’s own clock. The old modified_display field was removed in 4.3.0.

“Stop offering me this one.” Records that the member dismissed the changes on this post; it does not delete anything. Newer changes made after the dismissal are offered again. Dismissals older than 30 days are pruned.

Param Type Required
post_id integer yes

Needs edit rights on the post (403 bpmb_cannot_dismiss). Returns { "dismissed": true, "post_id": 412 }.

Permission: signed in plus bpmb_access()->can_edit( $id ). Clears the post thumbnail. Returns { "removed": true, "post_id": 412 }.

Adds a category from the post form. Permission: signed in; the callback then asks bpmb_access()->can_manage_categories(), which is where the Allow custom categories setting is enforced (403 bpmb_cannot_add_category). This creates a term only; there is no route to rename or delete one.

Param Type Required
name string yes (1 to 200 characters)
parent integer no, default 0; must exist (400 bpmb_invalid_parent)

A name that already exists returns the existing term. A category the site owner has excluded returns 403 bpmb_category_excluded. Returns { "id": 19, "name": "Engineering" }.

public/class-buddypress-member-blog-public.php. The only upload transport for the editor.

Permission: signed in, and either upload_files or bpmb_access()->can_create().

multipart/form-data, field name image, optional post_id (needs edit rights on it).

Allowed types come from bpmb_media()->allowed_mime_types(): the Allowed image types setting, never SVG, then the bpmb_allowed_image_mime_types and bpmb_rest_upload_allowed_types filters. The size limit comes from bpmb_media()->max_upload_bytes(): the upload size setting in MB, then bpmb_max_upload_size (MB) and bpmb_rest_upload_max_size (bytes).

{
"success": true,
"data": {
"file": { "url": "https://example.com/wp-content/uploads/2026/07/photo.jpg", "width": 1600, "height": 900 },
"attachment_id": 913
}
}

Error codes (no bpmb_ prefix on this route): no_file, invalid_file, invalid_type, file_too_large (all 400), no_permission (403), upload_failed (500).

Moves an existing attachment onto a post.

Param Type Required
post_id integer yes
attachment_id integer yes

Needs edit rights on the post, and the caller must be able to edit the attachment or own it. Returns { "success": true, "data": { "message": "Image attached to post." } }.

Free does not add a create or update route for posts. Members write through core’s /wp/v2/posts, and two classes make that endpoint follow the plugin’s rules.

src/Rest/RestGate.php hooks rest_pre_insert_post and rest_after_insert_post for the post type:

  • On create, bpmb_access()->can_create(); on update, bpmb_access()->can_edit(). Refusals return 403 bpmb_rest_cannot_create (with the real reason, such as a post limit or no credits) or 403 bpmb_rest_cannot_edit.
  • A request for publish, future or private is passed through bpmb_submission()->resolve_status(), so a moderated member lands in pending. It only clamps down; a request for draft stays a draft, and an edit to a live post does not pull it off the site.
  • A request for private that cannot be honoured (the member’s posts go to review, or bpmb_allow_private_posts is off) is refused with 403 bpmb_rest_private_not_allowed. It is never downgraded, because a clamped pending post would be published publicly on approval.
  • Runs the bp_member_blog_pro_validate_post filter, where Pro enforces the word limits and the co-author edit lock. Failures return 400 bpmb_rest_validation_failed.
  • After insert, excluded categories are stripped.

Administrators and anyone with edit_others_posts are exempt. Change that with bpmb_rest_gate_exempt (bool $exempt, int $user_id).

Pro adds one more guard on the same hook: a member can only assign their post to a series they own.

src/Rest/MetaFields.php registers three post meta keys on /wp/v2/posts, readable by anyone who can read the post and writable by anyone who can edit it: _bpmb_focus_keyword, _bpmb_meta_description, _bpmb_editorjs_blocks. Credit and moderation state is deliberately not exposed. Add keys with bpmb_rest_exposed_post_meta.

Pro routes live in includes/class-buddypress-member-blog-pro-rest.php. Every one uses can_mutate() (signed in, no Application Password) unless noted, and the object-level check lives in the module’s own method, which is also what the page’s buttons call. Errors use the same bpmb_ envelope.

Route What it does Who may
DELETE /posts/{id} Moves the post to Trash (not a permanent delete). bpmb_access()->can_delete() on the post.
POST /posts/{id}/status Takes a published post offline, or puts an unpublished one live. can_edit(); going live also needs can_publish().
Route Params Who may
POST /series name (required) The capability from bpmb_series_create_capability (default edit_posts).
PATCH /series/{id} name, description, status (active or completed). Fields left out are kept. The series owner, or a user with edit_others_posts.
DELETE /series/{id} The series owner, or a user with edit_others_posts.
PATCH /series/{id}/order order (post IDs) The series owner only. IDs not in the series are dropped.
POST /series/{id}/progress post_id (required) Signed-in reader. Records reading progress.
POST /series/{id}/guest-progress progress (object) Public, rate limited (429 bpmb_rate_limited). Validates a guest’s locally stored progress.

The owner check reads the series’ own author. Other members get 403 bpmb_forbidden. An unknown status returns 400 bpmb_invalid_status. POST /series/{id}/progress returns 403 bpmb_progress_off when progress tracking is off. POST /series reuses a series of that name only when the caller owns it; otherwise it creates a separate series for them. Core’s /wp/v2/bp_member_blog_series term endpoint requires edit_others_posts to rename or delete a series.

Route Params Notes
GET /coauthors/search query (required), post_id Members to invite. Matches names and whole email addresses, not partial ones.
POST /posts/{id}/coauthors invitee (required) Invite. The post must be saved first (400 bpmb_post_not_saved); the caller must manage the post’s co-authors.
PATCH /posts/{id}/coauthors order (user IDs, required) Reorder.
DELETE /posts/{id}/coauthors/{user_id} The author removes a co-author, or a co-author removes themselves.
POST /posts/{id}/coauthors/invitation/accept The invitee’s answer. Checks the invitation, not edit rights.
POST /posts/{id}/coauthors/invitation/decline Same.
POST /posts/{id}/lock force (boolean) Take the edit lock. Another holder returns 409 with locked_by; force: true is Take Over. Needs edit rights.
DELETE /posts/{id}/lock Release the lock.
POST /posts/{id}/review decision (approve or reject, required), feedback Registered only when the editorial workflow setting is on. Reviewers only. A reject needs feedback (400 bpmb_feedback_required); a post no longer pending returns 409 bpmb_not_pending. Uses the same approve and send-back path as Free’s moderation.
Route Params Notes
POST /posts/{id}/schedule datetime (required) Schedule or reschedule a draft or scheduled post.
DELETE /posts/{id}/schedule Cancel a schedule.

Both need rights over the post’s schedule (403 bpmb_forbidden). A post that is not a draft or scheduled post returns 404.

Route Params Notes
GET /analytics user_id, post_id, days (default 30) An author’s roll-up, or one post’s numbers.
GET /analytics/export user_id, days The same roll-up as { csv, filename }.

A post’s numbers are visible only to that post’s author and to users with edit_others_posts. The check reads the post’s real author, not a user_id in the request. A roll-up for another member also needs edit_others_posts. The caller must also hold one of the roles allowed in the Analytics settings (all signed-in members when none are set; administrators always).

POST /groups/{group_id}/posts/{id}/featured toggles a post’s featured flag inside a group. The post must be linked to the group (400 bpmb_not_in_group) and the caller must be allowed to feature posts in that group.

PATCH /me/notifications saves the current member’s email and on-site notification choices, the same form as the Author Dashboard settings screen. It is a whole-form save: every preference field not sent (for example bpmb_notify_published, bpmb_notify_digest) is stored as off.

Filter Default Effect
bpmb_rest_allow_app_passwords false Allow Application Passwords to write.
bpmb_rest_gate_exempt Staff exempt Who skips the submission rules on /wp/v2/posts.
bpmb_rest_exposed_post_meta Three keys Post meta exposed on /wp/v2/posts.
bpmb_rest_collection_items Filter the items of any collection response before it is sent.
bpmb_feature_capability edit_others_posts The capability /posts/{id}/featured needs.
bpmb_max_claps 50 The per-member clap cap.
bpmb_followable_taxonomies category Taxonomies a member may follow with object_type=term.
bpmb_allowed_image_mime_types Setting Upload allowlist everywhere.
bpmb_rest_upload_allowed_types Setting Upload allowlist, applied last.
bpmb_max_upload_size Setting (MB) Upload size limit everywhere.
bpmb_rest_upload_max_size Setting (bytes) Upload size limit, applied last.
bp_member_blog_dashboard_query_args Widens the dashboard query; applies to /dashboard and /posts/render.
bpmb_series_create_capability edit_posts Who may create a series (Pro).

The complete list is in the Hook Reference. To add your own routes, see Extend the REST API.