Skip to main content
Every route CartPresets for WooCommerce registers lives under one namespace: cartpresets/v1.
There are no public endpoints. Every route requires the manage_woocommerce capability and a valid nonce. The routes serve the admin panel. A preset reaches a customer through its link, never through REST.
The capability is filterable through cartpresets_admin_capability, which is safety-clamped. Changing the licence also needs manage_options (see Licensing, below). A validation failure is a 400 with a data.fields object of messages keyed by the field that caused them. A list where text belongs, such as name[]=x or a filter row whose field is an array, is a 400 too.

Presets

Listing parameters

GET /presets accepts search, status, sort, dir, page, per_page (up to 200) and filters. Dates, prices, orders, products and stock problems are all filter rows. A range is two rows, such as price~gt~10|price~lt~50.
  • search also looks inside a preset: product and variation names, SKUs, and custom item names.
  • sort takes name, status, items, price, revenue, orders or publish, with dir as asc or desc.
  • filters takes the fields price, revenue, items, orders, date and product, among others. It takes at most 8 rows, and at most 3 of them from product, product_name and stock_problem together, the rows that look inside a preset’s items. More is a 400 saying which limit it passed. The same limits apply to a bulk query and to export.
cartpresets_list_args documents the full grammar. It receives exactly these arguments once they’re resolved. A revenue sort or filter is Pro. On the free version the route refuses it with a 400 (cartpresets_needs_pro) instead of treating every preset as unsold.
The answer is presets, total and counts. counts holds one number per derived status, plus all, and it’s always the whole store’s: no search, filter or status narrows it.

What a preset carries

Among the rest, each preset in the answer has:

Saving

PUT /presets/<id> replaces the preset. created_at, the publish date, is Pro to change: sent empty, it’s saved as the moment of the save, and left out, it keeps the stored date. On the free version the stored date is kept either way. POST /presets also takes copy_of, the id of a preset to copy. The new preset then starts from that preset’s stored Pro values (the caps, the per-customer limit, the coupon and the unavailable-link settings), with no end date. On the free version those values are kept; on Pro, what you send is written. An id that isn’t a positive integer or doesn’t name a preset is refused with a 400 keyed copy_of. The editor’s Duplicate sends it. POST /presets and PUT /presets/<id> refuse, keyed by the field path (items, items.3):
  • more than 100 items in one preset;
  • a quantity, qty_min or qty_max over 9,999, or one that isn’t a whole number;
  • a custom_price or disc_value that isn’t a finite number of at most 1,000,000,000;
  • a variation_id that isn’t a variation of the item’s product_id.
Names, slugs and notes lose Unicode direction overrides and isolates (U+202A to U+202E, U+2066 to U+2069), zero-width spaces and joiners (U+200B to U+200D) and the byte-order mark (U+FEFF) as they’re saved. An item keeps its ID across a save when the payload names it (id on the item) and it’s still the same type, product and variation. Carts holding the preset find their lines by that ID. An item that doesn’t keep its ID, because it was deleted or its product or variation changed, takes its preset out of any cart that holds it.

Bulk actions

POST /presets/bulk takes an action: activate, deactivate, duplicate, clear_end_date or delete. It names the presets one of two ways:
  • ids: the presets you ticked.
  • query: { search, status, filters }, validated the way the listing validates them, for every preset that matches. This is the listing’s Select all across pages. Send expect with it: the count the merchant confirmed. If the query matches a different number by the time it runs, nothing changes and the answer is a 409 (cartpresets_bulk_changed) with the new count in data.total.
One request acts on at most 500 presets. More is refused whole, with a 400. The answer is { action, affected, matched, refused }, plus created (the new IDs) for duplicate. Copies are inactive. clear_end_date asks the same tier gate a save asks, so it’s Pro. activate and clear_end_date put a link on sale, so each preset first goes through the same validation as a save, as it’s stored. One that would fail isn’t changed and is listed in refused as { id, name, errors }, with errors keyed like a failed save’s data.fields. When nothing could be changed, the answer is a 400 with the same list in data.refused.

Export and import

GET /presets/export exports ids[] when given. Otherwise it exports every preset matching search, status and filters, and with none of those, every preset. There’s no ceiling. Each item carries product_sku and variation_sku beside its IDs. variation_sku is the variation’s own SKU, or an empty string when it has none. POST /presets/import takes document, an export, as an object or a JSON string. A document over 500 presets or 5,000 items is refused whole: a 400 with the reason in data.fields.document, and nothing is written. Otherwise every preset is checked by the same type check, quantity reader and validation as POST /presets before any is written. Products are matched by product_sku, then by ID. A variation is matched by variation_sku and must belong to the matched product. Without a SKU, its old ID is kept only when it’s a variation of that product, and otherwise the preset is skipped with an error on that item. Each valid preset is created inactive, with its slug suffixed if it’s taken. The answer is { created, skipped, imported, skipped_presets, stopped }:
  • imported lists { index, id, name } for each preset written.
  • skipped_presets lists { index, name, errors } for each one that wasn’t, with errors keyed by field path like a failed save’s data.fields.
  • index is the preset’s position in the document, from 0.
  • stopped is true when the import ran out of its time or memory budget and stopped between presets. The budget is half the request’s time limit, up to 20 seconds, and four fifths of PHP’s memory limit. The presets it didn’t reach are in skipped_presets. At least one preset is always imported.
See Import and export.

Settings

GET /settings answers settings, carrier and samples. carrier is null, or { id, name, edit, virtual, hidden }. virtual and hidden are read from the product itself; hidden means its catalogue visibility is hidden. GET /settings/carrier-options takes an optional search (a name, or an exact product ID). It answers products: up to thirty published, virtual, simple products, newest first, each { id, name, virtual, hidden, current }. current marks the carrier now. POST /settings/carrier applies the same test when you choose a product: one that isn’t published, simple and virtual is refused.

Supporting routes

GET /products answers twelve suggestions with no search, and up to thirty matches with one. A search matches a name, a SKU, or a variation’s name or SKU. Each product carries:
  • type: WooCommerce’s own, such as simple, variable, subscription or variable-subscription.
  • subscription: null unless the product bills as a subscription, otherwise { period, interval, length, trial_period, trial_length, sign_up_fee }. A variable subscription’s variations each carry their own.
POST /prefs saves any of fields (the optional columns), per_page (10, 20 or 50), sort (a listing sort key) and dir (asc or desc). A key you leave out keeps its stored value. GET /prefs answers {} until the first save.

Reporting

GET /sales/stats takes ids, a comma-separated list of preset IDs, and answers stats, keyed by ID. Every ID you ask about gets an entry, { orders, revenue }, with zeros for a preset that hasn’t sold. When WooCommerce Analytics’ tables aren’t there to read, each entry is { "orders": 0, "revenue": 0, "unavailable": true }, and the list shows a dash in place of the figure.

Licensing

Changing the licence needs manage_options, an administrator’s capability, on top of the plugin’s own. Without it these routes answer 403. It isn’t filterable. GET /licence answers canManage too, whether the current user may change it. For a user without manage_options, it answers from what’s stored and never asks the licensing service to refresh. POST /licence takes key. If the key has no account attached yet, the answer is a 409 (cartpresets_licence_identity_required) carrying the name and email that activating would send. Nothing is sent until you call again with confirm_identity: true. The Licence card asks the merchant first. See Licence and updates.
A licence key is sent in and never sent back out. GET /licence returns the status, plan, expiry, sites in use and when it was last checked, plus a masked copy showing only the key’s last four characters.

Pro routes on the free version

On the free version the Pro routes aren’t registered at all. A request to /sales or /presets/import returns a plain 404, exactly like a route that never existed. A route that exists and refuses tells an attacker the feature is there. A route that doesn’t exist tells them nothing. It also means the free version ships no premium code to refuse with.

Adding your own routes

Register on cartpresets_register_rest_routes, which fires once the plugin’s own controllers are up. Pro uses the same seam, so nothing about the premium routes is privileged.
Supply your own permission_callback. Registering inside this action doesn’t inherit the plugin’s capability check.
Last modified on September 28, 2026