{"slug":"wcm-abilities-api","title":"wcm-abilities-api","summary":"WooCommerce Memberships 1.29+ WordPress Abilities API","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-16T14:52:20.678374Z","repo":{"url":"https://github.com/Lonsdale201/wp-agent-skills","stars":22,"forks":2,"license":"MIT","updatedAt":"2026-09-26T23:03:36Z"},"bodyHtml":"<hr>\n<h2>name: wcm-abilities-api\ndescription: WooCommerce Memberships 1.29+ WordPress Abilities API\nreference for membership plan, user membership, and per-post content\nrestriction rule abilities, category slugs, registration requirements,\npermissions, schemas, annotations, REST route exposure, and safe\nautomation guardrails. Use when code or a task mentions\nwp_register_ability, wp_get_ability, WP Abilities API,\nwoocommerce-memberships/plans-create, plans-delete, plans-get,\nplans-list, user-memberships-create, user-memberships-delete,\nuser-memberships-get, user-memberships-list,\npost-restriction-rules-get, post-restriction-rules-update,\n/wc-memberships/v1/post-restriction-rules, or privileged\nagent/headless/admin automation for WooCommerce Memberships.\nmetadata:\nwp-skills-author: \"Soczó Kristóf\"\nwp-skills-contact: \"mailto:lonsdale201@hotmail.com\"\nwp-skills-plugin: \"woocommerce-memberships\"\nwp-skills-plugin-version-tested: \"1.29.0\"\nwp-skills-php-min: \"7.4\"\nwp-skills-last-updated: \"2026-07-06\"</h2>\n<h1>WooCommerce Memberships: Abilities API</h1>\n<p>Use this when building or reviewing privileged automation around Memberships plans, user memberships, and per-post content restriction rules through the WordPress Abilities API.</p>\n<h2>Misconception this skill corrects</h2>\n<blockquote>\n<p>\"Memberships abilities are customer-facing REST endpoints for headless member dashboards.\"</p>\n</blockquote>\n<p>They are privileged Abilities API operations. Plan and user-membership abilities check <code>manage_woocommerce</code>. The 1.29.0 post-restriction abilities check <code>manage_woocommerce_membership_plans</code>; the GET ability also reaches the trait's numeric <code>edit_post</code> check because its input is the post ID. The UPDATE ability input is an object/array, so add your own <code>edit_post</code> guard when wrapping it. Use these abilities for admin/editor/agent automation, not untrusted frontend flows.</p>\n<h2>When to use this skill</h2>\n<p>Trigger when ANY of the following is true:</p>\n<ul>\n<li>The task mentions WordPress Abilities API, <code>wp_register_ability()</code>, <code>wp_get_ability()</code>, <code>wp_abilities_api_init</code>, or agent automation for Memberships.</li>\n<li>Code contains ability names beginning with <code>woocommerce-memberships/</code>.</li>\n<li>Code needs to create/list/get/delete Memberships plans or user memberships through an ability layer instead of direct PHP APIs.</li>\n<li>Code reads or writes <code>/wc-memberships/v1/post-restriction-rules/{id}</code> or uses the block editor Memberships sidebar restriction entity.</li>\n<li>You are deciding whether to use Memberships REST API, PHP APIs, or Abilities API.</li>\n</ul>\n<h2>Registration facts</h2>\n<p>Memberships 1.28.0+ implements the SkyVerge framework <code>HasAbilitiesContract</code> in <code>WC_Memberships</code>. The framework initializes ability registration only when WordPress exposes both:</p>\n<pre><code>function_exists( 'wp_register_ability' )\nfunction_exists( 'wp_register_ability_category' )\n</code></pre>\n<p>On supported WordPress versions, the framework hooks:</p>\n<table>\n<thead>\n<tr>\n<th>Hook</th>\n<th>Purpose</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>wp_abilities_api_categories_init</code></td>\n<td>Registers Memberships ability categories.</td>\n</tr>\n<tr>\n<td><code>wp_abilities_api_init</code></td>\n<td>Registers the abilities.</td>\n</tr>\n<tr>\n<td><code>rest_api_init</code></td>\n<td>Registers framework REST routes only for abilities with explicit <code>RestConfig</code>. In 1.29.0 this applies to the post-restriction rule GET/PUT abilities.</td>\n</tr>\n</tbody>\n</table>\n<p>Do not assume these abilities exist on older WordPress installs. In WP 7.0+ contexts, they should be available if Memberships is loaded and no site-level code disables the Abilities API.</p>\n<h2>Categories</h2>\n<table>\n<thead>\n<tr>\n<th>Category slug</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>woocommerce-membership-plans</code></td>\n<td>Abilities related to <code>WC_Memberships_Membership_Plan</code>.</td>\n</tr>\n<tr>\n<td><code>woocommerce-user-memberships</code></td>\n<td>Abilities related to <code>WC_Memberships_User_Membership</code>.</td>\n</tr>\n<tr>\n<td><code>woocommerce-memberships-posts</code></td>\n<td>Abilities related to per-post membership restriction configuration.</td>\n</tr>\n</tbody>\n</table>\n<h2>Ability map</h2>\n<table>\n<thead>\n<tr>\n<th>Ability</th>\n<th>Class</th>\n<th>Permission</th>\n<th>Annotation</th>\n<th>Input</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>woocommerce-memberships/plans-create</code></td>\n<td><code>CreatePlan</code></td>\n<td><code>manage_woocommerce</code></td>\n<td>write, non-destructive, non-idempotent</td>\n<td>Plan object data.</td>\n</tr>\n<tr>\n<td><code>woocommerce-memberships/plans-delete</code></td>\n<td><code>DeletePlan</code></td>\n<td><code>manage_woocommerce</code></td>\n<td>destructive</td>\n<td>Integer plan ID.</td>\n</tr>\n<tr>\n<td><code>woocommerce-memberships/plans-get</code></td>\n<td><code>GetPlan</code></td>\n<td><code>manage_woocommerce</code></td>\n<td>readonly, idempotent</td>\n<td>Integer plan ID.</td>\n</tr>\n<tr>\n<td><code>woocommerce-memberships/plans-list</code></td>\n<td><code>ListPlans</code></td>\n<td><code>manage_woocommerce</code></td>\n<td>readonly, idempotent</td>\n<td>WP_Query-like args for plans.</td>\n</tr>\n<tr>\n<td><code>woocommerce-memberships/user-memberships-create</code></td>\n<td><code>CreateUserMembership</code></td>\n<td><code>manage_woocommerce</code></td>\n<td>write, non-destructive, non-idempotent</td>\n<td><code>plan_id</code>, <code>user_id</code>, optional <code>product_id</code>, <code>order_id</code>.</td>\n</tr>\n<tr>\n<td><code>woocommerce-memberships/user-memberships-delete</code></td>\n<td><code>DeleteUserMembership</code></td>\n<td><code>manage_woocommerce</code></td>\n<td>destructive</td>\n<td>Integer user membership ID.</td>\n</tr>\n<tr>\n<td><code>woocommerce-memberships/user-memberships-get</code></td>\n<td><code>GetUserMembership</code></td>\n<td><code>manage_woocommerce</code></td>\n<td>readonly, idempotent</td>\n<td>Integer user membership ID.</td>\n</tr>\n<tr>\n<td><code>woocommerce-memberships/user-memberships-list</code></td>\n<td><code>ListUserMemberships</code></td>\n<td><code>manage_woocommerce</code></td>\n<td>readonly, idempotent</td>\n<td><code>user_id</code>, optional <code>status</code>.</td>\n</tr>\n<tr>\n<td><code>woocommerce-memberships/post-restriction-rules-get</code></td>\n<td><code>GetPostRestrictionRules</code></td>\n<td><code>manage_woocommerce_membership_plans</code> plus <code>edit_post</code> for direct integer input</td>\n<td>readonly, idempotent</td>\n<td>Integer post ID.</td>\n</tr>\n<tr>\n<td><code>woocommerce-memberships/post-restriction-rules-update</code></td>\n<td><code>UpdatePostRestrictionRules</code></td>\n<td><code>manage_woocommerce_membership_plans</code> in source; add <code>edit_post</code> in wrappers</td>\n<td>write, non-destructive, idempotent</td>\n<td>Object with <code>id</code> and replacement <code>rules</code>.</td>\n</tr>\n</tbody>\n</table>\n<p>Output schemas use the plugin object JSON schemas:</p>\n<ul>\n<li><code>WC_Memberships_Membership_Plan::getJsonSchema()</code></li>\n<li><code>WC_Memberships_User_Membership::getJsonSchema()</code></li>\n<li><code>PostRestrictionRulesSerializer::getJsonSchema()</code></li>\n</ul>\n<h2>Plan creation input</h2>\n<p><code>plans-create</code> delegates to <code>wc_memberships()-&gt;get_plans_instance()-&gt;createPlan( $data )</code>.</p>\n<p>Important input groups:</p>\n<table>\n<thead>\n<tr>\n<th>Input</th>\n<th>Notes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>name</code></td>\n<td>Required by schema.</td>\n</tr>\n<tr>\n<td><code>slug</code></td>\n<td>Optional plan slug.</td>\n</tr>\n<tr>\n<td><code>status</code></td>\n<td><code>draft</code> or <code>publish</code>.</td>\n</tr>\n<tr>\n<td><code>description</code></td>\n<td>Optional description.</td>\n</tr>\n<tr>\n<td><code>access.method</code></td>\n<td><code>manual-only</code>, <code>signup</code>, or <code>purchase</code>.</td>\n</tr>\n<tr>\n<td><code>access.product_ids</code></td>\n<td>Required by business rules when method is <code>purchase</code>.</td>\n</tr>\n<tr>\n<td><code>membership_length.type</code></td>\n<td><code>unlimited</code>, <code>specific</code>, or <code>fixed</code>.</td>\n</tr>\n<tr>\n<td><code>membership_length.amount</code> / <code>period</code></td>\n<td>Required by business rules for <code>specific</code>.</td>\n</tr>\n<tr>\n<td><code>membership_length.start_date</code> / <code>end_date</code></td>\n<td>Required by business rules for <code>fixed</code>.</td>\n</tr>\n<tr>\n<td><code>rules.content_restriction</code></td>\n<td>Plan content restriction rules.</td>\n</tr>\n<tr>\n<td><code>rules.product_restriction</code></td>\n<td>Product view/purchase restriction rules.</td>\n</tr>\n<tr>\n<td><code>rules.purchasing_discount</code></td>\n<td>Member discount rules.</td>\n</tr>\n</tbody>\n</table>\n<p>Do not write the <code>wc_memberships_rules</code> option directly when an ability or plan API can create the plan and rules together.</p>\n<h2>User membership creation input</h2>\n<p><code>user-memberships-create</code> delegates to Memberships's user membership manager:</p>\n<pre><code>wc_memberships()-&gt;get_user_memberships_instance()-&gt;create_user_membership( $data );\n</code></pre>\n<p>Schema fields:</p>\n<table>\n<thead>\n<tr>\n<th>Input</th>\n<th>Notes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>plan_id</code></td>\n<td>Required membership plan ID.</td>\n</tr>\n<tr>\n<td><code>user_id</code></td>\n<td>Required WP user ID.</td>\n</tr>\n<tr>\n<td><code>product_id</code></td>\n<td>Optional product that granted access.</td>\n</tr>\n<tr>\n<td><code>order_id</code></td>\n<td>Optional order that granted access.</td>\n</tr>\n</tbody>\n</table>\n<p>For purchase-based access, prefer passing meaningful <code>product_id</code> and <code>order_id</code> when the membership is truly tied to a purchase. Do not fake order/product relations just to satisfy reporting.</p>\n<h2>Post restriction rule abilities</h2>\n<p>Memberships 1.29.0 added Abilities API operations for the block-editor Memberships sidebar. They are configuration APIs for restrictable posts, not runtime access checks.</p>\n<table>\n<thead>\n<tr>\n<th>Need</th>\n<th>Ability</th>\n<th>REST route</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Read rules applying to a post</td>\n<td><code>woocommerce-memberships/post-restriction-rules-get</code></td>\n<td><code>GET /wc-memberships/v1/post-restriction-rules/{id}</code></td>\n</tr>\n<tr>\n<td>Replace post-specific rules</td>\n<td><code>woocommerce-memberships/post-restriction-rules-update</code></td>\n<td><code>PUT /wc-memberships/v1/post-restriction-rules/{id}</code></td>\n</tr>\n</tbody>\n</table>\n<p><code>post-restriction-rules-get</code> returns:</p>\n<pre><code>array(\n    'id'    =&gt; 123,\n    'rules' =&gt; array(\n        array(\n            'id'                 =&gt; 'rule-id',\n            'membership_plan_id' =&gt; 456,\n            'access_schedule'    =&gt; array( 'type' =&gt; 'immediate' ),\n            'editable'           =&gt; true,\n        ),\n    ),\n);\n</code></pre>\n<p>The response includes both post-specific rules and inherited rules from post-type/taxonomy level configuration. The <code>editable</code> flag is the safety boundary:</p>\n<ul>\n<li><code>editable === true</code>: rule targets this post directly and can be sent to the update ability.</li>\n<li><code>editable === false</code>: inherited rule; render read-only and edit it on the membership plan/source rule, not from the post payload.</li>\n</ul>\n<p><code>post-restriction-rules-update</code> treats <code>rules</code> as the full desired state for post-specific content restriction rules:</p>\n<ul>\n<li>Existing direct rules omitted from the payload are deleted.</li>\n<li>Rows with a known direct rule <code>id</code> are updated.</li>\n<li>Rows without <code>id</code> are added.</li>\n<li><code>rules: array()</code> clears all direct post-specific rules.</li>\n<li>Inherited rules are not affected, and sending an inherited rule ID causes a <code>422 invalid_input</code> because the ID does not belong to this post.</li>\n</ul>\n<p>Safe PHP execution shape:</p>\n<pre><code>$get = wp_get_ability( 'woocommerce-memberships/post-restriction-rules-get' );\n$current = $get ? $get-&gt;execute( $post_id ) : null;\n\nif ( is_wp_error( $current ) || ! is_array( $current ) ) {\n    return $current;\n}\n\n$editable_rules = array_values( array_filter(\n    $current['rules'],\n    static fn( array $rule ): bool =&gt; ! empty( $rule['editable'] )\n) );\n\n$editable_rules[] = array(\n    'membership_plan_id' =&gt; $plan_id,\n    'access_schedule'    =&gt; array( 'type' =&gt; 'delayed', 'amount' =&gt; 7, 'period' =&gt; 'days' ),\n);\n\nif ( ! current_user_can( 'edit_post', $post_id ) ) {\n    return new WP_Error( 'forbidden', 'Cannot edit this post.', array( 'status' =&gt; 403 ) );\n}\n\n$update = wp_get_ability( 'woocommerce-memberships/post-restriction-rules-update' );\n$result = $update ? $update-&gt;execute( array(\n    'id'    =&gt; $post_id,\n    'rules' =&gt; $editable_rules,\n) ) : new WP_Error( 'missing_ability' );\n</code></pre>\n<p>The update rule schema accepts <code>membership_plan_id</code> and optional <code>access_schedule</code>. Delayed schedules use <code>type = delayed</code>, positive <code>amount</code>, and <code>period</code> in <code>days</code>, <code>weeks</code>, <code>months</code>, or <code>years</code>; immediate schedules use <code>array( 'type' =&gt; 'immediate' )</code>.</p>\n<p>The block editor sidebar also registers REST-exposed post meta for <code>_wc_memberships_force_public</code> and the per-post custom restriction message keys. Those meta writes are separate from rule replacement; use the abilities above for rule rows and normal post meta/REST for the sidebar's force-public/message settings.</p>\n<h2>Safe execution pattern</h2>\n<pre><code>$ability = function_exists( 'wp_get_ability' )\n    ? wp_get_ability( 'woocommerce-memberships/user-memberships-get' )\n    : null;\n\nif ( ! $ability || ! current_user_can( 'manage_woocommerce' ) ) {\n    return new WP_Error( 'forbidden', 'Membership ability is unavailable.', array( 'status' =&gt; 403 ) );\n}\n\n$result = $ability-&gt;execute( 123 );\n\nif ( is_wp_error( $result ) ) {\n    return $result;\n}\n</code></pre>\n<p>Let the ability permission callback run; the explicit <code>current_user_can()</code> guard is useful when your code is about to choose between an admin path and a frontend-safe path.</p>\n<h2>Choosing the right surface</h2>\n<table>\n<thead>\n<tr>\n<th>Need</th>\n<th>Prefer</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Admin/agent automation on WP 7.0+</td>\n<td>Abilities API.</td>\n</tr>\n<tr>\n<td>External integration over HTTP with Woo auth</td>\n<td>Memberships REST API.</td>\n</tr>\n<tr>\n<td>In-process plugin business logic</td>\n<td>Public PHP APIs and objects.</td>\n</tr>\n<tr>\n<td>Customer frontend/headless \"my memberships\"</td>\n<td>Custom endpoint that checks ownership and uses Memberships access APIs.</td>\n</tr>\n<tr>\n<td>Public member directory</td>\n<td><code>/wc/v4/memberships/members/directory</code>, with page/block validation and privacy-limited fields.</td>\n</tr>\n<tr>\n<td>Block-editor per-post restriction UI</td>\n<td><code>post-restriction-rules-get/update</code> abilities or their <code>/wc-memberships/v1/post-restriction-rules/{id}</code> routes.</td>\n</tr>\n</tbody>\n</table>\n<p>Do not use <code>manage_woocommerce</code> abilities for a customer-facing dashboard. A customer should not be able to list arbitrary users' memberships or delete plans.</p>\n<h2>Security guardrails</h2>\n<ul>\n<li>Never proxy ability execution from a public REST route without a capability check.</li>\n<li>Do not pass arbitrary frontend-controlled WP_Query args into <code>plans-list</code>; even though the ability is admin-gated, sanitize UI inputs before execution.</li>\n<li>Treat delete abilities as destructive and require an explicit admin confirmation in UI.</li>\n<li>Do not down-scope permission by filtering current user capabilities. Build a narrower custom endpoint/service when customers need self-service membership data.</li>\n<li>Do not assume every ability has a REST route. Plan and user-membership abilities pass <code>showInRest = true</code> for Abilities API metadata but do not provide the SkyVerge framework <code>RestConfig</code>; the post restriction rule abilities do.</li>\n<li>For post rule updates, never round-trip inherited rows from GET into PUT. Filter to <code>editable === true</code> and intentionally rebuild the direct post-specific rule set.</li>\n<li>The framework's route permission callback is invoked without request input before execution. <code>WP_Ability::execute()</code> passes validated input to the permission callback, but the update input is an array, not a numeric ID, so the shared trait's <code>edit_post</code> branch does not fire there in 1.29.0. Add an explicit <code>current_user_can( 'edit_post', $post_id )</code> check before custom update wrappers.</li>\n</ul>\n<h2>Common mistakes</h2>\n<pre><code>// WRONG: exposing a privileged ability to any logged-in user.\nregister_rest_route( 'my/v1', '/membership', array(\n    'methods'             =&gt; 'POST',\n    'permission_callback' =&gt; 'is_user_logged_in',\n    'callback'            =&gt; function ( WP_REST_Request $request ) {\n        return wp_get_ability( 'woocommerce-memberships/user-memberships-delete' )-&gt;execute( (int) $request['id'] );\n    },\n) );\n\n// RIGHT: use capability checks for privileged automation.\nregister_rest_route( 'my/v1', '/admin/membership', array(\n    'methods'             =&gt; 'POST',\n    'permission_callback' =&gt; static fn() =&gt; current_user_can( 'manage_woocommerce' ),\n    'callback'            =&gt; function ( WP_REST_Request $request ) {\n        $ability = wp_get_ability( 'woocommerce-memberships/user-memberships-get' );\n        return $ability ? $ability-&gt;execute( (int) $request['id'] ) : new WP_Error( 'missing_ability' );\n    },\n) );\n</code></pre>\n<h2>Cross-references</h2>\n<ul>\n<li>Use <code>wcm-membership-hooks</code> for lifecycle hooks, REST/webhooks, profile fields, member directory, CSV, and Subscriptions-linked memberships.</li>\n<li>Use <code>wcm-data-model-subscriptions-link</code> for CPT names, meta keys, rule storage, and Subscriptions relation storage.</li>\n<li>Use <code>wcm-access-discounts</code> for access checks, restriction/drip behavior, and member discount APIs.</li>\n</ul>\n<h2>References</h2>\n<ul>\n<li>Verified source paths:\n<ul>\n<li><code>wp-content/plugins/woocommerce-memberships/class-wc-memberships.php</code></li>\n<li><code>wp-content/plugins/woocommerce-memberships/src/Abilities/Provider.php</code></li>\n<li><code>wp-content/plugins/woocommerce-memberships/src/Plans/Abilities/</code></li>\n<li><code>wp-content/plugins/woocommerce-memberships/src/UserMemberships/Abilities/</code></li>\n<li><code>wp-content/plugins/woocommerce-memberships/src/Posts/Abilities/</code></li>\n<li><code>wp-content/plugins/woocommerce-memberships/src/Posts/Actions/SetPostRules.php</code></li>\n<li><code>wp-content/plugins/woocommerce-memberships/src/Posts/Adapters/JsonSerializers/PostRestrictionRulesSerializer.php</code></li>\n<li><code>wp-content/plugins/woocommerce-memberships/src/Posts/Traits/CanCheckRestrictablePostPermissionTrait.php</code></li>\n<li><code>wp-content/plugins/woocommerce-memberships/src/Blocks/BlockEditorSidebar.php</code></li>\n<li><code>wp-content/plugins/woocommerce-memberships/vendor/skyverge/wc-plugin-framework/woocommerce/Abilities/</code></li>\n</ul>\n</li>\n</ul>\n","files":[{"path":"SKILL.md","sizeBytes":15544,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-16T14:58:35.240477Z","sha256":"77DC6023406F2D05EC485FEBB418AE0C0B34AA380A916FF313C38A43D2D5705E","sizeBytes":5135},"review":null,"source":{"repositoryUrl":"https://github.com/Lonsdale201/wp-agent-skills","path":"woocommerce/wcm-abilities-api","license":"MIT","commit":"c51b571a259f0c4b5f5c0a3bc50ed580c6851f98","subtreeSha":"9494E2A764CCBFBEED767BA5E58026318A315F7267AFA8572C557E1AEDC49156","lastSyncedAt":"2026-09-29T23:33:03.303675Z"},"reviewedAt":"2026-09-16T15:19:08.446067Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/woocommerce/wcm-abilities-api"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart"},{"target":"git","command":"git clone https://github.com/Lonsdale201/wp-agent-skills.git"}]}