REST: Pro namespace
Free & ProThe Pro plugin registers its own REST namespace, buddynext-pro/v1, with over 70 registered routes (across the controllers under includes/) and growing - read /wp-json/buddynext-pro/v1 on a live install rather than trusting a count in prose. This page is the route reference for developers building on the Pro surfaces: membership and billing, analytics, drip and broadcast campaigns, member labels and plans, moderation rules, AI assistance, scheduled posts, push, saved searches, the member portfolio, Learnomy course links, and the realtime + payment-gateway (Stripe / PayPal) webhook endpoints.


Contract
buddynext-pro/v1 follows the same envelope, error shape, pagination, and wp_rest nonce rules as Free - see the REST contract page (14-rest-contract) for the cross-surface conventions, and REST: Auth and Account (21-rest-auth-account) for the auth flows. Pro-specific points:
- The namespace is
buddynext-pro/v1, notbuddynext/v1. All paths below are prefixed with/wp-json/buddynext-pro/v1. - Most routes are admin- or owner-gated. Campaign, moderation-rule, label-admin, analytics-overview, and AI-classify routes require an admin capability. Member-scoped routes (anything under
/me/..., own subscriptions, saved searches, push) require login. A few are public reads. - The payment-webhook routes are open at the permission layer but signed at the payload layer:
/stripe/membership-webhookand/paypal/membership-webhookregisterpermission_callback => __return_trueand are authorised entirely by verifying the provider's signature on the payload./realtime/authis login-gated and additionally enforces per-channel access. See the highlight below - "open" does not mean "unauthenticated trust".
Source of truth: the controllers under includes/ in the Pro plugin - grep register_rest_route( for the buddynext-pro/v1 namespace.
Open-but-signed routes (read this first)
| Method | Path | Permission gate | How it is actually authorised |
|---|---|---|---|
| POST | /stripe/membership-webhook |
none (public) |
Verifies the Stripe-Signature header against the configured webhook secret via \Stripe\Webhook::constructEvent(); rejects with stripe_invalid_signature on mismatch and stripe_webhook_secret_missing when no secret is set. Registered by Payments/Gateways/Stripe/StripeGateway::register_webhook_route(); certain invoice event types are delegated internally to Stripe/WebhookController's dispatch logic rather than being a separate route. |
| POST | /paypal/membership-webhook |
none (public) |
The membership gateway's PayPal webhook receiver; verifies the event via PayPal's verify-webhook-signature API before processing. Handled in Payments/Gateways/PayPal/PayPalGateway. |
| POST | /realtime/auth |
require_logged_in |
Mints a Soketi/Pusher channel auth signature key:hmac_sha256(socket_id:channel, secret) - but only after confirming the current user may access the requested private channel. Handled in Realtime/AuthController. |
/stripe/membership-webhook has no WordPress capability check because Stripe calls it server-to-server with no session; its trust comes entirely from the HMAC signature on the payload. /realtime/auth is login-gated and additionally enforces per-channel access before returning the signature, so a logged-in user cannot subscribe to a channel they are not entitled to.
Routes by domain
Membership: plans, checkout, subscriptions
Plans are the membership plans. Plan CRUD lives under /tiers; the buyer-facing checkout flow (plan list, gateway list, checkout, quote) lives under /membership/* in Membership/CheckoutController; subscriptions and the billing portal live in Membership/Controllers/SubscriptionsController.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET, POST | /tiers |
Public (GET) / Admin (POST) | List plans; create a plan. |
| GET, DELETE | /tiers/{id} |
Public (GET) / Admin (DELETE) | Get a plan; delete a plan. |
| GET | /membership/plans |
Public | List purchasable plans. |
| GET | /membership/gateways |
Public | List enabled payment gateways. |
| POST | /membership/checkout |
Logged in | Start a checkout for a plan (plan_id, optional gateway, mode, coupon, country). |
| GET | /membership/checkout-nonce |
Public | Mint a checkout CSRF nonce for an anonymous visitor. Public by design: a nonce is session-bound CSRF protection, not a secret, and anonymous checkout (which registers the account on submit) needs to work while logged out. |
| POST | /membership/register |
Public | Register + buy in one call, for the native app's paid-signup funnel where /membership/checkout (which requires a session) has no account to attach to. Re-dispatches into Free's POST /auth/register first - so the spam guard, registration policy, approval hold, 2FA, and session issuer all still run with no second copy - then hands the new member to the same run_checkout() the web form uses. Body: plan_id (required), optional gateway, mode. |
| POST | /membership/quote |
Logged in | Return a price quote (subtotal, tax, discount, total) for a plan without charging. |
| POST | /me/billing-portal |
Logged in | Create a billing-portal session for the current user. |
| GET | /me/subscriptions |
Logged in | Current user's subscriptions. Each row carries a capabilities block - see below. |
| POST | /me/subscriptions/{id}/cancel |
Logged in | Cancel one of the current user's subscriptions. |
| GET | /me/plan-change/quote |
Logged in | Price a plan change (upgrade/downgrade proration) for the caller's own subscription. Body/query: plan_id. Distinct from /membership/quote, which prices a NEW purchase, not a change to an existing subscription. |
| POST | /me/plan-change |
Logged in | Move the caller's own subscription onto a different plan. Body: plan_id (required), plan_interval (month / year, default month - honoured only when the target plan sells that interval). |
| GET | /users/{id}/subscriptions |
Admin | A user's subscription history. |
The capabilities block on /me/subscriptions
Every row carries capabilities, and any client rendering a membership control should read it
rather than deriving the rules again. That is not style advice: the same rules exist in the
cancel endpoint, the plan-change service and the gateway registry, and every time a surface has
re-derived them it has drifted - a Cancel button that 409'd on click, a Switch button that worked
for a member billed by WooCommerce.
The response is a bare array (typed MySubscription[] by the mobile app), so the block is a key on
each element rather than a sibling of the list; wrapping the list would break every installed copy.
| Key | Type | Means |
|---|---|---|
can_buy |
bool | The site has something for sale. The one site-level answer in the block, so it is identical on every row. False on a free-only site or one whose gateway was never credentialed - render no Buy CTA at all rather than a link to an empty pricing page. |
can_change |
bool | This member may move to another plan. False for anything billed elsewhere. |
can_cancel |
bool | The cancel endpoint will accept. When false, reason says why. |
can_update_payment |
bool | POST /me/billing-portal will return somewhere to go - a minted provider portal, or the partner's own account page. False for a comped member and for Offline, both of which have an active subscription and no billing to manage. |
billed_by |
string | gateway (billed here), external (billed by a connected system), manual (comped), none (no subscription). |
source |
string | Raw source slug, e.g. stripe, woocommerce. For logic, prefer billed_by. |
source_label |
string | The system's own spelling, for display: WooCommerce, not Woocommerce. |
manage_url |
string | Where an externally-billed member manages their billing. May be empty - a source with nowhere to send them. Show the explanation without a link; a wrong link is worse than none. |
reason |
string | Member-facing, already translated. Non-empty whenever a control is missing. |
Two rules that make the difference between a correct client and a plausible one:
- If a control is hidden, show
reason. Silence reads as a broken screen. The string is the same sentence the endpoint would return if the member forced the request, so the explanation and the refusal cannot describe different rules. - Never infer one key from another.
can_update_paymentis the clearest case: Offline isbilled_by: gatewayand still has no portal, sobilled_by === 'gateway'is not a substitute.
Member plans vs labels
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET, POST | /labels |
Mixed (GET public / POST admin) | List member labels; create a label. |
| GET, PUT, DELETE | /labels/{id} |
Mixed (GET public / PUT, DELETE admin manage_options) |
Get, update, or delete a label. |
| GET | /users/{user_id}/labels |
Public | Get a user's labels. |
| POST, DELETE | /users/{user_id}/labels/{slug} |
Admin (manage_options) |
Assign / unassign a label to a user. |
Admin member search
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /admin/member-search |
Admin (manage_options) |
Member lookup for the wp-admin "Add order" screen. Query: q (required). A search-and-return-matches picker rather than a <select> of every member, which would ship tens of megabytes of markup on a large community. |
Analytics
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /analytics/overview |
Admin | Site DAU/WAU/MAU + growth. |
| GET | /analytics/content/top |
Admin | Top content by engagement. |
| GET | /analytics/members/top |
Admin | Top members by activity. |
| GET | /analytics/spaces/{space_id}/health |
Admin | Space health metrics. |
| GET | /analytics/me/profile-views |
Logged in | Current user's own profile-view data. |
| GET | /analytics/users/{user_id}/profile-views |
Admin | Any user's profile-view data. |
| GET | /analytics/cohorts |
Admin | Retention cohort data. |
| GET | /analytics/funnel |
Admin | Conversion-funnel data. |
Drip sequences
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET, POST | /drip-sequences |
Admin | List drip sequences; create one. |
| GET, PUT, DELETE | /drip-sequences/{id} |
Admin | Get, update, delete a drip sequence. |
| POST | /drip-sequences/{id}/steps |
Admin | Add a step to a sequence. |
| PUT | /drip-sequences/{id}/steps/{index} |
Admin | Update a step at a given index. |
| POST | /drip-sequences/{id}/enroll |
Admin | Enroll a user in a sequence. |
Broadcasts
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET, POST | /broadcasts |
Admin | List broadcast campaigns; create one. |
| GET, PUT, DELETE | /broadcasts/{id} |
Admin | Get, update, or delete a broadcast. |
| POST | /broadcasts/{id}/dispatch |
Admin | Send a broadcast now. |
| POST | /broadcasts/{id}/cancel |
Admin | Cancel a broadcast that is queued or mid-send. |
| GET | /broadcasts/{id}/stats |
Admin | Delivery / engagement stats for a broadcast. |
| GET | /broadcasts/{id}/preview |
Admin | Render a preview of the broadcast. |
| POST | /broadcasts/{id}/test-send |
Admin | Send a test copy of the broadcast. |
Email preferences
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET, POST | /me/email-preferences |
Logged in | Read and update the current user's email unsubscribe settings. |
Moderation rules and AI
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET, POST | /mod-rules |
Admin | List moderation rules; create one. |
| GET, PUT, DELETE | /mod-rules/{id} |
Admin | Get, update, delete a rule. |
| POST | /mod-rules/{id}/toggle |
Admin | Toggle a rule's enabled state. |
| GET | /mod-rules/defaults |
Admin | List the built-in default rule definitions. |
| PUT | /mod-rules/defaults/{id} |
Admin | Update a built-in default rule (id is a slug). |
| POST | /moderation/bulk |
Admin | Run a bulk moderation action. |
| GET | /moderation/bulk/{batch_id} |
Admin | Poll the status of a bulk moderation batch. |
| POST | /ai/classify |
Admin | Classify content (moderation signal). |
| POST | /ai/reply-suggestions |
Commenter | AI smart-reply suggestions for a thread. |
Scheduled posts
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST, DELETE | /posts/{id}/schedule |
Post owner | Schedule a post; DELETE unschedules it. |
| GET | /me/scheduled-posts |
Logged in | Current user's scheduled posts. |
| GET | /posts/scheduled |
Admin | All scheduled posts across the site. |
Push notifications
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET, POST | /me/push-tokens |
Logged in | List and register the current user's device tokens. |
| DELETE | /me/push-tokens/{id} |
Logged in | Delete a push token. |
| POST | /me/push-tokens/test |
Admin | Send a test push notification. |
| GET, PUT | /me/push-prefs |
Logged in | Read and update push notification preferences. |
Saved searches
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET, POST | /me/saved-searches |
Logged in | List and create saved searches. |
| GET, PUT, DELETE | /me/saved-searches/{id} |
Logged in | Get, update, or delete a saved search. |
| POST | /me/saved-searches/{id}/run |
Logged in | Execute a saved search. |
Profile field conditional logic
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /profile-conditions |
Public | Every field's show/hide rule (field_id, field_key, condition {key, kind, op, values}, summary). Rules that no longer apply are left out for visitors; administrators also get them with a problem. Read-only; rules are edited in the Profile Fields screen. See Conditional Logic for Profile Fields. |
Member portfolio
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /members/{id}/portfolio |
Public | Read a member's aggregated portfolio (Suite/Controllers/PortfolioController). |
Learnomy course links
Course-space linking for the Learnomy integration (Integrations/Learnomy/LearnomyLinkController). Registered only when Learnomy is active.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /learnomy-link/spaces |
Logged in | List spaces available to link a course to. |
| GET, POST | /learnomy-link |
Logged in | List existing course-space links; create a link. |
| POST | /learnomy-link/create |
Course creator | Create a linked course (requires create capability). |
| DELETE | /learnomy-link/{space} |
Logged in | Remove the course link for a space. |
Eventonomy bridge: events
The member/space Events surface for the native app (Integrations/Eventonomy/EventsRestController), mirroring what the profile/space Events tabs render server-side so web and app draw the same buckets from the same EventBuckets source. BN-namespaced (buddynext-pro/v1) deliberately, not eventonomy-pro's BuddyPress-only eventonomy/v1/member-events route, which never registers without BuddyPress. Registered only when Eventonomy is active. Routes are public at the permission layer; privacy is enforced inside EventBuckets by viewer id (owner-only buckets self-hide for other viewers), matching the tab behaviour.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /events/member/{id} |
Public (privacy in handler) | A member's event buckets (upcoming/past, hosting/attending). |
| GET | /events/space/{id} |
Public (privacy in handler) | A space's linked events. |
| GET | /events/{id}/rsvp |
Public | Read the caller's RSVP state for one event. |
| GET | /events/headcounts |
Public | Batch RSVP headcounts for a set of event ids. Query: ids (required). |
| POST | /spaces/{space_id}/events/link |
Logged in | Link an existing event to a space. |
| POST | /spaces/{space_id}/events/{event_id}/unbind |
Logged in | Remove an event's link to a space. |
Realtime and payment webhooks
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /realtime/auth |
Logged in (+ per-channel check) | Mint a realtime channel auth signature. See open-but-signed above. |
| POST | /realtime/test-connection |
Admin | Test the realtime connection. |
| POST | /stripe/membership-webhook |
Public (signature-verified) | Membership gateway Stripe webhook receiver (Payments/Gateways/Stripe/StripeGateway). See open-but-signed above. |
| POST | /paypal/membership-webhook |
Public (signature-verified) | Membership gateway PayPal webhook receiver (Payments/Gateways/PayPal/PayPalGateway). |
Example: create a checkout session
curl -X POST https://example.com/wp-json/buddynext-pro/v1/membership/checkout \
-H 'Content-Type: application/json' \
-H 'X-WP-Nonce: <wp_rest nonce>' \
--cookie 'wordpress_logged_in_...=...' \
-d '{ "plan_id": 12, "gateway": "stripe" }'
The handler (Membership/CheckoutController::handle_checkout) takes a required plan_id (plus optional gateway, mode, coupon, country) and returns a gateway checkout URL for the current user to redirect to. After payment, the gateway calls back into its membership webhook (POST /stripe/membership-webhook or POST /paypal/membership-webhook), which verifies the signature and updates the user's subscription. The user can later open the billing portal with POST /me/billing-portal.
Notes
- Mixed-permission routes.
/tiers,/tiers/{id}, and/labelsregister more than one method with different gates - the GET read is public or member-facing, the write (POST/PUT/DELETE) is admin. Treat the "Auth" column as per-method. - Pro requires Free. These routes only register when Pro is active, and they read Free data (spaces, posts, follows, analytics tables) through Free services. The namespaces stay separate: Free is
buddynext/v1, Pro isbuddynext-pro/v1. - Webhook secret is a setup precondition.
/stripe/membership-webhookreturnsstripe_webhook_secret_missinguntil the Stripe webhook secret is configured in the membership settings - that is required setup, not a fault.