The 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.
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, not buddynext/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-webhook and /paypal/membership-webhook register permission_callback => __return_true and are authorised entirely by verifying the provider’s signature on the payload. /realtime/auth is 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.
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.
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. Body: slug and name (required), description, sort_order, annual_price, annual_enabled.
GET, DELETE
/tiers/{id}
Public (GET) / Admin (DELETE)
Get a plan; delete a plan.
GET
/membership/plans
Public
List purchasable plans. Since 1.2.4 each plan also carries the viewer’s state: owned (they have access now, including a cancelled plan until it ends), access_until (UTC ISO 8601, null for lifetime or not owned) and can_purchase (buyable and not owned). Logged out, owned is always false.
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). Buying a plan the member still has (active, trialing, past due, or cancelled before its end date) returns 409 already_has_access with data.access_until; the one exception is switching between a plan’s monthly and annual prices while still billing.
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.
POST
/users/{id}/subscriptions
Admin
Grant a member a plan (a comp). Body: tier_id (required), expires_at. Goes through the same subscription-creation path as a paid checkout with source manual.
POST
/users/{id}/subscriptions/{sub_id}/cancel
Admin
Cancel one of a member’s subscriptions.
GET
/me/invoices
Logged in
The caller’s billing history, newest first. Query: cursor, per_page (default 20, max 50). Returns { items, next_cursor, total }.
GET
/me/invoices/{id}
Logged in
One of the caller’s invoices with the amount, tax and discount lines; 404 bnpro_invoice_not_found for anyone else’s.
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_payment is the clearest case: Offline is
billed_by: gateway and still has no portal, so billed_by === 'gateway' is not a substitute.
Space typeahead for the plan editor’s “Spaces this plan unlocks” field. Query: q (required, at least 2 characters); returns at most 10 rows.
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.
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.
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. Query: q.
GET, POST
/learnomy-link
Logged in
Read the link for a course or cohort (query type and id, both required); create a link (body bn_space_id, type, id, all required).
POST
/learnomy-link/create
Course creator
Create a new private community space named after the course or Learnomy Space, link it and backfill the current members (body type, id; 409 when already linked; requires the create capability).
DELETE
/learnomy-link/{space}
Logged in
Remove the course link for a space. The caller must be able to manage that space.
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.
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.
Mixed-permission routes./tiers, /tiers/{id}, and /labels register 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 is buddynext-pro/v1.
Webhook secret is a setup precondition./stripe/membership-webhook returns stripe_webhook_secret_missing until the Stripe webhook secret is configured in the membership settings - that is required setup, not a fault.