The Pro plugin registers its own REST namespace, buddynext-pro/v1, with 63 registered routes (across the controllers under includes/). 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/webhook, /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. Handled in Stripe/WebhookController.
POST
/stripe/membership-webhook
none (public)
The membership gateway’s own Stripe webhook receiver. Signature-verified inside the handler. Handled in Payments/Gateways/Stripe/StripeGateway.
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/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.
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).
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.
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.
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.
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/webhook returns stripe_webhook_secret_missing until the Stripe webhook secret is configured in the membership settings - that is required setup, not a fault.