# WordPress Abilities API for store catalogues in 2026

Register WooCommerce lookups with schemas, permissions and wp-abilities/v1 so admin tools stop inventing AJAX handlers.

- Date: 2026-09-16
- Category: WordPress

The WordPress Abilities API is how I now expose catalogue jobs on a WooCommerce site as named, schema-checked units instead of one more admin-ajax handler. I am Alan Vo, a Gold Coast web developer with 18 years on business sites and shops. In 2026 this is no longer a plugin experiment. WordPress 6.9 shipped the PHP registry. WordPress 7.0 shipped the JavaScript store. Both belong on a production catalogue if you already have custom stock lookups, merchandising reports, or an internal tool that talks to products.

This note is for people who already compose a [WooCommerce product page with blocks](https://alanvo.com/blog/woocommerce-product-page-blocks/) and then reach for `admin-ajax.php` the first time merchandising wants a SKU lookup in another app. It is also for anyone shipping a [WordPress block theme](https://alanvo.com/blog/wordpress-block-themes/) who needs a machine-readable way to run a job that is not a click on the storefront.

## Why the WordPress Abilities API matters on store catalogues

The WordPress Abilities API matters on store catalogues because catalogue work is a set of discrete jobs, and those jobs used to hide in isolated functions that only one plugin knew how to call. A furniture shop needs "get dimensions for this SKU". A footwear shop needs "is this last width in stock". A ticketing site needs "does this venue SKU still sell". Those are not REST collections. They are capabilities with inputs, outputs and a permission check.

WordPress 6.9, documented in the [Abilities API handbook](https://developer.wordpress.org/apis/abilities-api/) and the [6.9 Abilities API note](https://make.wordpress.org/core/2025/11/10/abilities-api-in-wordpress-6-9/), put that contract in core: register a category, register an ability, validate JSON Schema, run a callback. WordPress 7.0 added `@wordpress/abilities` and `@wordpress/core-abilities`, covered in the [7.0 Field Guide](https://make.wordpress.org/core/2026/05/14/wordpress-7-0-field-guide/) and the [client-side Abilities API note](https://make.wordpress.org/core/2026/03/24/client-side-abilities-api-in-wordpress-7-0/). I do not treat that as an AI novelty. I treat it as a replacement for the fifth custom REST route that nobody documented.

The other reason is permission. A WooCommerce REST product endpoint is a collection. An ability is one job with one `permission_callback`. I can let a merchandiser run a read-only stock check without handing them `manage_options`, and refuse a destructive job from someone who should not delete products.

I still use WooCommerce REST and the [WooCommerce CRUD objects](https://developer.woocommerce.com/docs/best-practices/data-management/crud-objects/) for ordinary product writes. Abilities sit on top of those getters. They are not a reason to skip `wc_get_product()`. They are a reason to stop inventing a new URL every time someone asks the catalogue a question.

This is also not the [WordPress Interactivity API](https://alanvo.com/blog/wordpress-interactivity-api/). Directives update the storefront DOM. Bindings fill a paragraph from product data. Abilities are for callers that need a named action with a schema, including admin tools and authenticated automation.

![WordPress Abilities API in the block editor on a laptop](../../images/blog/wordpress-abilities-api-1.jpg)

## How the WordPress Abilities API actually works

The WordPress Abilities API actually works as a central registry of `WP_Ability` objects, plus optional REST under `wp-abilities/v1`, plus a JavaScript store in WordPress 7.0. You do not register on `init`. Categories go on `wp_abilities_api_categories_init` with `wp_register_ability_category()`. Abilities go on `wp_abilities_api_init` with `wp_register_ability()`. Registering off those hooks fails and fires `_doing_it_wrong()`.

An ability name is `namespace/ability-name`. Lowercase letters, numbers, dashes and one slash. I use the theme or mu-plugin slug as the namespace, then an action: `skanvi/get-product-dimensions`, `janet/check-last-width`. Categories are separate slugs such as `catalogue-lookups`. The category must exist before the ability that points at it.

Each ability carries a label, a description, a category, `input_schema`, `output_schema`, `execute_callback` and `permission_callback`. Schemas are a WordPress subset of JSON Schema Version 4. If the job takes input or returns a value, the schema is mandatory. The registry validates both sides. A caller cannot send a random string when you asked for an object with `sku`.

In the execute callback I load the product with [`wc_get_product()`](https://developer.woocommerce.com/docs/code-snippets/useful-functions/) or query with [`wc_get_products()`](https://developer.woocommerce.com/docs/features/products/wc-get-products/). Then I read through CRUD getters (`get_sku()`, `get_stock_status()`, `get_weight()`, attribute getters). I format money with `wc_price()` when the output is a price string. I never print a raw `_price` meta key. If the product is missing I return a `WP_Error`, as the 6.9 note asks, not a PHP warning in the HTML.

To run an ability in PHP I fetch it with `wp_get_ability( 'namespace/name' )` and call `execute()` with the input. That is for other plugins on the same site, not for guests. I use `wp_before_execute_ability` to log who asked, not to rewrite the catalogue.

REST is opt-in. Set `meta.show_in_rest` to `true` when you register. Until you do that, the ability lives in PHP only. Once you opt in, the endpoints are:

- `GET /wp-json/wp-abilities/v1/categories`
- `GET /wp-json/wp-abilities/v1/abilities`
- `GET /wp-json/wp-abilities/v1/abilities/{name}`
- `GET|POST|DELETE /wp-json/wp-abilities/v1/abilities/{name}/run`

Every one of those routes requires an authenticated user. Cookie auth with an `X-WP-Nonce` header is the same-origin path, as in the [REST API authentication handbook](https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/). Application passwords over HTTPS are the external path. I do not put a catalogue ability on a public, unauthenticated URL.

WordPress 7.0 execution from JavaScript uses `executeAbility` from `@wordpress/abilities`. Server abilities loaded by `@wordpress/core-abilities` pick the HTTP method from annotations: `readonly: true` uses GET, `destructive: true` plus `idempotent: true` uses DELETE, everything else uses POST. I mark stock and dimension lookups readonly. Guessing the method is how you cache a write.

![Developer desk with two monitors during WordPress Abilities API work](../../images/blog/wordpress-abilities-api-2.jpg)

Core already enqueues `@wordpress/core-abilities` on admin screens. I do not enqueue that module on the public product page. Fetching every server ability into a guest PDP is not a storefront feature. For mixed hosts I guard with `function_exists( 'wp_register_ability' )` before I hook registration, as the 6.9 note documents.

## Production checklist for catalogue abilities

A catalogue ability is not "it returned JSON on my laptop". Work through this list before it hits a live shop.

1. Confirm WordPress is 6.9 or newer for the PHP registry, and 7.0 if you need the client store, `executeAbility`, or admin discovery. Check `function_exists( 'wp_register_ability' )` if the mu-plugin must load on mixed versions.
2. Register the category on `wp_abilities_api_categories_init` before the ability. Empty category lists are how discoverability dies.
3. Name the ability `namespace/action`. Keep the action a verb plus a noun. Do not reuse a WooCommerce REST slug as the ability name.
4. Write `input_schema` and `output_schema`. If the job takes a SKU, require it. If it returns stock, type the fields. Schema is the documentation agents actually read.
5. Put `permission_callback` on a real WooCommerce capability: `manage_woocommerce` or `edit_products` for writes, a tighter custom cap for read-only lookups if merchandisers should not open wp-admin.
6. Load products with `wc_get_product()` or `wc_get_products()`. Return `WP_Error` when the SKU is missing, private, or not a product.
7. Set `meta.show_in_rest` only on abilities you intend to call over HTTP. Leave internal PHP-only jobs off the REST list.
8. Annotate `readonly`, `destructive` and `idempotent` so WordPress 7.0 picks GET, POST or DELETE correctly.
9. Keep execute callbacks free of current-user HTML. Return data. Let the caller render.
10. Test as a shop manager, a merchandiser, and an application-password user. Then test logged out and confirm 401.

I keep a staging SKU that is draft, a SKU that is out of stock, and a SKU that does not exist. Ability bugs hide in those three.

## What breaks when you expose catalogue abilities

The first break is treating abilities like public storefront JSON. All `wp-abilities/v1` routes need an authenticated user. If you "fix" a 401 by stripping the permission callback, you have published a catalogue RPC. I have seen people paste an application password into a theme script that runs on every PDP. That is a credential leak, not an integration.

![Website wireframes on paper next to a keyboard](../../images/blog/wordpress-abilities-api-3.jpg)

The second break is reading underscore meta instead of CRUD getters. `_price` skips tax display, sale price and currency. The real price block says one thing. Your ability says another. Merchandisers will paste the wrong number into a campaign. Load the product object. Use getters.

Other breaks I have had to unwind: registering on `init` instead of `wp_abilities_api_init`, a missing category, `show_in_rest` left false while a JS client calls `/run`, a readonly ability that still writes, HTML in the callback, `@wordpress/core-abilities` on the public theme, and a cache plugin storing a GET `/run` for the next user. Application passwords in a theme bundle belong in the same bin as a leaked deploy password.

When an ability "does nothing", I check three layers. Is it registered (`wp_has_ability()`)? Does REST list it as that user? Does execute return `WP_Error`, `ability_invalid_input`, `ability_invalid_output` or `ability_permission_denied`? Those client codes are in the 7.0 note. Do not swallow them in a generic toast.

Variable products need an explicit rule. A parent SKU lookup that returns parent stock while add to cart uses the variation is a lie. Put parent versus variation in the input schema.

## How to measure catalogue abilities

Measure whether the returned object matches the product object, not whether an AI demo looked clever. I pick a staging SKU, run the ability as a merchandiser, and compare `get_sku()`, `get_stock_status()` and the dimension attributes to the JSON. Then I change the product in admin and run it again with object cache primed.

I also check the REST method. A readonly lookup should be GET. A stock adjustment should not. I revoke any application password that was pasted into a browser snippet. Logs for `/wp-json/wp-abilities/v1/` should show authenticated traffic from known tools, not anonymous GET storms.

Do not claim a conversion lift from registering an ability. I will not invent one here. The honest win is one registry, one permission check, and fewer mystery AJAX actions when the next contractor opens the theme.

## Related work on this site

Catalogue abilities pay rent where photography and spec copy are the product, and where editors already live in WordPress. The [Skanvi furniture storefront](https://alanvo.com/work/skanvi/) needs dimensions next to the gallery, which is a lookup I would rather expose as an ability than as a one-off route. The [Janet Janet footwear work](https://alanvo.com/work/janet-janet/) needs last width as catalogue data. The [BridgeWorx ticketing site](https://alanvo.com/work/bridgeworx/) treats a venue line the same way. All three were contract development on an agency team. The rule is the same: read the product object, wrap the job, keep add to cart as a Woo block.

![Content editor reviewing a page on a tablet](../../images/blog/wordpress-abilities-api-4.jpg)

## FAQ

### What is the WordPress Abilities API for if I already have WooCommerce REST?

The WordPress Abilities API is for named jobs with a schema and a permission callback, not for replacing the product collection. WooCommerce REST still lists and updates products. An ability answers one question, such as dimensions for a SKU, without inventing a custom controller.

### Does the WordPress Abilities API replace the Block Bindings API?

The WordPress Abilities API does not replace the [WordPress Block Bindings API](https://alanvo.com/blog/wordpress-block-bindings-api/). Bindings fill block attributes from catalogue data on the product template. Abilities are callable units for tools and other plugins. Use both when a PDP must display a fact and an internal app must fetch the same fact.

### Can guests call the WordPress Abilities API from a product page?

Guests cannot call the WordPress Abilities API REST routes. Core requires an authenticated user on every `wp-abilities/v1` endpoint. Storefront clicks belong in the Interactivity API. If a guest needs a fact, print it in the HTML or bind it. Do not ship an application password to the browser.

### Which WordPress version do I need for the WordPress Abilities API on a shop?

You need WordPress 6.9 for the PHP registry and optional REST, and WordPress 7.0 if you want the client-side store, annotations that pick GET or DELETE, and admin discovery through `@wordpress/core-abilities`. Mixed hosts should feature-detect `wp_register_ability` before they hook registration.


![Laptop and coffee while shipping WordPress Abilities API](../../images/blog/wordpress-abilities-api-5.jpg)


HTML version: https://alanvo.com/blog/wordpress-abilities-api/
