Abilities API
WP Sell Services registers 12 abilities through the WordPress Abilities API, and Pro adds 3 more. They give AI agents, MCP clients, and any Abilities-aware tool a described, permission-checked way to work with your marketplace -- browse services, place and manage orders, message, review, and read earnings -- without that client needing to learn the REST surface.
Every ability is registered with an input schema, an output schema, an execute callback, and its own permission check.
They are a facade over the REST API, not a second implementation. Each
execute callback builds a WP_REST_Request against the plugin's own
wpss/v1 routes and dispatches it internally. That has three consequences worth
knowing:
- Behaviour cannot drift between an ability and its REST route -- there is one code path.
- The route's own permission callback still runs, on top of the ability's. An ability whose permission check passes can still be refused by the endpoint.
- Any filter you have added to a REST route applies to the ability too, for free.
Requirements
The Abilities API is part of WordPress 6.9 and later. WP Sell Services
supports WordPress 6.4+, so on 6.4-6.8 the registrar detects that
wp_register_ability() is absent and registers nothing. No error, no notice:
the marketplace works exactly as before, and the abilities simply are not there.
To check on a given site:
wp eval 'var_dump( function_exists( "wp_register_ability" ) );'
wp eval 'print_r( array_keys( wp_get_abilities() ) );'
The category
All abilities live under one category:
| Slug | Label |
|---|---|
wpss-marketplace |
Service Marketplace |
Registered on wp_abilities_api_categories_init; the abilities themselves on
wp_abilities_api_init.
Free abilities
All are exposed to REST (show_in_rest) and none are marked destructive.
| Ability | Permission | Read-only | Input |
|---|---|---|---|
wpss/browse-services |
Public | Yes | search, category, min_price, max_price, sort_by, page, per_page |
wpss/view-service |
Public | Yes | id |
wpss/create-service |
wpss_manage_services |
No | title, description, category, packages[] |
wpss/manage-orders |
Logged in | No | action, order_id, status, page, per_page |
wpss/send-message |
Logged in | No | order_id, message |
wpss/view-earnings |
wpss_manage_services |
Yes | period |
wpss/request-withdrawal |
wpss_manage_services |
No | amount, method |
wpss/post-buyer-request |
Logged in | No | title, description, budget, category, deadline |
wpss/submit-proposal |
wpss_respond_to_requests |
No | request_id, price, delivery_days, cover_letter |
wpss/leave-review |
Logged in | No | order_id, rating, comment |
wpss/view-notifications |
Logged in | Yes | unread_only, page, per_page |
wpss/manage-favorites |
Logged in | No | action, service_id |
wpss/browse-services and wpss/view-service are the only two with a permission
callback that returns true unconditionally -- they expose the same public
catalog a visitor can already see. Everything else requires at least a logged-in
user.
wpss/manage-orders and wpss/manage-favorites take an action enum rather
than splitting into one ability per verb, which keeps the surface small for
clients enumerating what they can do.
Pro abilities [PRO]
Registered only when Pro is active with a valid license -- an expired license means Pro loads nothing, so these disappear along with the rest of Pro.
| Ability | Permission | Read-only | Input |
|---|---|---|---|
wpss/analytics-overview |
manage_options or wpss_view_analytics |
Yes | period, metrics[] |
wpss/manage-vendor-subscriptions |
manage_options |
Yes | action, plan_id, vendor_id |
wpss/configure-stripe-connect |
manage_options |
Yes | action |
Consuming them
REST
Abilities are discoverable and runnable under the core namespace:
/wp-json/wp-abilities/v1/abilities
/wp-json/wp-abilities/v1/abilities/wpss/browse-services
Note this is wp-abilities/v1, WordPress's namespace -- not the plugin's
wpss/v1. The two are separate surfaces over the same services.
A client sees only the abilities whose permission callback passes for the authenticated user, so an anonymous request lists the two public ones and a vendor's token lists considerably more.
JavaScript
Use @wordpress/abilities rather than hand-rolling fetches, so permission
filtering and schema validation come for free:
import { store as abilitiesStore } from '@wordpress/abilities';
Choosing abilities or REST
They are complementary, not alternatives:
- Abilities are self-describing. A client can enumerate what exists, read the input schema, and call it without prior knowledge of your API. That is what makes them useful to an AI agent.
- REST is finer-grained and complete. Every route in REST API Controllers is available; abilities cover the twelve highest-value marketplace actions.
Build a normal integration on REST. Reach for abilities when the caller is an agent, an MCP server, or anything that has to discover capability at runtime.
Adding your own
Register on the same hook and reuse the existing category so your ability appears alongside the marketplace's:
add_action( 'wp_abilities_api_init', function () {
if ( ! function_exists( 'wp_register_ability' ) ) {
return; // WordPress < 6.9.
}
wp_register_ability( 'my-plugin/export-orders', array(
'label' => __( 'Export Orders', 'my-plugin' ),
'description' => __( 'Export marketplace orders as CSV for a date range.', 'my-plugin' ),
'category' => 'wpss-marketplace',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'from' => array( 'type' => 'string', 'format' => 'date' ),
'to' => array( 'type' => 'string', 'format' => 'date' ),
),
),
'output_schema' => array( 'type' => 'object' ),
'execute_callback' => 'my_plugin_export_orders',
'permission_callback' => static function (): bool {
return current_user_can( 'wpss_manage_orders' );
},
'meta' => array(
'annotations' => array(
'readonly' => true,
'destructive' => false,
),
'show_in_rest' => true,
),
) );
} );
Two things to get right:
- Gate on a real capability. Your permission callback is the only thing standing between an agent and the action. Reuse the plugin's capabilities -- see Capabilities & Roles.
- Annotate honestly.
readonlyanddestructiveare how a client decides whether to confirm with a human first. Marking a write as read-only invites an agent to call it unattended.
Troubleshooting
| Symptom | Cause |
|---|---|
No wpss/* abilities at all |
WordPress below 6.9, so registration self-skips |
| Pro abilities missing | Pro inactive or its license invalid |
| Ability listed but a client cannot call it | Either the ability's permission callback, or the underlying REST route's, returned false for that user |
| Yours does not appear | Registered on the wrong hook, or show_in_rest not set |
| Changes not reflected | Object cache -- flush and retry |