Skip to content

REST API Controllers Reference

WP Sell Services overviewFree + ProGet WP Sell Services →

WP Sell Services registers 25 REST controllers plus a set of generic utility routes. WP Sell Services Pro adds 9 more. Everything lives under one namespace:

/wp-json/wpss/v1/

Every route below is generated from the 1.4.0 source. Path parameters are shown as WordPress route regex ((?P<id>[\d]+)) so you can match them exactly.

For authentication, pagination, error shapes, and the generic endpoints, see REST API Overview.

POST/PUT/PATCH means the route is registered as EDITABLE, so WordPress accepts all three verbs on it.

Two namespaces – and only one of them is the API

Section titled “Two namespaces – and only one of them is the API”

There are two namespaces, and the split is not the one the names suggest.

Namespace What is on it
wpss/v1 Everything. All 25 free controllers and all 10 Pro controllers. Pro extends the API; it does not run a parallel one.
wpss-pro/v1 Exactly four cart-adapter routes, and only while the matching cart plugin is active.

The four wpss-pro/v1 routes are:

Method Route Present when
GET /wpss-pro/v1/fluentcart/products FluentCart is active
GET /wpss-pro/v1/fluentcart/orders FluentCart is active

Build every client against wpss/v1. Prefixing a Pro endpoint with wpss-pro/v1 returns rest_no_route on every single one.

Two things change which routes exist on a given site, so enumerate rather than assume:

  • The active e-commerce rail. /payments/* – in free and Pro – registers only when wpss_uses_standalone_payments() is true, i.e. no cart plugin has claimed payments. Activate WooCommerce, EDD or FluentCart and those routes stop registering entirely; a call to them answers 404 rest_no_route from WordPress core. That is by design: when a cart plugin is enabled it owns all payment, and the plugin does not offer a second way in.
  • Pro + a valid license. The 10 Pro controllers register on wpss_loaded.

To see the truth for a site, read the index: GET /wp-json/wpss/v1.

Registered by the API bootstrap rather than a dedicated controller.

Method Route Purpose
GET / Namespace index – the WordPress-generated route list for wpss/v1. The authoritative answer to “what exists on this site”.
GET /categories Service categories
GET /tags Service tags
GET /settings Public marketplace settings
GET /me Current user summary
GET /dashboard Dashboard payload for the current user
GET /search Cross-entity search
POST /batch Batch several reads into one request
POST /tour/complete Mark the guided onboarding tour finished for the current user. Called by the bundled tour; persists per-user so the tour does not replay.
Method Route
GET, POST /services
GET /services/grid
GET /services/limits
GET, POST/PUT/PATCH, DELETE /services/(?P<id>[\d]+)
GET /services/(?P<id>[\d]+)/packages
GET /services/(?P<id>[\d]+)/faqs
GET /services/(?P<id>[\d]+)/reviews
GET, POST /services/(?P<id>[\d]+)/addons
POST/PUT/PATCH, DELETE /services/(?P<id>[\d]+)/addons/(?P<addon_id>[\d]+)

/services/grid returns the lighter payload used by the catalog grid. Prefer it over /services when you only need cards.

/services/limits returns the ceilings a single service may not exceed, so a client can build its create-service form without discovering each one by being rejected. Public read; no authentication needed.

{
"max_packages": 3,
"max_gallery": 4,
"max_videos": 1,
"max_extras": 3,
"max_faq": 5,
"max_requirements": 5
}

-1 means unlimited. The numbers above are the Free defaults; Pro raises max_gallery, max_extras, max_faq and max_requirements to -1 and leaves max_packages and max_videos where they are. Each value passes through its own wpss_service_max_* filter, so a site can set its own ceilings - read the route rather than hard-coding these numbers.

Method Route
GET /orders
GET, POST/PUT/PATCH /orders/(?P<id>[\d]+)
GET, POST /orders/(?P<id>[\d]+)/messages
GET, POST /orders/(?P<id>[\d]+)/deliverables
GET /orders/(?P<id>[\d]+)/files/(?P<file_id>[A-Za-z0-9\-]+)
POST /orders/(?P<id>[\d]+)/(?P<action>…)
GET, POST /orders/(?P<id>[\d]+)/requirements
POST /orders/(?P<id>[\d]+)/requirements/skip
DELETE /orders/(?P<id>[\d]+)/requirements/files/(?P<file_id>[\d]+)
GET /orders/(?P<id>[\d]+)/sub-orders
GET /orders/(?P<id>[\d]+)/receipts
GET /orders/(?P<id>[\d]+)/timeline
POST /orders/(?P<id>[\d]+)/pay

Order transitions all go through one action route rather than a verb per transition. (?P<action>…) accepts exactly:

start | deliver | complete | revision | cancel | dispute
hold | resume | accept-cancellation | reject-cancellation

Changed in 1.4.0: accept and reject were removed. They had no handler behind them and returned a misleading success on some paths, so a client built against them was never actually transitioning anything. There is no replacement – an order is accepted by being paid. deliver now routes through DeliveryService rather than writing the status directly, so a delivery made over REST produces the same records and notifications as one made in the dashboard.

Terminal window
curl -X POST https://yoursite.com/wp-json/wpss/v1/orders/42/deliver \
-H "X-WP-Nonce: $NONCE"

/sub-orders lists the milestone, tip, and extension sub-orders attached to a parent order – see Sub-Order Pattern.

GET /orders/{id}/timeline (new in 1.4.0) returns the merged, chronological event history for one order – status transitions, deliveries, revisions, milestone and extension events, and payment events – as a single list. It is what an order-detail screen renders instead of stitching four endpoints together.

POST /orders/{id}/pay and POST /milestones/{id}/pay do not take money. They resolve where the buyer must go to pay, and enforce the milestone lock-step guard while doing so.

POST /orders/{id}/pay – 200:

{
"success": true,
"order_id": 42,
"checkout_url": "https://yoursite.com/checkout/order-pay/1042/?key=wc_order_…",
"platform": "milestone"
}

platform is the sub-order type when the row is one (milestone, tip, extension) and an empty string for a normal order.

POST /milestones/{id}/pay – 200:

{
"success": true,
"milestone_id": 57,
"checkout_url": "https://yoursite.com/checkout/order-pay/1043/?key=wc_order_…"
}

Errors on both:

Status Code When
404 wpss_order_not_found / wpss_milestone_not_found No such row, or the row is not a milestone
409 wpss_order_not_payable / wpss_milestone_not_payable The row is not in pending_payment
409 wpss_milestone_locked An earlier phase is still open

checkout_url is a BROWSER url, not an API endpoint

Section titled “checkout_url is a BROWSER url, not an API endpoint”

This is the single most common way to get this wrong. checkout_url is a page for a human, resolved through the wpss_ensure_pay_order filter by whatever e-commerce rail is active. Do not fetch it, do not parse it, do not reconstruct it. A native client must open it in a webview (or the system browser) and watch for the return URL.

On the WooCommerce rail it is a WooCommerce order-pay page (/checkout/order-pay/{wc_id}/?key=…), rendered by WooCommerce with whatever gateways the store has enabled. There is no JSON behind it.

Side effect on Woo: generating this URL creates a real WooCommerce order. Pro’s WCPayOrderResolver creates (or reuses) an unpaid WC order for the amount owed so the link survives an email with no cart session. It is idempotent – the WC order id is stored on the WPSS row as wc_pay_order_id and reused while the order still needs payment – but calling /pay speculatively on a site with Woo active still puts a pending order in the store. Call it when the buyer is actually about to pay.

On the standalone rail it is …/checkout/?pay_order={id} and nothing is created.

On the FluentCart rail it is a FluentCart checkout URL for an order that rail minted, with the same side effect and the same idempotence: Pro’s FluentCartPayOrderResolver creates or reuses one FluentCart order carrying a single fee line. FluentCart is a beta rail, off until wpss_pro_beta_rails returns true.

EDD has no pay-order rail. It hooks neither half of the seam, so wpss_can_pay_single_order() is false and checkout_url is an empty string – there is no Pay button to offer. See Pay-order support by rail for the one support table.

Method Route
GET, POST /orders/(?P<order_id>[\d]+)/milestones
GET, DELETE /milestones/(?P<id>[\d]+)
POST /milestones/(?P<id>[\d]+)/pay
POST /milestones/(?P<id>[\d]+)/submit
POST /milestones/(?P<id>[\d]+)/approve
POST /milestones/(?P<id>[\d]+)/decline
POST /milestones/(?P<id>[\d]+)/request-revision

The terminal actions are approve and decline (not “reject”) – the same vocabulary as the milestone hooks.

request-revision is the buyer’s third option on a submitted phase: it takes a reason, returns the phase to revision_requested so the seller can resubmit, and posts the reason into the parent contract’s conversation. A phase has no conversation of its own. Only the buyer may call it, and only while the phase is pending_approval.

Method Route
GET, POST /orders/(?P<order_id>[\d]+)/extensions
POST /orders/(?P<order_id>[\d]+)/extension
POST /extensions/(?P<id>[\d]+)/decline
GET, POST /orders/(?P<order_id>[\d]+)/tip
GET /vendors/(?P<vendor_id>[\d]+)/tips
GET /vendors/(?P<vendor_id>[\d]+)/tips/total
Method Route
GET /vendors
GET /vendors/(?P<id>[\d]+)
GET, POST/PUT/PATCH /vendors/me
POST/PUT/PATCH /vendors/me/vacation
GET /vendors/(?P<id>[\d]+)/services
GET /vendors/(?P<id>[\d]+)/reviews
GET /vendors/(?P<id>[\d]+)/stats
POST /vendors/register
Method Route
GET /seller-levels
GET /seller-levels/(?P<level>[a-z_]+)
GET /vendors/me/level
GET /vendors/(?P<vendor_id>[\d]+)/level
Method Route
GET /vendors/(?P<vendor_id>[\d]+)/portfolio
GET, POST /portfolio
GET, POST/PUT/PATCH, DELETE /portfolio/(?P<id>[\d]+)
POST /portfolio/(?P<id>[\d]+)/featured
POST /portfolio/reorder
Method Route
GET, POST /reviews
GET, POST/PUT/PATCH, DELETE /reviews/(?P<id>[\d]+)
POST /orders/(?P<order_id>[\d]+)/review
POST /reviews/(?P<id>[\d]+)/reply
POST /reviews/(?P<id>[\d]+)/helpful
GET /services/(?P<service_id>[\d]+)/reviews/summary
GET /vendors/(?P<vendor_id>[\d]+)/reviews/summary
Method Route
GET, POST /buyer-requests
GET /buyer-requests/mine
GET, POST/PUT/PATCH, DELETE /buyer-requests/(?P<id>[\d]+)
GET, POST /buyer-requests/(?P<id>[\d]+)/proposals
POST /buyer-requests/(?P<id>[\d]+)/proposals/(?P<proposal_id>[\d]+)/accept
POST /buyer-requests/(?P<id>[\d]+)/proposals/(?P<proposal_id>[\d]+)/reject
GET, POST /proposals
GET, POST/PUT/PATCH /proposals/(?P<id>[\d]+)
POST /proposals/(?P<id>[\d]+)/withdraw
GET /proposals/stats

Request attachments. PUT /buyer-requests/{id} accepts attachments, an array of media IDs uploaded with POST /media and context=request. The array replaces the list; leave the field out to keep the attachments as they are. While the request has no proposal, a file left out of the list is deleted. Once any proposal exists, files already on the request stay whatever the array says, new IDs are added, and DELETE /media/{id} on such a file answers 409 wpss_file_in_use.

Method Route
GET /conversations
GET /conversations/(?P<id>[\d]+)
GET, POST /conversations/(?P<id>[\d]+)/messages
POST /conversations/(?P<id>[\d]+)/read
GET /conversations/unread-count
GET /orders/(?P<order_id>[\d]+)/conversation
POST /orders/(?P<order_id>[\d]+)/conversation/messages
Method Route
GET /disputes
GET /disputes/(?P<id>[\d]+)
GET, POST /orders/(?P<order_id>[\d]+)/dispute
POST /disputes/(?P<id>[\d]+)/respond
GET, POST /disputes/(?P<id>[\d]+)/evidence
GET /disputes/(?P<id>[\d]+)/timeline
POST /disputes/(?P<id>[\d]+)/escalate
POST /disputes/(?P<id>[\d]+)/cancel
POST /disputes/(?P<id>[\d]+)/resolve
POST /disputes/(?P<id>[\d]+)/assign
GET /disputes/options

resolve and assign require dispute-management capability. See Admin Mediation.

Method Route
GET /earnings/summary
GET /earnings/history
GET /wallet/transactions
GET, POST /withdrawals
POST/PUT/PATCH /withdrawals/(?P<id>[\d]+)
DELETE /withdrawals/(?P<id>[\d]+) (vendor cancels their own pending request)
GET, PUT /withdrawals/profile (the vendor’s payout profile)
GET /withdrawals/methods
Method Route
GET /payments/methods
POST /payments/create-intent
POST /payments/confirm

These are the standalone checkout payment routes shipped in free. Pro replaces them with a wider, gateway-specific set – see Payments (Pro).

POST /payments/create-intent takes service_id, package_id (stable id or legacy index), quantity and addons (ids, or {id, quantity, option, text} objects), or pay_order for an existing order, plus gateway. It is priced on the server exactly like the web checkout - package, quantity, add-ons and tax - and never reads a price from the request.

These routes do not exist on every site. As of 1.4.0 the whole controller is skipped unless wpss_uses_standalone_payments() is true (src/API/PaymentController.php). With WooCommerce, EDD, FluentCart or FluentCart enabled, that rail owns all payment and these routes are never registered – a client calling them gets 404 rest_no_route. Do not treat that as an error to retry: check GET /wpss/v1 (or GET /settings) and send the buyer to the rail’s own checkout instead.

Switching rails never rewrites past orders – an order paid through a gateway keeps its record, and that gateway’s webhooks keep working.

Method Route
GET /cart
POST /cart/add
DELETE /cart/(?P<item_key>[a-z0-9]+)
POST /cart/checkout

Cart items are addressed by item_key, not by service id – one service can appear more than once with different packages and add-ons.

GET /cart prices every line the way checkout will charge it. Since 1.8.0 each line also carries unavailable (boolean) and unavailable_reason (string). A line is unavailable when its service was deleted, unpublished or paused after it was added. Such a line is returned at 0 and is left out of subtotal, tax and total; show the reason and offer to remove it.

Authentication ships in the free plugin, not Pro.

Method Route
POST /auth/login
POST /auth/register
POST /auth/logout
GET /auth/me
POST /auth/forgot-password
POST /auth/change-password
GET, POST /auth/devices
DELETE /auth/devices/(?P<device_id>[a-zA-Z0-9_-]+)
GET, DELETE /auth/sessions
DELETE /auth/sessions/(?P<uuid>[a-zA-Z0-9\-]+)

/auth/devices registers a device for push notifications – what a mobile client calls after login.

/auth/sessions is a different thing and deliberately a separate route: it lists and revokes the app tokens a member holds. DELETE /auth/sessions ends them all; the {uuid} form ends one. They are not on /auth/devices because that route already means push tokens.

Favorites, media, notifications, moderation, audit log

Section titled “Favorites, media, notifications, moderation, audit log”
Method Route
GET /favorites
POST, DELETE /favorites/(?P<service_id>[\d]+)
GET /services/(?P<service_id>[\d]+)/favorited
GET, POST /media
GET, DELETE /media/(?P<id>[\d]+)
GET /notifications
GET /notifications/unread-count
POST /notifications/(?P<id>[\d]+)/read
POST /notifications/read-all
DELETE /notifications/(?P<id>[\d]+)
GET /moderation/pending
GET /moderation/count
GET /moderation/(?P<service_id>[\d]+)
POST /moderation/(?P<service_id>[\d]+)/approve
POST /moderation/(?P<service_id>[\d]+)/reject
GET /audit-log

Both ship in the free plugin. A store submitting a marketplace app needs them: App Store Guideline 1.2 asks for a way to report content and a way to block the person who posted it.

Method Route
GET, POST /reports
POST /reports/(?P<id>[\d]+)/resolve
GET /blocks
POST, DELETE /blocks/(?P<user_id>[\d]+)

POST /reports files a report against any target type; the vocabulary of target types is filterable, so the same controller serves services, orders, vendors and messages. GET /reports is the owner’s queue and is admin-only. On the website, members reach POST /reports through the Report link on the service page and the vendor profile; see Member Reports.

Reporting asks the owner to act. Blocking lets a member act immediately, which is the one that actually ends a bad interaction, so the two are separate routes rather than one moderation surface. Blocks are stored in user meta, and enforcement lives at the points where two members can reach each other rather than in the controller.

Method Route
POST /realtime/auth

Private-channel authorization for the realtime (WebSocket) layer. The plugin speaks the Pusher protocol, so this works with Pusher.com or any self-hosted Pusher-compatible server (e.g. Soketi). Client connection settings (key, host, cluster, port, TLS) are exposed under the realtime key of GET /settings; the app secret never leaves the server.

Authorize a private-channel subscription per the Pusher auth contract. Called automatically by the bundled wpss-realtime.js client (with the X-WP-Nonce header); external clients can call it with any supported authentication method.

Authentication Required: Yes (logged-in user)

Parameters:

  • socket_id (string, required) - Pusher socket ID of the connecting client (format 123.456)
  • channel_name (string, required) - Private channel to subscribe to

Allowed channels:

  • private-wpss-user-{ID} - Only the user themself
  • private-wpss-order-{ID} - The order’s customer, its vendor, or administrators

Response (200):

{
"auth": "app_key:hmac_sha256_signature"
}

Errors:

  • 401 rest_not_logged_in - Not authenticated
  • 403 wpss_realtime_forbidden - Channel not owned by the current user (any channel outside the two shapes above is also refused)
  • 404 wpss_realtime_disabled - Realtime is not enabled/configured on this site

Example:

Terminal window
curl -X POST \
https://yoursite.com/wp-json/wpss/v1/realtime/auth \
-u "username:xxxx xxxx xxxx xxxx" \
-d "socket_id=1234.5678" \
-d "channel_name=private-wpss-user-45"

Events published by the plugin:

  • notification.created on private-wpss-user-{ID} - payload { id, type }
  • message.created on private-wpss-order-{ID} and the recipient’s private-wpss-user-{ID} - payload { order_id, sender_id, message_id, excerpt }

Available only when WP Sell Services Pro is active with a valid license.

Method Route
GET /payments/methods
POST /payments/stripe/create-intent
POST /payments/stripe/confirm
POST /payments/paypal/create-order
POST /payments/paypal/capture
POST /payments/razorpay/create-order
POST /payments/razorpay/verify
POST /payments/offline/submit
GET /payments/(?P<order_id>[\d]+)/status
Method Route
GET /wallet/balance
GET /wallet/transactions
POST /wallet/withdraw
GET /wallet/withdrawals
GET /wallet/providers
Method Route
POST /stripe-connect/onboard
GET /stripe-connect/status
POST /stripe-connect/disconnect
GET /stripe-connect/accounts
GET /stripe-connect/accounts/(?P<vendor_id>[\d]+)
GET, POST/PUT/PATCH /stripe-connect/settings
Method Route
GET, POST /paypal-payouts/batches
GET /paypal-payouts/batches/(?P<id>[\d]+)
POST /paypal-payouts/batches/(?P<id>[\d]+)/sync
GET /paypal-payouts/pending
GET, POST/PUT/PATCH /paypal-payouts/profile

/pending is what an owner exports to pay vendors manually; sync reconciles a submitted batch back against the wallet ledger.

Method Route
GET, POST /commission-rules
GET /commission-rules/preview
GET, POST/PUT/PATCH, DELETE /commission-rules/(?P<id>[\d]+)

preview resolves which rule would apply to a given vendor/category/amount without persisting anything – use it to explain a rate in your own UI.

Method Route
GET, POST /subscription-plans
GET, POST/PUT/PATCH, DELETE /subscription-plans/(?P<id>[\d]+)
GET /subscription-plans/my-subscription
POST, DELETE /subscription-plans/subscribe
Method Route
GET, POST /recurring-services
GET /recurring-services/(?P<id>[\d]+)
GET /recurring-services/my-subscriptions
GET /recurring-services/vendor-subscriptions
POST /recurring-services/(?P<id>[\d]+)/cancel
POST /recurring-services/(?P<id>[\d]+)/pause
POST /recurring-services/(?P<id>[\d]+)/resume

Recurring services sit behind a default-off feature flag in 1.3.0. The routes register, but the feature’s UI is hidden until you opt in – see Recurring Services.

Method Route Scope
GET /analytics/overview Marketplace-wide – admin only
GET /analytics/revenue Marketplace-wide – admin only
GET /analytics/vendor/overview The calling vendor
GET /analytics/vendor/revenue The calling vendor
GET /analytics/vendor/orders The calling vendor
GET /analytics/vendor/services The calling vendor
POST /analytics/export Queue a data export

The two un-prefixed routes (/analytics/overview, /analytics/revenue) are the platform-owner numbers and were previously undocumented; the /vendor/* ones are scoped to whoever is calling. /analytics/export is POST, not GET – it starts an export rather than returning one.

/analytics/revenue takes period, one of 7days | 30days (default) | 90days | 12months.

Both admin routes register even on an unlicensed site, deliberately: an unlicensed call answers 403 wpss_pro_license_required rather than 404 rest_no_route, so a client can tell “you need a license” apart from “this build does not have that endpoint”. Anonymous callers get 401 rest_not_logged_in; a logged-in non-admin gets 403.

Method Route
GET, POST/PUT/PATCH /white-label

Returns the active branding (name, logo, colors) so a headless or mobile client can render the same identity as the site.