cartpresets/v1.
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.
searchalso looks inside a preset: product and variation names, SKUs, and custom item names.sorttakesname,status,items,price,revenue,ordersorpublish, withdirasascordesc.filterstakes the fieldsprice,revenue,items,orders,dateandproduct, among others. It takes at most 8 rows, and at most 3 of them fromproduct,product_nameandstock_problemtogether, the rows that look inside a preset’s items. More is a 400 saying which limit it passed. The same limits apply to a bulkqueryand 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.
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_minorqty_maxover 9,999, or one that isn’t a whole number; - a
custom_priceordisc_valuethat isn’t a finite number of at most 1,000,000,000; - a
variation_idthat isn’t a variation of the item’sproduct_id.
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. Sendexpectwith 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 indata.total.
{ 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 }:
importedlists{ index, id, name }for each preset written.skipped_presetslists{ index, name, errors }for each one that wasn’t, witherrorskeyed by field path like a failed save’sdata.fields.indexis the preset’s position in the document, from 0.stoppedistruewhen 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 inskipped_presets. At least one preset is always imported.
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 assimple,variable,subscriptionorvariable-subscription.subscription:nullunless the product bills as a subscription, otherwise{ period, interval, length, trial_period, trial_length, sign_up_fee }. A variable subscription’svariationseach 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 oncartpresets_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.