Run the store from an AI assistant
Connect an AI assistant to your store so it can answer questions and run the admin for you.
Note
This page is the admin side: an assistant working for you, with an API key and your permissions. To let shoppers' AI agents find your products and buy them, see Agentic commerce and the public Storefront MCP endpoint, which needs no key.
The Model Context Protocol (MCP) is how an AI client calls tools on an outside system. grav-mcp is the MCP server for Grav. It connects an assistant to your site over the REST API and picks up KahunaCart's tools automatically.
Before you begin
- Grav 2.0 or later.
- The Grav API plugin 1.0.22 or later, installed and enabled. That release added
GET /mcp/tools. - grav-mcp 0.1.2 or later.
- Node.js 18 or later on the machine running the assistant.
- An API key on an account holding
api.accessplus thekahunacart.*permissions the assistant needs. See REST API to generate one and Permissions and safety to choose which.
How it works
grav-mcp fetches GET /mcp/tools at startup and registers one MCP tool per entry, named kahunacart_<name>: kahunacart_list_products, kahunacart_refund_order. Calling one calls the matching API route, under the same permissions and rules as clicking the button in the admin.
| Piece | Where it lives | What it does |
|---|---|---|
mcp.yaml |
KahunaCart's plugin root | Describes each route as a tool: name, description, method, path, permission, input schema |
GET /mcp/tools |
The API plugin | Serves every enabled plugin's manifest, filtered to what the calling key may see |
| grav-mcp | The machine running the assistant | Registers one MCP tool per entry and calls the API on the assistant's behalf |
Note
Every KahunaCart tool comes from the manifest, so an update that adds a route adds a tool. grav-mcp needs no change.
Connect Claude Code
- Open your Claude Code MCP config.
- Add a
gravserver:
{
"mcpServers": {
"grav": {
"command": "npx",
"args": ["-y", "grav-mcp"],
"env": {
"GRAV_API_URL": "https://mysite.com/api",
"GRAV_API_KEY": "grav_your_api_key_here"
}
}
}
}
- Restart Claude Code.
The assistant now has the core Grav tools and every KahunaCart tool your key can call.
GRAV_API_URL is your site plus /api, without the version prefix. GRAV_API_KEY is the key you generated, starting with grav_.
Warning
The key carries its owner's permissions. Do not commit it or paste it into a shared config file. grav-mcp reads it from the environment and never logs or returns it.
Other MCP clients
Any MCP client that launches a stdio server takes the same command and the same two variables: npx -y grav-mcp, with GRAV_API_URL and GRAV_API_KEY set. Claude Desktop, Cursor and VS Code each keep their MCP configuration somewhere different, so check that client's docs.
To test outside a client, run grav-mcp standalone:
GRAV_API_URL=https://mysite.com/api GRAV_API_KEY=grav_abc123 npx grav-mcp
The same settings are available as CLI flags: --url, --key, --environment, --plugin-tools, --transport and --port. For a remote deployment, use HTTP instead of stdio:
npx grav-mcp --url https://mysite.com/api --key grav_abc123 --transport http --port 3100
Check the connection
- Ask the assistant to run
discover_plugins. - Read the
mcp_toolsblock in the answer. It lists the loaded tools by plugin, andkahunacartshould be one of them. - If your client can show its tool list, look there for names starting with
kahunacart_instead.
When kahunacart is missing, read grav-mcp's stderr. A plugin-tools problem never stops it: grav-mcp prints a warning naming the cause and starts with the core Grav tools only. Troubleshooting lists the causes and fixes.
Fix the cause, then ask the assistant to run refresh_plugin_tools. No restart is needed.
What the assistant can do
KahunaCart publishes 379 tools over 415 admin routes, behind 11 permissions. An assistant calls each one by its name below with kahunacart_ in front of it.
These 36 routes are not published as tools: GET /kahunacart/admin/sections/{id}.js, GET /kahunacart/admin/widget.js, POST /kahunacart/categories/{id}/image, GET /kahunacart/config/export, GET /kahunacart/orders/{id}/receipt.html, GET /kahunacart/address-lookup, GET /kahunacart/import/runs/{id}/errors.csv, POST /kahunacart/import/uploads, POST /kahunacart/notification-channels, DELETE /kahunacart/notification-channels/{id}, PATCH /kahunacart/notification-channels/{id}, POST /kahunacart/notification-channels/{id}/test, POST /kahunacart/object-templates/preview, GET /kahunacart/previews, GET /kahunacart/previews/email/{kind}, GET /kahunacart/previews/pdf/{document}, POST /kahunacart/orders/{id}/downloads, GET /kahunacart/badge, POST /kahunacart/orders/bulk/pdf, GET /kahunacart/orders/{id}/credit-notes/{cnid}.pdf, GET /kahunacart/orders/{id}/packing-slip.pdf, GET /kahunacart/orders/{id}/refund-request/label.pdf, GET /kahunacart/orders/{id}/shipments/{sid}/packing-slip.pdf, POST /kahunacart/products/{id}/files, POST /kahunacart/products/{id}/media, POST /kahunacart/products/{id}/releases/publish, GET /kahunacart/reports/export, GET /kahunacart/reports/export.pdf, GET /kahunacart/export/catalog.yaml, GET /kahunacart/export/customers.csv, GET /kahunacart/export/products.csv, GET /kahunacart/exports/{id}/download, POST /kahunacart/restores/uploads, GET /kahunacart/tax/rates/export, POST /kahunacart/webhook-subscriptions/{id}/ping, GET /kahunacart/zones/widgets/{id}.js.
Your key's permissions decide which of these exist in a session. The routes with no tool are the multipart uploads, the CSV and PDF exports, the two admin script routes and the sidebar badge the admin shell polls for itself. Manifest version 1 does not cover them.
The Arguments column names the properties of each tool's input schema.
| Tool | Route | Permission | Arguments | What it does |
|---|---|---|---|---|
get_config |
GET /kahunacart/config |
kahunacart.orders.view |
none | Read the store-level facts every other call is interpreted against: the currency and how many decimal places it has, the storefront route, the site languages, the running plugin version, the registered payment providers and what each declares it can do, background job health, and whether the enabled payment methods can actually sell the published catalog |
get_store_status |
GET /kahunacart/store/status |
kahunacart.orders.view |
none | Read whether the store is open, coming soon or paused, and the message shoppers see while it is not open |
set_store_status |
POST /kahunacart/store/status |
kahunacart.settings |
status required; message optional |
Set the store to open, coming_soon or paused |
list_admin_sections |
GET /kahunacart/admin/sections |
kahunacart.orders.view |
none | List the extra KahunaCart admin screens contributed by other plugins, each with its id, label, icon and sort position |
list_products |
GET /kahunacart/products |
kahunacart.products.manage |
q, sort, dir, featured, missing_price, cf, page, per_page |
List catalog products, newest added first, with their variants, option axes, category ids, tags, attribute values, primary image and per-language translation overrides |
get_product |
GET /kahunacart/products/{id} |
kahunacart.products.manage |
id, required |
One product with everything on it: variants and their option combinations, option axes, category ids, tags, the attribute value map, the full image gallery, translation overrides, plus its ten most recent completed orders and the total count of them |
create_product |
POST /kahunacart/products |
kahunacart.products.manage |
title required; slug, type_code, type, type_fields, kit_items, sell_as_set, lock_parts, option_prices, variant_attributes, min_qty, max_qty, qty_step, combine_variants, sold_individually, exclude_order_rules, exclude_category_rules, pay_what_you_want, pwyw_suggested_minor, pwyw_max_minor, currency_amounts, buy_now, button_label, badge_text, badge_tone, release_at, brand, item_condition, final_sale, agent_checkout, google_category, manufacturer_id, safety_info, status, visibility, summary, description, tax_class, shipping_class, position, featured, variant, variants, category_ids, customer_group_ids, tags, attributes, translations optional |
Create a product and at least one variant, and return the created product in the same form get_product does |
update_product |
PATCH /kahunacart/products/{id} |
kahunacart.products.manage |
id required; title, slug, type_code, type, type_fields, kit_items, sell_as_set, lock_parts, option_prices, variant_attributes, min_qty, max_qty, qty_step, combine_variants, sold_individually, exclude_order_rules, exclude_category_rules, pay_what_you_want, pwyw_suggested_minor, pwyw_max_minor, currency_amounts, buy_now, button_label, badge_text, badge_tone, release_at, brand, item_condition, final_sale, agent_checkout, google_category, manufacturer_id, safety_info, status, visibility, summary, description, tax_class, shipping_class, position, featured, variants, category_ids, customer_group_ids, tags, attributes, translations optional |
Change one or more fields of a product and return the updated product |
reorder_products |
POST /kahunacart/products/reorder |
kahunacart.products.manage |
ids, required |
Put the catalog in the order the ids are given, and answer with how many rows moved |
delete_product |
DELETE /kahunacart/products/{id} |
kahunacart.products.manage |
id, required |
Delete a product, its variants, its option axes, its attribute values, its releases and file rows, and its images including the bytes on disk |
update_variant |
PATCH /kahunacart/products/{id}/variants/{variantId} |
kahunacart.products.manage |
id, variantId required; price_minor, price, currency_amounts, sku, position, is_default, min_qty, max_qty, qty_step, bundle_of, gtin, mpn, backorder_note, is_digital optional |
Change one variant of a product and return it with the rest of the product's variants beside it |
search_variants |
GET /kahunacart/variants/search |
kahunacart.products.manage |
q, limit, ids |
Find variants by product title, variant title or SKU, across every product whatever its status, for choosing the parts of a bundle |
delete_variant |
DELETE /kahunacart/products/{id}/variants/{variantId} |
kahunacart.products.manage |
id, variantId, all required |
Remove one variant of a product and return the variants that remain |
delete_variants |
POST /kahunacart/products/{id}/variants/delete |
kahunacart.products.manage |
id, ids, all required |
Remove several variants of one product in one call, under the same rules as delete_variant, and return the variants that remain |
list_product_options |
GET /kahunacart/products/{id}/options |
kahunacart.products.manage |
id, required |
The product's option axes with their values, its variants with the option-value combination each of them answers, and the widget names an axis can be rendered with |
create_product_option |
POST /kahunacart/products/{id}/options |
kahunacart.products.manage |
id, name required; kind, required, widget, values, translations optional |
Add an option axis to a product, for example Size or Colour, optionally with its values in the same call, and return the product's axes and variants |
update_product_option |
PATCH /kahunacart/products/{id}/options/{optionId} |
kahunacart.products.manage |
id, optionId required; name, widget, position, translations optional |
Rename an axis, change how it is rendered, move it among the product's other axes, or replace its translation overrides |
delete_product_option |
DELETE /kahunacart/products/{id}/options/{optionId} |
kahunacart.products.manage |
id, optionId, all required |
Remove an axis from a product and return the axes and variants that remain |
create_option_value |
POST /kahunacart/products/{id}/options/{optionId}/values |
kahunacart.products.manage |
id, optionId, label required; color, image_id, translations optional |
Add one value to an option axis, for example Large on Size, and return the product's axes and variants |
update_option_value |
PATCH /kahunacart/products/{id}/options/{optionId}/values/{valueId} |
kahunacart.products.manage |
id, optionId, valueId required; label, color, image_id, position, translations optional |
Relabel a value, change its swatch colour, move it among the axis's other values, or replace its translation overrides |
delete_option_value |
DELETE /kahunacart/products/{id}/options/{optionId}/values/{valueId} |
kahunacart.products.manage |
id, optionId, valueId, all required |
Remove one value from an axis and return the product's axes and variants |
generate_variants |
POST /kahunacart/products/{id}/options/generate |
kahunacart.products.manage |
id required; apply optional |
Work out which combinations of the product's option axes have no variant yet and report the plan: how many combinations there are in total, how many already exist, how many are missing and their labels, plus the orphan variants whose combination the axes no longer describe and the unassigned ones that answer no combination at all |
assign_variant_options |
POST /kahunacart/products/{id}/options/assign/{variantId} |
kahunacart.products.manage |
id, variantId, option_value_ids, all required |
Point one existing variant at one combination of option values, which is how a variant that predates the axes is adopted into the matrix instead of being deleted and retyped |
list_product_files |
GET /kahunacart/products/{id}/files |
kahunacart.products.manage |
id, required |
The downloadable files attached to a product, with their file name, the title and description customers read (null when unset, and the file name shows instead), size, download limit, expiry, position and the release each belongs to, and variant_id when it goes only to one variant's buyers |
update_product_file |
PATCH /kahunacart/products/{id}/files/{fileId} |
kahunacart.products.manage |
id, fileId required; name, title, description, download_limit, expiry_days, position, variant_id optional |
Edit a downloadable's metadata: what the customer's browser saves it as, the title and description customers read for it, how many times it may be downloaded, how long the link lives, where it sits in the list, and which variant's buyers get it (variant_id; null gives it to every buyer of the product, a physical product's included) |
delete_product_file |
DELETE /kahunacart/products/{id}/files/{fileId} |
kahunacart.products.manage |
id, fileId, all required |
Detach a downloadable from its product and delete the bytes on disk |
list_product_media |
GET /kahunacart/products/{id}/media |
kahunacart.products.manage |
id required; role, kind optional |
Everything attached to a product, gallery first and then the samples |
list_product_images |
GET /kahunacart/products/{id}/images |
kahunacart.products.manage |
id, required |
The compatibility view of the above: a product's gallery pictures alone, each as id, stored path, site-relative url, alt text and position |
create_product_embed |
POST /kahunacart/products/{id}/media/embeds |
kahunacart.products.manage |
id, embed_url required; alt, role, poster_media_id optional |
Attach a third-party video by URL |
update_product_media |
PATCH /kahunacart/products/{id}/media/{mediaId} |
kahunacart.products.manage |
id, mediaId required; alt, position, duration_seconds, poster_media_id, embed_url optional |
Change a row's alt text, its place in its list, its recorded duration, its poster, or an embed's URL |
reorder_product_media |
POST /kahunacart/products/{id}/media/reorder |
kahunacart.products.manage |
id, ids required; role optional |
Put a product's gallery, or its samples, in the order of the ids given, in one transaction |
delete_product_media |
DELETE /kahunacart/products/{id}/media/{mediaId} |
kahunacart.products.manage |
id, mediaId, all required |
Detach a row from its product and delete the bytes on disk, where there are any |
list_product_labels |
GET /kahunacart/products/{id}/labels |
kahunacart.products.manage |
id, required |
A product's labels in display order: EU energy labels and image labels such as a warranty badge |
create_product_label |
POST /kahunacart/products/{id}/labels |
kahunacart.products.manage |
id, kind, image_media_id required; energy_class, energy_scale, title, variant_id, position, thumb_media_id, document_media_id optional |
Add an EU energy label or an image label (a warranty badge, a certification) |
update_product_label |
PATCH /kahunacart/products/{id}/labels/{labelId} |
kahunacart.products.manage |
id, labelId required; energy_class, energy_scale, title, variant_id, position, image_media_id, thumb_media_id, document_media_id optional |
Change a label's energy class and scale, title, variant, position or the files it names |
delete_product_label |
DELETE /kahunacart/products/{id}/labels/{labelId} |
kahunacart.products.manage |
id, labelId, all required |
Remove a label from its product, and delete its files from disk unless another label uses them |
list_releases |
GET /kahunacart/products/{id}/releases |
kahunacart.products.manage |
id, required |
A product's versioned releases, newest first, each with its version, release date, Markdown changelog, published flag and the files attached to it, plus the evergreen files that belong to no release and are therefore offered to every buyer |
create_release |
POST /kahunacart/products/{id}/releases |
kahunacart.products.manage |
id, version required; released_at, changelog, published optional |
Cut a new version of a product |
update_release |
PATCH /kahunacart/products/{id}/releases/{releaseId} |
kahunacart.products.manage |
id, releaseId required; version, released_at, changelog, published optional |
Edit a release's version, date, notes or published flag |
delete_release |
DELETE /kahunacart/products/{id}/releases/{releaseId} |
kahunacart.products.manage |
id, releaseId, all required |
Delete a release and the rows of the files on it |
attach_release_file |
POST /kahunacart/products/{id}/releases/{releaseId}/files/{fileId} |
kahunacart.products.manage |
id, releaseId, fileId, all required |
Move a file already attached to the product into this release, which is how a build uploaded before the version was cut gets joined to it |
detach_release_file |
DELETE /kahunacart/products/{id}/releases/{releaseId}/files/{fileId} |
kahunacart.products.manage |
id, releaseId, fileId, all required |
Take a file back out of its release, making it evergreen |
list_categories |
GET /kahunacart/categories |
kahunacart.settings |
none | The whole catalog tree as a flat list, each row carrying its parent_id, position, display mode, picture, translation overrides, the number of products filed directly under it, and the number a shopper finds when they click it including everything in its subtree |
create_category |
POST /kahunacart/categories |
kahunacart.settings |
title required; slug, description, parent_id, position, display, image_alt, min_qty, max_qty, qty_step, translations optional |
Add a category to the tree |
update_category |
PATCH /kahunacart/categories/{id} |
kahunacart.settings |
id required; title, slug, description, parent_id, position, display, image_alt, min_qty, max_qty, qty_step, translations optional |
Change a category's fields |
delete_category |
DELETE /kahunacart/categories/{id} |
kahunacart.settings |
id, required |
Delete a category and its picture |
move_category |
POST /kahunacart/categories/{id}/move |
kahunacart.settings |
id required; parent_id, position optional |
Give a category a new parent, a new place among its siblings, or both |
reorder_category |
POST /kahunacart/categories/{id}/reorder |
kahunacart.settings |
id required; direction optional |
Swap a category with the sibling above or below it |
delete_category_image |
DELETE /kahunacart/categories/{id}/image |
kahunacart.settings |
id, required |
Remove a category's picture, the row and the bytes, and return the category |
list_tags |
GET /kahunacart/tags |
kahunacart.settings |
none | Every catalog tag with its label, slug, translation overrides and how many products wear it |
create_tag |
POST /kahunacart/tags |
kahunacart.settings |
label required; slug, translations optional |
Create a tag |
update_tag |
PATCH /kahunacart/tags/{id} |
kahunacart.settings |
id required; label, slug, translations optional |
Rename a tag, change its slug, or replace its translation overrides |
delete_tag |
DELETE /kahunacart/tags/{id} |
kahunacart.settings |
id, required |
Delete a tag and every assignment of it, and report how many products were carrying it |
list_manufacturers |
GET /kahunacart/manufacturers |
kahunacart.products.manage |
none | Every manufacturer with its name, trade name, address, email, website, EU responsible person, default safety text with its translations, and how many products name it as their own, plus default_manufacturer_id, the store default a product without its own falls back to |
create_manufacturer |
POST /kahunacart/manufacturers |
kahunacart.products.manage |
name required; trade_name, address, email, url, rp_name, rp_address, rp_email, rp_url, safety_info, translations optional |
Create a manufacturer |
update_manufacturer |
PATCH /kahunacart/manufacturers/{id} |
kahunacart.products.manage |
id required; name, trade_name, address, email, url, rp_name, rp_address, rp_email, rp_url, safety_info, translations optional |
Change one or more fields of a manufacturer |
delete_manufacturer |
DELETE /kahunacart/manufacturers/{id} |
kahunacart.products.manage |
id, required |
Delete a manufacturer and take it off every product that named it, and report how many there were (products_affected) and whether it was the store default (was_default) |
list_attributes |
GET /kahunacart/attributes |
kahunacart.products.manage |
none | Every attribute definition this store has, in display order, with its slug, label, type, option list, unit, group heading, filterable, filter_panel and show_on_product flags, translation overrides and how many products answer it, plus the vocabulary lists a create form needs: every type, the types that may be filtered on, and the two types that carry options |
get_attribute |
GET /kahunacart/attributes/{id} |
kahunacart.products.manage |
id, required |
One attribute definition with its option list, translation overrides and product_count |
create_attribute |
POST /kahunacart/attributes |
kahunacart.products.manage |
label, type required; slug, options, unit, group_label, position, filterable, filter_panel, show_on_product, translations optional |
Define a store-wide attribute products can answer |
update_attribute |
PATCH /kahunacart/attributes/{id} |
kahunacart.products.manage |
id required; label, slug, type, options, unit, group_label, position, filterable, filter_panel, show_on_product, translations optional |
Change an attribute definition |
reorder_attributes |
POST /kahunacart/attributes/reorder |
kahunacart.products.manage |
ids, required |
Put the whole attribute list in the order the ids are given, and answer with how many moved and the reordered list |
delete_attribute |
DELETE /kahunacart/attributes/{id} |
kahunacart.products.manage |
id required; force optional |
Delete an attribute definition, every value of it and its translations, and report how many products were carrying a value |
list_tax_zones |
GET /kahunacart/tax/zones |
kahunacart.settings |
none | Every tax zone with its country, region and postcode criteria decoded, its priority, and the rates hanging off it |
create_tax_zone |
POST /kahunacart/tax/zones |
kahunacart.settings |
name required; countries, regions, postcodes, priority, rates optional |
Create a tax zone and its rates in one call |
update_tax_zone |
PATCH /kahunacart/tax/zones/{id} |
kahunacart.settings |
id required; name, countries, regions, postcodes, priority, rates optional |
Change a zone's fields |
delete_tax_zone |
DELETE /kahunacart/tax/zones/{id} |
kahunacart.settings |
id, required |
Delete a tax zone and its rates |
import_eu_vat_presets |
POST /kahunacart/tax/presets/eu-vat |
kahunacart.settings |
none | Create a tax zone and rate for each EU member state at its standard VAT rate, and report how many were actually added |
list_tax_drivers |
GET /kahunacart/tax/engines |
kahunacart.settings |
none | Every tax engine an add-on registered, the store-level default engine code, whether the built-in zone and rate editors still govern anything, and the registered tax-id validators |
bind_tax_driver |
PUT /kahunacart/tax/engines/bind |
kahunacart.settings |
zone_id required; engine optional |
Attach a registered tax engine to one tax zone, so a store can quote a connector for one jurisdiction and its own rates for another from the same install |
list_price_preferences |
GET /kahunacart/tax/price-preferences |
kahunacart.settings |
none | Whether prices contain tax, per region and per currency, plus the published resolution order and the store-wide tax.included at the bottom of it |
create_price_preference |
POST /kahunacart/tax/price-preferences |
kahunacart.settings |
attribute, value, is_tax_inclusive required; currency optional |
Say that one region or one currency is priced inclusive or exclusive of tax |
update_price_preference |
PUT /kahunacart/tax/price-preferences/{id} |
kahunacart.settings |
id required; attribute, value, is_tax_inclusive, currency optional |
Change a preference's scope or its answer |
delete_price_preference |
DELETE /kahunacart/tax/price-preferences/{id} |
kahunacart.settings |
id, required |
Remove a preference |
import_tax_rates_csv |
POST /kahunacart/tax/rates/import |
kahunacart.settings |
csv, required |
Import the 10-column rate table — country_code, state_code, postcode, city, rate, tax_rate_name, priority, compound, shipping, tax_class — as a csv string |
list_shipping_zones |
GET /kahunacart/shipping/zones |
kahunacart.settings |
none | Every shipping zone in the merchant's own order, with its country, region and postcode criteria decoded and the delivery methods it offers |
create_shipping_zone |
POST /kahunacart/shipping/zones |
kahunacart.settings |
name required; countries, regions, postcodes, position, methods optional |
Create a shipping zone and the methods it offers |
update_shipping_zone |
PATCH /kahunacart/shipping/zones/{id} |
kahunacart.settings |
id required; name, countries, regions, postcodes, position, methods optional |
Change a zone's fields |
delete_shipping_zone |
DELETE /kahunacart/shipping/zones/{id} |
kahunacart.settings |
id, required |
Delete a shipping zone and the methods it offered |
get_shipping_rate_rules |
GET /kahunacart/shipping/methods/{id}/rates |
kahunacart.settings |
id, required |
Every rate type on one method — base, per item, per kilogram, percentage, per shipping class — plus the minimum and maximum clamps, the cost expression, the three-valued shipping-class policy and the carrier the method is bound to |
set_shipping_rate_rules |
PUT /kahunacart/shipping/methods/{id}/rates |
kahunacart.settings |
id required; per_item, per_kg, percent_rate, min_total_cost, max_total_cost, cost_expression, class_policy, class_policy_classes, class_costs_mode, carrier_code, carrier_service_code, currency_amounts optional |
Replace the rate rules on one method |
validate_shipping_expression |
POST /kahunacart/shipping/expressions/validate |
kahunacart.settings |
expression, required |
Parse a cost expression without saving it and answer whether it is valid, with the offending token named when it is not |
list_shipping_profiles |
GET /kahunacart/shipping/profiles |
kahunacart.settings |
none | The profiles a store packs by, in assembly order, with how many products point at each |
create_shipping_profile |
POST /kahunacart/shipping/profiles |
kahunacart.settings |
code required; name, position, ships optional |
Add a profile for a part of the catalogue that travels separately |
update_shipping_profile |
PUT /kahunacart/shipping/profiles/{id} |
kahunacart.settings |
id required; code, name, position, ships optional |
Change a profile's code, name, position or whether it ships |
delete_shipping_profile |
DELETE /kahunacart/shipping/profiles/{id} |
kahunacart.settings |
id, required |
Refused while any product still points at the profile, and refused outright for the built-in default and digital profiles, which are what an unassigned product falls back to |
list_shipping_packages |
GET /kahunacart/orders/{id}/packages |
kahunacart.orders.view |
id, required |
The packages one order was priced as: which profile assembled each, which method priced it, what it cost, what it weighed, which lines went in it and any carrier metadata for the method that actually won |
list_carrier_drivers |
GET /kahunacart/shipping/carriers |
kahunacart.settings |
none | The live carrier rate drivers installed in this store, the shipping methods bound to them and whether each binding resolves |
clear_carrier_rate_cache |
POST /kahunacart/shipping/carriers/cache/clear |
kahunacart.settings |
none | Drop every stored carrier quote so the next checkout asks the carriers again |
pickup_locations_list |
GET /kahunacart/pickup-locations |
kahunacart.products.manage |
none | Every collection point in the merchant's own order, enabled or not, with its full address, opening hours, optional collection charge as a decimal string in the store currency, and the language key holding its collection instruction |
pickup_location_create |
POST /kahunacart/pickup-locations |
kahunacart.products.manage |
code, name, country required; address_1, address_2, city, state, postcode, phone, hours, instructions_key, price, currency_amounts, tax_class, enabled, position optional |
Add a collection point |
pickup_location_update |
PUT /kahunacart/pickup-locations/{id} |
kahunacart.products.manage |
id required; code, name, address_1, address_2, city, state, postcode, country, phone, hours, instructions_key, price, currency_amounts, tax_class, enabled, position optional |
Change a collection point's fields |
pickup_location_delete |
DELETE /kahunacart/pickup-locations/{id} |
kahunacart.products.manage |
id, required |
Delete a collection point |
list_coupons |
GET /kahunacart/coupons |
kahunacart.settings |
page, per_page, group_id, id, sort, dir, q |
Discount codes with their type, percentage or amount, minimum and maximum cart totals, schedule, usage limits, how many times each has been redeemed, and the restriction matrix: what each is scoped to, whether it pays the postage, whether it skips items already on sale, how many units it covers, which addresses it is reserved for, and its conditions as a nested conditions document (an empty all when it has none) |
create_coupon |
POST /kahunacart/coupons |
kahunacart.settings |
code required; type, percent, amount, min_total, max_total, currency_amounts, starts_at, expires_at, usage_limit, usage_limit_per_user, enabled, individual_use, basis, product_ids, scopes, free_shipping, exclude_sale_items, limit_to_items, allowed_emails, conditions, grants_customer_group_id, utm_campaign optional |
Create a discount code |
update_coupon |
PATCH /kahunacart/coupons/{id} |
kahunacart.settings |
id required; code, type, percent, amount, min_total, max_total, currency_amounts, starts_at, expires_at, usage_limit, usage_limit_per_user, enabled, individual_use, basis, product_ids, scopes, free_shipping, exclude_sale_items, limit_to_items, allowed_emails, conditions, grants_customer_group_id, utm_campaign optional |
Change a coupon's fields |
delete_coupon |
DELETE /kahunacart/coupons/{id} |
kahunacart.settings |
id, required |
Delete a discount code |
list_coupon_groups |
GET /kahunacart/coupon-groups |
kahunacart.settings |
page, per_page, sort, dir, q |
Campaigns of coupon codes |
get_coupon_group |
GET /kahunacart/coupon-groups/{id} |
kahunacart.settings |
id required; page, per_page optional |
One campaign with its totals and a page of the codes under it |
create_coupon_group |
POST /kahunacart/coupon-groups |
kahunacart.settings |
name required; prefix, pattern, enabled, usage_limit, usage_limit_per_user, total_usage_limit, used_codes_limit, template optional |
Create the header a batch of codes will be generated into |
update_coupon_group |
PATCH /kahunacart/coupon-groups/{id} |
kahunacart.settings |
id required; name, prefix, pattern, enabled, usage_limit, usage_limit_per_user, total_usage_limit, used_codes_limit, template optional |
Change a campaign's fields |
delete_coupon_group |
DELETE /kahunacart/coupon-groups/{id} |
kahunacart.settings |
id, required |
Delete the campaign header |
generate_coupon_codes |
POST /kahunacart/coupons/generate |
kahunacart.settings |
group_id, count, all required |
Mint a batch of unique codes into a campaign |
export_coupon_group |
GET /kahunacart/coupon-groups/{id}/export |
kahunacart.settings |
id, required |
Every code in a campaign as a CSV — code, whether it is enabled, how many times it has been redeemed, its own limit, and the day it was minted |
list_rule_attributes |
GET /kahunacart/rules/vocabulary |
kahunacart.sales.manage |
none | The published vocabulary a sale's conditions are written against: every attribute, its type, the operators it offers, whether it is a fact about the cart, one line, the customer or the order, and which picker the admin draws for it |
list_rule_operations |
GET /kahunacart/rules/operations |
kahunacart.sales.manage |
none | Every operation a sale rule can price through, with the arguments each takes |
get_catalog_index |
GET /kahunacart/catalog/index |
kahunacart.products.manage |
none | What the denormalised catalog index holds and whether the storefront is actually reading it |
rebuild_catalog_index |
POST /kahunacart/catalog/index/rebuild |
kahunacart.products.manage |
full, max |
Rebuild the denormalised index every listing, facet count, sort and search reads |
list_search_synonyms |
GET /kahunacart/search/synonyms |
kahunacart.products.manage |
none | The merchant's own search vocabulary: each row is a word shoppers type and the words it should also find |
set_search_synonyms |
PUT /kahunacart/search/synonyms |
kahunacart.products.manage |
synonyms, required |
Replace the whole synonym table with the rows you send |
list_product_types |
GET /kahunacart/product-types |
none (public route) | none | Every kind of thing this store can sell: the four core types (physical, digital, grouped, external) plus any an installed add-on registers |
list_product_relations |
GET /kahunacart/products/{id}/relations |
kahunacart.products.manage |
id, required |
List the products a merchant hand-picked to sit beside this one, in the order they picked them, split into the three kinds: related ("you might also like"), cross_sell ("goes well with this") and up_sell ("consider this instead") |
set_product_relations |
PUT /kahunacart/products/{id}/relations |
kahunacart.products.manage |
id required; related, cross_sell, up_sell optional |
Replace one product's curated lists |
rebuild_product_relations |
POST /kahunacart/products/relations/rebuild |
kahunacart.products.manage |
none | Orphan every cached relation block in the store and delete relation rows pointing at products that no longer exist |
list_bulk_fields |
GET /kahunacart/products/bulk/fields |
kahunacart.products.manage |
none | List what a bulk edit can change, which modes and units each field accepts, the values the enumerated fields allow, and the store's admin.bulk.max_rows limit |
list_price_lists |
GET /kahunacart/price-lists |
kahunacart.sales.manage |
none | Every price list with its header and its rules |
get_price_list |
GET /kahunacart/price-lists/{id} |
kahunacart.sales.manage |
id, required |
One price list with its rules in order and every price row it holds, variant by variant and rung by rung |
create_price_list |
POST /kahunacart/price-lists |
kahunacart.sales.manage |
name required; currency, kind, priority, starts_at, expires_at, active, show_in_catalog, catalog_label, rules, prices optional |
Create a named set of prices |
update_price_list |
PATCH /kahunacart/price-lists/{id} |
kahunacart.sales.manage |
id required; name, currency, kind, priority, starts_at, expires_at, active, show_in_catalog, catalog_label, rules, prices optional |
Change a price list's header, its rules, its prices, or any combination |
delete_price_list |
DELETE /kahunacart/price-lists/{id} |
kahunacart.sales.manage |
id, required |
Delete a price list, its rules and every price row it holds |
list_price_tiers |
GET /kahunacart/price-tiers |
kahunacart.sales.manage |
variant_id, sku, currency |
One variant's own quantity ladder — the "buy ten and save" breaks that belong to no price list and apply to every shopper |
set_price_tiers |
PUT /kahunacart/price-tiers |
kahunacart.sales.manage |
tiers required; variant_id, sku, currency optional |
Replace one variant's quantity ladder |
preview_bulk_update |
POST /kahunacart/products/bulk/preview |
kahunacart.products.manage |
product_ids, operations, all required |
Say what a bulk edit would do without doing any of it — how many products and variants it would touch, and the before and after of the first ten |
bulk_update_products |
POST /kahunacart/products/bulk |
kahunacart.products.manage |
product_ids, operations, all required |
Change price, compare price, stock quantity, status, visibility, featured or the quantity limits across many products in one transaction, using Change to / Increase by / Decrease by with a fixed amount or a percentage |
get_product_history |
GET /kahunacart/products/{id}/history |
kahunacart.products.manage |
id required; per_page optional |
Read what has happened to one product — every bulk edit and every import that changed it, newest first, each carrying the fields that changed with their before and after values in minor units and the person who made the change |
list_import_fields |
GET /kahunacart/import/fields |
kahunacart.products.manage |
none | List what the CSV importer can import — the five entities (categories, products, variants, customers, orders) in the order a whole-store migration has to run them, the fields each accepts with their types and the column headings they are usually exported under, which keys rows can be matched on, and which fields are required |
list_import_mappings |
GET /kahunacart/import/mappings |
kahunacart.products.manage |
entity |
List the saved column mappings, each with the entity it describes, the key rows are matched on, what it does with rows that already exist, and the column-to-field map itself |
create_import_mapping |
POST /kahunacart/import/mappings |
kahunacart.products.manage |
name, entity, match_key, columns required; on_existing optional |
Save a reusable column mapping so the same export can be imported again next month without describing the file twice |
update_import_mapping |
PATCH /kahunacart/import/mappings/{id} |
kahunacart.products.manage |
id required; name, entity, match_key, on_existing, columns optional |
Change a saved mapping |
delete_import_mapping |
DELETE /kahunacart/import/mappings/{id} |
kahunacart.products.manage |
id, required |
Delete a saved column mapping |
dry_run_import |
POST /kahunacart/import/dry-run |
kahunacart.products.manage |
upload required; mapping_id, mapping, limit optional |
Rehearse an import of an already-uploaded file: the same writers, the same values, inside a transaction that is always rolled back |
start_import |
POST /kahunacart/import/runs |
kahunacart.products.manage |
upload required; mapping_id, mapping, filename optional |
Queue an import of an already-uploaded file |
list_import_runs |
GET /kahunacart/import/runs |
kahunacart.products.manage |
per_page |
List recent import runs, newest first, with their entity, status, progress and counts of rows created, updated, skipped and failed |
get_import_run |
GET /kahunacart/import/runs/{id} |
kahunacart.products.manage |
id, required |
Read one import run's progress and its first fifty failed rows, each with the line number of the merchant's own file and the reason it failed |
list_export_sets |
GET /kahunacart/export/sets |
kahunacart.export |
none | List the parts of the store a bundle can carry (settings, customers, catalog, pictures, digital files, promotions, reviews, orders, store credit, and each add-on's own), grouped as the Export screen shows them, with how many rows each holds, the sets each depends on, the permission each needs, and whether this caller holds it |
start_store_export |
POST /kahunacart/exports |
kahunacart.export |
everything, sets, media, files, passwords, secrets, passphrase, orders_from, orders_to |
Queue an export of the store into one bundle (a zip) on the server |
get_store_export |
GET /kahunacart/exports/{id} |
kahunacart.export |
id, required |
Read one export's progress, and once it has finished, its size, its sets with their row and file counts, whether secrets were sealed in it, what was not included, and when the bundle will be deleted from the server |
delete_store_export |
DELETE /kahunacart/exports/{id} |
kahunacart.export |
id, required |
Delete an export's bundle from the server |
list_store_bundles |
GET /kahunacart/restores/bundles |
kahunacart.restore |
none | List the bundles in user/data/kahunacart/exports, newest first: this store's exports, uploaded bundles, and bundles copied into the folder by hand |
inspect_store_bundle |
GET /kahunacart/restores/bundles/{name} |
kahunacart.restore |
name, required |
Read what a bundle on the server holds before restoring it: where it came from, the KahunaCart and add-on versions that wrote it, each set with its counts and whether it can be restored here (and which add-on to install when not), whether it carries sealed secrets, and whether a full restore is allowed into this store and why not |
delete_store_bundle |
DELETE /kahunacart/restores/bundles/{name} |
kahunacart.restore |
name, required |
Delete a bundle from user/data/kahunacart/exports by its file name |
dry_run_store_restore |
POST /kahunacart/restores/dry-run |
kahunacart.restore |
bundle, mode required; sets, passphrase optional |
Queue a rehearsal of restoring a bundle: the whole restore inside a transaction that is always rolled back, so it writes nothing |
start_store_restore |
POST /kahunacart/restores |
kahunacart.restore |
bundle, mode required; sets, passphrase optional |
Queue the restore of a bundle into this store |
get_store_restore |
GET /kahunacart/restores/{id} |
kahunacart.restore |
id, required |
Read one restore or dry run: its progress, and once finished its report |
list_store_transfers |
GET /kahunacart/transfers |
kahunacart.export |
kind, per_page |
The history of store exports and restores, newest first: who ran each, when, which sets, the mode, the bundle's size and how it went |
list_order_statuses |
GET /kahunacart/order-statuses |
kahunacart.orders.view |
none | Every order status this store has, grouped by the three axes an order carries — status (the cart lifecycle), payment_status and fulfillment_status — with a live count of how many orders stand in each |
create_order_status |
POST /kahunacart/order-statuses |
kahunacart.orders.manage |
axis, code, flag required; label, colour, position, enabled, counts_as_paid, reduces_stock, restores_stock, is_final optional |
Add a status to one axis |
update_order_status |
PATCH /kahunacart/order-statuses/{id} |
kahunacart.orders.manage |
id required; label, colour, position, flag, enabled, counts_as_paid, reduces_stock, restores_stock, is_final optional |
Rename, recolour, reposition, reflag or switch off one status |
delete_order_status |
DELETE /kahunacart/order-statuses/{id} |
kahunacart.orders.manage |
id, required |
Remove a status a merchant added |
reorder_order_statuses |
POST /kahunacart/order-statuses/reorder |
kahunacart.orders.manage |
axis, ids, all required |
Put one axis in an order, posted as the whole list of ids in the order you want |
list_order_changes |
GET /kahunacart/orders/{id}/changes |
kahunacart.orders.view |
id, required |
Every change ever staged against an order, newest first, each with its typed actions, its state (staged, confirmed or discarded) and the versions it moved the order between |
create_order_change |
POST /kahunacart/orders/{id}/changes |
kahunacart.orders.manage |
id required; change_type, reason, idempotency_key optional |
Open a change against a placed order |
add_order_change_action |
POST /kahunacart/orders/{id}/changes/{cid}/actions |
kahunacart.orders.manage |
id, cid, action required; item_id, adjustment_id, variant_id, qty, received_qty, damaged_qty, unit_price_minor, amount_minor, title, label, type, basis optional |
Add one typed verb to a staged change and get back the previewed totals |
confirm_order_change |
POST /kahunacart/orders/{id}/changes/{cid}/confirm |
kahunacart.orders.manage |
id, cid, all required |
Apply the change to a new version of the order, atomically |
cancel_order_change |
POST /kahunacart/orders/{id}/changes/{cid}/cancel |
kahunacart.orders.manage |
id, cid, all required |
Abandon a staged change |
get_order_version |
GET /kahunacart/orders/{id}/versions/{v} |
kahunacart.orders.view |
id, v, all required |
The lines and adjustments an order carried at a given version, exactly as they were written |
unlock_order |
POST /kahunacart/orders/{id}/unlock |
kahunacart.orders.manage |
id required; reason optional |
Open an order whose status is marked final — cancelled, archived, or anything a merchant flagged that way — so a change can be staged against it |
mark_test_order |
POST /kahunacart/orders/{id}/test |
kahunacart.orders.manage |
id required; test, reason optional |
Mark an order the merchant placed to try the store (a rehearsal, a comp, a card run through a live provider) as a test order, or with test false take the mark off |
set_order_dispute |
POST /kahunacart/orders/{id}/dispute |
kahunacart.orders.manage |
id, outcome required; note optional |
Set an order's dispute to won, lost, or back to open, by hand |
list_features |
GET /kahunacart/features |
kahunacart.orders.view |
none | Core features that change how orders are stored, with whether each is on, which installed add-ons have declared they understand it, and which have not |
set_feature |
POST /kahunacart/features/{code}/enable |
kahunacart.orders.manage |
code required; enabled optional |
Switch a feature on, or off with {"enabled": false} |
list_credit_accounts |
GET /kahunacart/credit/accounts |
kahunacart.orders.view |
customer_id, sort, dir, q, page |
Every store-credit account with its balance, which is summed from the account's transactions on every read and stored in no column anywhere |
get_credit_account |
GET /kahunacart/credit/accounts/{id} |
kahunacart.orders.view |
id, required |
One account with its balance, the customer it belongs to, and how many movements it carries |
list_credit_transactions |
GET /kahunacart/credit/accounts/{id}/transactions |
kahunacart.orders.view |
id required; sort, dir, q, page optional |
One account's movements, newest first, each with the provenance the ledger required of it — what produced the row (gift_card, refund, order or adjustment), which one, the order it belongs to, and a source snapshot of what the producing rule saw |
adjust_store_credit |
POST /kahunacart/credit/accounts/{id}/adjust |
kahunacart.orders.refund |
id, note required; amount, amount_minor optional |
Add credit to an account or take some back, by hand |
refund_to_credit |
POST /kahunacart/credit/refunds |
kahunacart.orders.refund |
order_id required; amount, amount_minor, items, shipping_minor, restock, reason, notify, return_id optional |
Give a refund back as store credit instead of to the card that paid for the order |
list_credit_notes |
GET /kahunacart/orders/{id}/credit-notes |
kahunacart.orders.view |
id, required |
The numbered documents an order's refunds have earned, oldest first |
list_gift_cards |
GET /kahunacart/gift-cards |
kahunacart.orders.view |
status, sort, dir, q, page |
Every gift card with its value, its state and the last four characters of its code |
create_gift_card |
POST /kahunacart/gift-cards |
kahunacart.orders.refund |
amount, amount_minor, currency, note |
Mint a card by hand — a goodwill gesture, a promotion, a card sold over the counter |
redeem_gift_card |
POST /kahunacart/gift-cards/redeem |
kahunacart.orders.refund |
code, customer_id required; order_id optional |
Put a card's value into a customer's store credit on their behalf — the support-desk case, for somebody who cannot type the code in themselves |
void_gift_card |
POST /kahunacart/gift-cards/{id}/void |
kahunacart.orders.refund |
id required; reason optional |
Cancel a card that has not been redeemed and take its value back off the ledger |
list_customer_groups |
GET /kahunacart/customer-groups |
kahunacart.settings |
none | List the store's customer groups with their codes, labels, flat discount percentages and how many customers are in each |
create_customer_group |
POST /kahunacart/customer-groups |
kahunacart.settings |
label required; code, description, position, is_default, discount_percent, show_on_account optional |
Create a customer group |
update_customer_group |
PATCH /kahunacart/customer-groups/{id} |
kahunacart.settings |
id required; code, label, description, position, is_default, discount_percent, show_on_account optional |
Change a customer group |
delete_customer_group |
DELETE /kahunacart/customer-groups/{id} |
kahunacart.settings |
id, required |
Delete a customer group |
set_customer_group |
POST /kahunacart/customers/{id}/group |
kahunacart.customers.manage |
id required; group_ids, group_id optional |
Set the groups one customer is in |
list_customer_wishlists |
GET /kahunacart/customers/{id}/wishlists |
kahunacart.customers.view |
id, required |
List the saved lists one customer keeps, each with its name, its sharing setting and the products on it |
list_product_stock_alerts |
GET /kahunacart/products/{id}/stock-alerts |
kahunacart.products.manage |
id required; status optional |
List the back-in-stock alerts on one product, newest first: each with the variant it is for, the shopper's email, whether it is pending (a guest who has not confirmed yet), active, sent, cancelled or expired, and when it was confirmed and sent |
list_customer_stock_alerts |
GET /kahunacart/customers/{id}/stock-alerts |
kahunacart.customers.view |
id required; all optional |
List the back-in-stock alerts one customer holds, found by their account and by their email address, so an alert they left as a guest before signing up is included |
cancel_stock_alert |
POST /kahunacart/stock-alerts/{id}/cancel |
kahunacart.customers.manage |
id, required |
Stop one back-in-stock alert, pending or active, so no email is sent for it |
get_erasure_blockers |
GET /kahunacart/customers/{id}/erasure-blockers |
kahunacart.customers.manage |
id, required |
Say whether this customer can be erased and, when they cannot, name every reason — an order still awaiting payment or awaiting despatch, a download they can still fetch, and whatever add-ons report holding on their behalf such as an active subscription or an issued licence |
erase_customer |
POST /kahunacart/customers/{id}/erase |
kahunacart.customers.manage |
id, required |
Anonymise a customer and everything that points at them: the email, the names and addresses frozen onto their orders, their saved address book, their login, their wishlists, and the identity behind their reviews |
reassign_orders |
POST /kahunacart/customers/{id}/reassign-orders |
kahunacart.customers.manage |
id, to_customer_id required; dry_run optional |
Move every order from one customer to another, with its history, its notes, its refunds and its download grants |
merge_customer |
POST /kahunacart/customers/{id}/merge |
kahunacart.customers.manage |
id, to_customer_id required; dry_run optional |
Merge a customer into another one, for somebody who bought under two addresses |
get_retention_policy |
GET /kahunacart/retention |
kahunacart.customers.view |
none | Read the store's data-retention windows — how long placed-but-unpaid orders are kept, how long failed ones are, how long before a completed order is anonymised, how long before an inactive customer is erased — and what each rule last did |
run_retention |
POST /kahunacart/retention/run |
kahunacart.customers.manage |
dry_run, limit |
Run the retention sweep immediately instead of waiting for the daily worker tick |
list_reviews |
GET /kahunacart/reviews |
kahunacart.products.manage |
state, product_id, page, per_page, sort, dir, q |
List product reviews for moderation, oldest first, optionally narrowed to one state or one product |
approve_review |
POST /kahunacart/reviews/{id}/approve |
kahunacart.products.manage |
id, required |
Publish a review on its product page and count it in that product's star rating |
reject_review |
POST /kahunacart/reviews/{id}/reject |
kahunacart.products.manage |
id, required |
Turn a review down |
delete_review |
DELETE /kahunacart/reviews/{id} |
kahunacart.products.manage |
id, required |
Remove a review outright, leaving no record of it |
rebuild_review_ratings |
POST /kahunacart/reviews/rebuild |
kahunacart.products.manage |
none | Throw away every cached star aggregate in the store so the next request recomputes it |
list_payment_methods |
GET /kahunacart/payment-methods |
kahunacart.settings |
none | Every merchandised payment method this store offers, with its fee, its scoping conditions and the gateway it runs over |
create_payment_method |
POST /kahunacart/payment-methods |
kahunacart.settings |
provider, code required; label_key, description_key, logo, instructions, enabled, position, fee_fixed_minor, currencies, currency_amounts, fee_percent, fee_tax_class, zero_total_only, config, conditions optional |
Add a checkout choice over an existing gateway |
update_payment_method |
PATCH /kahunacart/payment-methods/{id} |
kahunacart.settings |
id required; provider, code, label_key, description_key, logo, instructions, enabled, position, fee_fixed_minor, currencies, currency_amounts, fee_percent, fee_tax_class, zero_total_only, config, conditions optional |
Change a method's merchandising, its fee, or its conditions |
delete_payment_method |
DELETE /kahunacart/payment-methods/{id} |
kahunacart.settings |
id, required |
Remove a checkout choice and the conditions that scoped it |
reorder_payment_methods |
POST /kahunacart/payment-methods/reorder |
kahunacart.settings |
ids, required |
Put the checkout list in the order given |
bulk_order_status |
POST /kahunacart/orders/bulk/status |
kahunacart.orders.manage |
order_ids, status required; message, notify optional |
Move a selection of orders onto one status, as a single background job |
get_bulk_run |
GET /kahunacart/orders/bulk/{jobId} |
kahunacart.orders.view |
jobId, required |
What a bulk status change did, order by order: which changed, which were already in that status, and which failed and why |
export_orders |
POST /kahunacart/orders/bulk/export |
kahunacart.orders.view |
scope, format, column_set_id, columns, from, to, statuses, order_ids, filter |
Orders out in the columns you choose, as CSV text or as JSON rows |
list_export_column_sets |
GET /kahunacart/export-column-sets |
kahunacart.orders.view |
none | The saved column choices, plus the whole column vocabulary for both scopes and the defaults a merchant gets when they have saved none |
create_export_column_set |
POST /kahunacart/export-column-sets |
kahunacart.orders.manage |
name, columns required; scope optional |
Save a named column choice so the same export comes out the same way next month |
update_export_column_set |
PATCH /kahunacart/export-column-sets/{id} |
kahunacart.orders.manage |
id required; name, scope, columns optional |
Rename a set, move it to the other scope, or replace its columns |
delete_export_column_set |
DELETE /kahunacart/export-column-sets/{id} |
kahunacart.orders.manage |
id, required |
Remove a saved column choice |
quote_order |
POST /kahunacart/orders/{id}/quote |
kahunacart.orders.manage |
id required; expires_at, days, send optional |
Turn a placed order into an offer with a deadline, and optionally email it |
accept_quote |
POST /kahunacart/orders/{id}/quote/accept |
kahunacart.orders.manage |
id, required |
Record that the customer accepted, for an answer that arrived by phone or by email — the customer's own button is on their order page |
decline_quote |
POST /kahunacart/orders/{id}/quote/decline |
kahunacart.orders.manage |
id, required |
Record that the customer turned the offer down |
list_custom_fields |
GET /kahunacart/custom-fields |
kahunacart.settings |
entity |
Every custom field defined on this store, optionally for one entity — product, variant, order, order_item, customer, address, or an entity an add-on registered — with its type, visibility (public, readonly or internal), whether the admin list can sort and filter on it, its validation rules, its default and the blueprint fragment the admin renders it from |
create_custom_field |
POST /kahunacart/custom-fields |
kahunacart.settings |
entity, name required; type, label_key, help_key, visibility, sortable, filterable, required, default, validate, position optional |
Define a custom field on an entity |
update_custom_field |
PATCH /kahunacart/custom-fields/{id} |
kahunacart.settings |
id required; label_key, help_key, visibility, sortable, filterable, required, default, validate, position optional |
Change a custom field's label, help, visibility, sortable and filterable flags, required flag, default, validation or position |
delete_custom_field |
DELETE /kahunacart/custom-fields/{id} |
kahunacart.settings |
id, required |
Delete a custom field definition and every value stored under it, on every row of its entity |
read_custom_field_values |
GET /kahunacart/custom-fields/values/{entity}/{entityId} |
kahunacart.orders.view |
entity, entityId, all required |
Every custom field value on one row of an entity — a product, a variant, an order, an order line, a customer or an address — with the field definitions beside them |
write_custom_field_values |
PUT /kahunacart/custom-fields/values/{entity}/{entityId} |
kahunacart.orders.view |
entity, entityId, values, all required |
Set custom field values on one row of an entity |
read_settings_store |
GET /kahunacart/settings-store/{namespace} |
kahunacart.orders.view |
namespace, required |
Every key in one namespace of the add-on settings store with its current value, or its default when nothing has been written, plus the key's scope and permission |
write_settings_store |
PUT /kahunacart/settings-store/{namespace} |
kahunacart.orders.view |
namespace, values, all required |
Write one or more keys in a namespace of the add-on settings store |
list_zones |
GET /kahunacart/zones |
kahunacart.orders.view |
none | The zone catalogue: every place an add-on can mount a widget in the admin or on the storefront, by its stable id — order.details.side, product.details, cart.express and the rest — with the widgets currently mounted in each, already in the merchant's order, the saved layouts, and whether the caller has dev mode switched on |
list_schema_extensions |
GET /kahunacart/schema-extensions |
kahunacart.settings |
none | What every installed add-on has declared it adds to KahunaCart's own responses, and where |
list_hook_points |
GET /kahunacart/hook-points |
kahunacart.settings |
none | The five points in an order's life where an add-on may do something that has to be undone if a later step fails — order_completed, payment_captured, fulfillment_created, order_canceled and order_refunded — with every handler attached to each, the plugin that owns it, and whether it declared an undo |
list_product_revisions |
GET /kahunacart/products/{id}/revisions |
kahunacart.products.manage |
id, required |
Every version of one product, newest first, with what wrote it (a save, a restore or a scheduled change), the note and the admin who made it, and the product's publish window |
restore_product_revision |
POST /kahunacart/products/{id}/revisions/{revision}/restore |
kahunacart.products.manage |
id, revision, all required |
Put a product back exactly as one of its revisions found it — variants, options, media, categories, tags, attributes, kit children, relations, quantity tiers, translations and custom-field values, ids included |
duplicate_product |
POST /kahunacart/products/{id}/duplicate |
kahunacart.products.manage |
id, required |
Copy a product into a new draft and answer with the copy, in the same form create_product answers |
set_scheduled_publish |
PUT /kahunacart/products/{id}/schedule |
kahunacart.products.manage |
id required; publish_at, unpublish_at optional |
Give a product a time to go live and a time to come down, as UTC epoch seconds |
list_product_prerequisites |
GET /kahunacart/products/{id}/prerequisites |
kahunacart.products.manage |
id, required |
What a customer has to have bought before they may buy this product |
set_product_prerequisites |
PUT /kahunacart/products/{id}/prerequisites |
kahunacart.products.manage |
id, prerequisites required; mode optional |
Replace the list wholesale |
list_payment_collections |
GET /kahunacart/payment-collections |
kahunacart.orders.view |
status, page, per_page, sort, dir, q |
The orders that have been part paid, with what each owes, what has been authorised, captured and refunded, and what is still outstanding |
create_deposit |
POST /kahunacart/orders/{id}/deposit |
kahunacart.orders.manage |
id required; amount, percent, provider optional |
Open a payment collection on one order and record the deposit session against it |
collect_order_balance |
POST /kahunacart/orders/{id}/collect-balance |
kahunacart.orders.manage |
id, required |
Send the balance request email to a part-paid order's customer and record the send on the order |
capture_payment |
POST /kahunacart/transactions/{id}/capture |
kahunacart.orders.manage |
id required; amount_minor, amount optional |
Take some or all of the money a provider is holding against an order |
void_payment |
POST /kahunacart/transactions/{id}/void |
kahunacart.orders.manage |
id required; reason optional |
Release the money a provider is holding against an order without taking any of it, and cancel the order — the goods go back on the shelf, the coupon is released and download grants are revoked, exactly as Cancel does, and the history says why |
list_payment_tokens |
GET /kahunacart/customers/{id}/payment-tokens |
kahunacart.customers.view |
id, required |
The payment methods a provider keeps on file for one customer, as this store knows them: the provider, a display label, the brand, the last four digits and the expiry the provider reports, and which one is the default |
delete_payment_token |
DELETE /kahunacart/payment-tokens/{id} |
kahunacart.customers.manage |
id, required |
Ask the provider to forget a stored payment method, then retire the store's own record of it |
list_webhook_subscriptions |
GET /kahunacart/webhook-subscriptions |
kahunacart.settings |
none | Every outbound webhook subscription — a topic and a URL another system is told about — with its status (active, paused, disabled), failure streak and last delivered and failed times, plus the counts per status, every topic a subscription may name, the disable threshold and the delivery-log retention |
create_webhook_subscription |
POST /kahunacart/webhook-subscriptions |
kahunacart.settings |
name, topic, target_url required; ping optional |
Create a subscription and answer with its signing secret, once |
update_webhook_subscription |
PATCH /kahunacart/webhook-subscriptions/{id} |
kahunacart.settings |
id required; name, topic, target_url, status, rotate_secret optional |
Rename, retarget or change the topic; pause; activate, which pings the target first and refuses with the reason when the ping fails; or rotate the secret, which answers with the new one once and stops the old one working at that moment |
delete_webhook_subscription |
DELETE /kahunacart/webhook-subscriptions/{id} |
kahunacart.settings |
id, required |
Delete a subscription |
list_outbound_webhook_deliveries |
GET /kahunacart/webhook-deliveries |
kahunacart.settings |
subscription_id, order_id, event_id, sort, dir, q, page, per_page |
The outbound delivery log, one row per attempt, newest first: the subscription, the topic, the event id, the order it concerned, the attempt number, the response status, a 2 KB excerpt of the response, how long it took and any error |
redeliver_webhook |
POST /kahunacart/webhook-deliveries/{id}/redeliver |
kahunacart.settings |
id, required |
Queue one attempt's occurrence again, to that one subscription, with the same event id so the receiver can deduplicate |
list_jobs |
GET /kahunacart/jobs |
kahunacart.settings |
state, type, sort, dir, page, per_page |
A page of the job queue, newest first: every job with its state — pending, running, complete, failed or cancelled — its arguments, when it was queued and started, how long it took, how many attempts of how many, the worker that held it and the last error, plus the counts per state |
run_job |
POST /kahunacart/jobs/{id}/run |
kahunacart.settings |
id, required |
Release a job for the next worker tick, whatever it was waiting for; a failed job is granted one more attempt |
cancel_job |
POST /kahunacart/jobs/{id}/cancel |
kahunacart.settings |
id, required |
Cancel a pending job outright, or ask a running one to stop: the flag is read at a transaction boundary, so a bulk change stops between orders and nothing half-written is left |
run_check |
GET /kahunacart/check |
kahunacart.settings |
offline |
Run every going-live check — database, migrations, scheduler, mail transport, download roots, trusted proxies, provider credentials, webhook reachability, template drift, the report rollup — and answer each finding with its status, detail, suggested fix and docs anchor, the overall verdict, and the redacted support report as text |
template_drift_report |
GET /kahunacart/template-drift |
kahunacart.settings |
none | Every template the plugin ships with the state of its theme override: current, behind (the override's @version is lower than the shipped one), ahead, unstamped, or none |
list_notifications |
GET /kahunacart/notifications-registry |
kahunacart.settings |
none | Every email the store can send — the base plugin's own and any an add-on registers — with whether it is switched on, the template pair it renders from, the shipped pair it can be restored to, who it goes to and what triggers it, plus the template pairs a notification may be pointed at |
update_notification |
PATCH /kahunacart/notifications-registry |
kahunacart.settings |
code required; enabled, template, restore, channels, priority, use_default optional |
Change one notification by code |
list_notification_channels |
GET /kahunacart/notification-channels |
kahunacart.settings |
none | Every channel the store can reach its owner on — email, Slack, Discord, Telegram, Pushover, ntfy, Gotify, a text message through Textbelt or Twilio or a webhook of your own — with its id, its kind and what it is called, plus the fields each kind asks for |
set_default_notification_channels |
PUT /kahunacart/notification-channels/default |
kahunacart.settings |
channels, required |
Set the channels every store-owner message goes to until its own channels are saved — "New messages go to" on the Channels page |
get_object_template |
GET /kahunacart/object-templates |
kahunacart.settings |
slot |
The operator-editable formats — SKU, product and variant line titles, the order description a provider is handed, the order reference, the invoice file name and every order email's subject — each with its shipped default, the template it currently holds, the variables it may read and the sample its preview renders against |
set_object_template |
PUT /kahunacart/object-templates |
kahunacart.settings |
slot, template, all required |
Save the Twig template one slot renders |
list_sales |
GET /kahunacart/sales |
kahunacart.sales.manage |
none | Every sale with its header, its rules and its quantity ladder |
get_sale |
GET /kahunacart/sales/{id} |
kahunacart.sales.manage |
id, required |
One sale with its rules in order, its quantity ladder lowest rung first, and the translation overrides on its shopper-facing label |
create_sale |
POST /kahunacart/sales |
kahunacart.sales.manage |
name, label required; starts_at, expires_at, priority, enabled, stack_with_coupons, rules, conditions, tier_basis, tiers, translations optional |
Create an automatic discount |
update_sale |
PATCH /kahunacart/sales/{id} |
kahunacart.sales.manage |
id required; name, label, starts_at, expires_at, priority, enabled, stack_with_coupons, rules, conditions, tier_basis, tiers, translations optional |
Change a sale's header, its rules, or both |
delete_sale |
DELETE /kahunacart/sales/{id} |
kahunacart.sales.manage |
id, required |
Delete a sale, its rules and the translations of its label |
report_summary |
GET /kahunacart/reports/summary |
kahunacart.reports |
currency_table, currency_picker, currency, from, to |
Headline figures for a date range: revenue, orders, average order value, items sold, refunds, top products, a breakdown by payment method, and a per-day series carrying revenue, orders, units and refunds |
report_products |
GET /kahunacart/reports/products |
kahunacart.reports |
currency_picker, currency, from, to, page, sort, dir, q |
What sold in a date range, by product and by variant, sorted by revenue, with units, revenue and refunds on each |
report_customers |
GET /kahunacart/reports/customers |
kahunacart.reports |
currency_picker, currency, from, to, page, sort, dir, q |
Who bought in a date range, sorted by revenue, with each customer's orders, revenue, average order value, first and last order and whether they were new in the range, plus new against returning splits for the whole range |
report_taxes |
GET /kahunacart/reports/taxes |
kahunacart.reports |
currency_picker, currency, from, to |
Tax collected in a date range, per label and rate, with the taxable base and the tax beside it |
report_refunds |
GET /kahunacart/reports/refunds |
kahunacart.reports |
currency_picker, currency, from, to, page, sort, dir, q |
Every refund transaction raised in a date range, newest first, whatever became of it, with the order and customer it belongs to |
report_low_stock |
GET /kahunacart/reports/low-stock |
kahunacart.reports |
threshold |
Stock-managed variants at or below a threshold, right now rather than over a range |
list_orders |
GET /kahunacart/orders |
kahunacart.orders.view |
q, currency, sort, dir, cf, product, variant, from, to, customer, status, payment, fulfillment, channel, location, refund_request, page, per_page |
Every order that got past the cart, newest first, canceled ones included, with its number, email, totals, payment status and fulfillment status |
get_order |
GET /kahunacart/orders/{id} |
kahunacart.orders.view |
id, required |
One order with its lines, adjustments, payment transactions, its stored history, its notes thread and which of its emails were last queued and sent, plus the invoice and invoice PDF URLs, a pay link while there is still something to pay, and the refunded and pending-refund totals |
create_order |
POST /kahunacart/orders |
kahunacart.orders.manage |
items required; email, channel, location_id, register_ref, name, note, currency, billing, shipping, shipping_method_id, save_addresses, billing_address_id, shipping_address_id, tax_exempt, vat_id optional |
Record an order taken somewhere other than the storefront, over the phone, at a stall or by email |
list_order_tenders |
GET /kahunacart/orders/{id}/tenders |
kahunacart.orders.view |
id, required |
Every payment on an order, oldest first, as a till reads them: type (cash, card, store_credit, gift_card, pay_link or online), the amount, for cash what was handed over (tendered_minor) and the change, any cash rounding, the provider and its reference, who took it (taken_by), the till (register_ref), a card's reader, and what has been refunded against it |
add_order_tender |
POST /kahunacart/orders/{id}/tenders |
kahunacart.orders.manage |
id, type required; amount_minor, tendered_minor, code, register_ref, idempotency_key optional |
Record one payment on an order that is waiting for money, so a sale can be paid several ways |
counter_return |
POST /kahunacart/orders/{id}/counter-return |
kahunacart.orders.refund |
id, lines required; reason, note, refund optional |
The customer brought goods back to the shop: open an in-store return, approve it and receive it in one step, then refund it |
open_counter_cart |
POST /kahunacart/counter-carts |
kahunacart.orders.manage |
location_id required; register_ref, currency optional |
Start a sale at a till as a cart, to be priced as items are added and then completed into a counter-sale order with complete_counter_cart |
get_counter_cart |
GET /kahunacart/counter-carts/{id} |
kahunacart.orders.manage |
id, required |
A counter cart priced, or the counter-sale order it became once it was completed (open is then false) |
discard_counter_cart |
DELETE /kahunacart/counter-carts/{id} |
kahunacart.orders.manage |
id, required |
Void a sale that was never charged, or a parked sale nobody came back for: the cart, its lines and its adjustments are deleted |
add_counter_cart_line |
POST /kahunacart/counter-carts/{id}/lines |
kahunacart.orders.manage |
id, variant_id required; qty, line_data, parts optional |
Add a variant to the sale, as a buy button would: the price, the title and the stock check are the catalog's, and a variant already on the cart with the same line data gains the quantity |
set_counter_cart_line_qty |
PATCH /kahunacart/counter-carts/{id}/lines/{item} |
kahunacart.orders.manage |
id, item, qty, all required |
Set a line's quantity; 0 takes the line off, with its staff discount |
remove_counter_cart_line |
DELETE /kahunacart/counter-carts/{id}/lines/{item} |
kahunacart.orders.manage |
id, item, all required |
Take a line off the sale, with its staff discount |
apply_counter_cart_coupon |
POST /kahunacart/counter-carts/{id}/coupons |
kahunacart.orders.manage |
id, code, all required |
Apply a discount code to the sale through the cart's own coupon path, so it is worked out before tax exactly as at an online checkout |
remove_counter_cart_coupon |
DELETE /kahunacart/counter-carts/{id}/coupons/{code} |
kahunacart.orders.manage |
id, code, all required |
Take a discount code off the sale |
set_counter_cart_discount |
POST /kahunacart/counter-carts/{id}/discount |
kahunacart.orders.manage |
id, kind, value, reason required; item_id optional |
A discount given by hand at the till, on one line (item_id) or on the whole sale (no item_id) |
remove_counter_cart_discount |
DELETE /kahunacart/counter-carts/{id}/discount |
kahunacart.orders.manage |
id, required |
Take the staff discount on the whole sale off |
remove_counter_cart_line_discount |
DELETE /kahunacart/counter-carts/{id}/lines/{item}/discount |
kahunacart.orders.manage |
id, item, all required |
Take the staff discount on one line off |
set_counter_cart_customer |
POST /kahunacart/counter-carts/{id}/customer |
kahunacart.orders.manage |
id, customer_id, all required |
Attach a customer to the sale, which is repriced at once for their customer group's price list and group sales, or send customer_id null to make it a walk-in sale again |
complete_counter_cart |
POST /kahunacart/counter-carts/{id}/complete |
kahunacart.orders.manage |
id, required |
Turn the sale into a counter-sale order, payment pending, exactly as create_order with channel pos would have built it for the same lines, customer and location; the signed-in user is its staff_user |
refund_order |
POST /kahunacart/orders/{id}/refund |
kahunacart.orders.refund |
id required; items, shipping_minor, amount_minor, amount, reason, restock, notify, against_reference, return_id optional |
Refund an order through the provider that took the payment, either by naming the lines that came back or as a bare amount |
record_refund |
POST /kahunacart/orders/{id}/refunds/record |
kahunacart.orders.refund |
id, reference required; amount_minor, amount, reason, notify, return_id optional |
Write down a refund that was issued somewhere KahunaCart cannot see — in the provider's own dashboard, by bank transfer, or in cash |
get_refund_request |
GET /kahunacart/orders/{id}/refund-request |
kahunacart.orders.view |
id, required |
What a customer asked for on this order and what came of it: the reason they picked off the store's fixed list, anything they wrote, when they asked, and the decision with the sentence it was declined with |
approve_refund_request |
POST /kahunacart/orders/{id}/refund-request/approve |
kahunacart.orders.refund |
id required; items, shipping_minor, amount_minor, amount, reason, restock, notify, method, note, label_url, label_pdf, tracking_number, carrier optional |
Agree to a customer's refund request: the order is refunded through the provider exactly as refund_order does it, and the request is then marked approved — only if the refund answered, so a request never claims a refund the provider refused |
decline_refund_request |
POST /kahunacart/orders/{id}/refund-request/decline |
kahunacart.orders.manage |
id, note, all required |
Say no to a customer's refund request, and tell them why |
list_returns |
GET /kahunacart/returns |
kahunacart.orders.view |
status, kind, order_id, page, per_page |
Every refund request and return across the store, newest first, each with its lines and the order's order_number |
receive_return |
POST /kahunacart/orders/{id}/refund-request/receive |
kahunacart.orders.refund |
id required; lines optional |
Check in the parcel of the order's approved return: per line, how many arrived and how many of those are damaged |
cancel_return |
POST /kahunacart/orders/{id}/refund-request/cancel |
kahunacart.orders.refund |
id required; note optional |
Call off the order's open or approved return: you stopped waiting for the parcel, or the customer withdrew it |
mark_return_refunded |
POST /kahunacart/orders/{id}/refund-request/mark-refunded |
kahunacart.orders.refund |
id required; transaction_id optional |
Close the order's received (or approved) return as refunded without moving any money, for a return you settled some other way |
attach_return_label |
POST /kahunacart/orders/{id}/refund-request/label |
kahunacart.orders.refund |
id required; label_url, label_pdf, tracking_number, carrier, notify optional |
Give the order's open or approved return a label: a PDF (base64), a URL, a tracking number and a carrier |
buy_return_label |
POST /kahunacart/orders/{id}/refund-request/label/buy |
kahunacart.orders.refund |
id, provider required; notify optional |
Buy a label for the order's open or approved return from an add-on that sells them (get_refund_request lists them as label_providers) and attach it |
mark_order_paid |
POST /kahunacart/orders/{id}/mark-paid |
kahunacart.orders.manage |
id required; notify optional |
Settle an order whose payment arrived outside KahunaCart, a bank transfer most often |
fulfill_order |
POST /kahunacart/orders/{id}/fulfill |
kahunacart.orders.manage |
id required; notify optional |
Record one shipment covering every unit on the order that has not gone out yet, with no carrier named, and queue the shipped email to the customer |
list_shipments |
GET /kahunacart/orders/{id}/shipments |
kahunacart.orders.view |
id, required |
Every parcel recorded against an order, oldest first, each with its carrier, tracking number, the tracking link the customer sees and the lines it carried |
create_shipment |
POST /kahunacart/orders/{id}/shipments |
kahunacart.orders.manage |
id, items required; carrier, tracking_number, tracking_url, note, notify optional |
Record that these quantities of these lines went out, optionally with a carrier and a tracking number |
cancel_order |
POST /kahunacart/orders/{id}/cancel |
kahunacart.orders.manage |
id required; notify optional |
Call off an unpaid sale and give back everything completion took: stock, the coupon redemption and any live download links |
resend_order_confirmation |
POST /kahunacart/orders/{id}/resend-confirmation |
kahunacart.orders.manage |
id, required |
Queue the order confirmation email again |
get_order_history |
GET /kahunacart/orders/{id}/history |
kahunacart.orders.view |
id, required |
Everything recorded against an order, oldest first: when it was placed, every payment and refund at each stage of its lifecycle, when it shipped, when it was cancelled, every email queued and every email sent, and every note written |
order_notices_list |
GET /kahunacart/orders/{id}/notices |
kahunacart.orders.view |
id, required |
What still has to be said about one order, in two lists |
order_notice_resolve |
POST /kahunacart/orders/{id}/notices/{noticeId}/resolve |
kahunacart.orders.manage |
id, noticeId, all required |
Mark one queue item on an order dealt with, recording who did it and when |
order_notice_reopen |
POST /kahunacart/orders/{id}/notices/{noticeId}/reopen |
kahunacart.orders.manage |
id, noticeId, all required |
Put a resolved queue item back, for the case where somebody pressed the button on the wrong row |
admin_notices_list |
GET /kahunacart/notices |
kahunacart.orders.view |
resolved, type, sort, dir, q, page, per_page |
Orders that need a human, across the whole store, unresolved first and newest first within each half |
list_order_notes |
GET /kahunacart/orders/{id}/notes |
kahunacart.orders.view |
id, required |
The notes thread on an order — internal notes and notes written to the customer in one list, oldest first, each with its author and time |
add_order_note |
POST /kahunacart/orders/{id}/notes |
kahunacart.orders.manage |
id, body required; to_customer optional |
File a note against an order |
delete_order_note |
DELETE /kahunacart/orders/{id}/notes/{entryId} |
kahunacart.orders.manage |
id, entryId, all required |
Remove an internal note from an order |
resend_order_email |
POST /kahunacart/orders/{id}/emails/{kind}/resend |
kahunacart.orders.manage |
id, kind, all required |
Queue one of an order's emails again — confirmation, payment-received, shipped, refund, pay-link, customer-note or download-added |
issue_order_pay_link |
POST /kahunacart/orders/{id}/pay-link |
kahunacart.orders.manage |
id required; send optional |
Mint a new signed pay link for an order and move its deadline forward by orders.pay_link_days |
list_order_downloads |
GET /kahunacart/orders/{id}/downloads |
kahunacart.orders.view |
id, required |
Every download grant issued against an order, with the file it points at, how many times it has been used, when it expires and whether it has been revoked |
revoke_download |
POST /kahunacart/orders/{id}/downloads/{grantId}/revoke |
kahunacart.orders.manage |
id, grantId, all required |
Kill one download grant, which is the answer to a chargeback or a link that turned up where it should not have |
list_customers |
GET /kahunacart/customers |
kahunacart.customers.view |
currency_picker, currency, q, group, account, sort, dir, cf, page, per_page |
Everyone who has bought from this store, with their order count, lifetime revenue and, on a store that keeps customer accounts in its own database, the state of each account |
get_customer |
GET /kahunacart/customers/{id} |
kahunacart.customers.view |
id required; currency_picker, currency optional |
One customer with their lifetime stats (in one currency, with other_currencies beside them for anything they ordered in other currencies), their orders including canceled and refunded ones, their saved address book, the state of their account, and any panels contributed by add-on plugins such as a subscription or a membership |
invite_customer |
POST /kahunacart/customers/{id}/invite |
kahunacart.customers.manage |
id, required |
Open a store account for this customer if there is none yet and queue the set-password email, the same message and the same token machinery the checkout opt-in uses |
customer_magic_link_send |
POST /kahunacart/customers/{id}/magic-link |
kahunacart.customers.manage |
id, required |
Queue a single-use, short-lived link that signs this customer in, to the address on their record and never to one supplied here |
clear_customer_2fa |
POST /kahunacart/customers/{id}/clear-2fa |
kahunacart.customers.manage |
id, required |
Destroy the authenticator secret on a customer's account, for the case where somebody lost their phone |
set_customer_state |
POST /kahunacart/customers/{id}/state |
kahunacart.customers.manage |
id, state, all required |
Switch a customer's ability to sign in on or off |
list_customer_addresses |
GET /kahunacart/customers/{id}/addresses |
kahunacart.customers.view |
id, required |
A customer's address book, default billing and shipping addresses first |
create_customer_address |
POST /kahunacart/customers/{id}/addresses |
kahunacart.customers.manage |
id required; name, company, address1, address2, city, region, postcode, country, phone, label, is_default_billing, is_default_shipping optional |
Save an address for a customer, for example one taken over the phone |
update_customer_address |
PATCH /kahunacart/customers/{id}/addresses/{addressId} |
kahunacart.customers.manage |
id, addressId required; name, company, address1, address2, city, region, postcode, country, phone, label, is_default_billing, is_default_shipping optional |
Change one address in a customer's book |
delete_customer_address |
DELETE /kahunacart/customers/{id}/addresses/{addressId} |
kahunacart.customers.manage |
id, addressId, all required |
Remove an address from a customer's book |
set_default_customer_address |
POST /kahunacart/customers/{id}/addresses/{addressId}/default |
kahunacart.customers.manage |
id, addressId, kind, all required |
Make one saved address the customer's default billing or shipping address, the one checkout and the admin's New order form start on |
list_countries |
GET /kahunacart/countries |
kahunacart.orders.manage |
none | Every country an address may name, as ISO 3166-1 alpha-2 code and English name, and under regions the states of the countries that publish a list of them, keyed by country code |
webhook_summary |
GET /kahunacart/webhooks/summary |
kahunacart.settings |
none | Per-provider delivery counts and the current run of consecutive failures, plus how many days of log are kept |
list_webhook_deliveries |
GET /kahunacart/webhooks |
kahunacart.settings |
provider, page, per_page |
Recent webhook deliveries, newest first, each with the provider, the time it arrived, the status code, whether it succeeded, the action it was read as, the order it touched, any error and an excerpt of the provider's raw body |
abandoned_carts_list |
GET /kahunacart/carts/abandoned |
kahunacart.orders.view |
sort, dir, q, hours, page, per_page |
Carts that have sat untouched past the store's abandoned window and still have an email address and something in them, biggest first by default |
abandoned_cart_send_recovery |
POST /kahunacart/carts/{id}/recovery-email |
kahunacart.orders.manage |
id, required |
Queue the recovery message for one abandoned cart now instead of waiting for the sweep, and stamp the cart so the sweep does not mail it again |
cart_link_create |
POST /kahunacart/carts/{id}/link |
kahunacart.orders.manage |
id required; purpose, email optional |
Mint a signed link that opens one cart in a customer's own browser — the way a phone or social sale is handed over |
email_log_list |
GET /kahunacart/emails |
kahunacart.orders.view |
kind, recipient, status, order_id, sort, dir, page, per_page |
A page of the email log, newest first: every message the store queued for a customer with its kind, recipient, subject, order, how many attempts it took, whether it was sent, failed or is still pending, and the transport's own words on a failure |
report_recovery |
GET /kahunacart/reports/recovery |
kahunacart.orders.view |
currency_picker, currency, days |
How well cart recovery is working over a window: carts mailed, carts reminded, carts recovered, the recovery rate, and what the recovered carts were worth once they became orders |
list_providers |
GET /kahunacart/providers |
kahunacart.settings |
none | Every registered payment method with its label, the capabilities it declares, whether it can be connection-checked, whether it can register its own webhook endpoint, whether it syncs products, what automatic sync is configured to do, and which one checkout starts on |
check_provider |
POST /kahunacart/providers/{slug}/check |
kahunacart.settings |
slug, required |
Ask a payment provider to prove the credentials already configured for it actually work, and report what it resolved, such as the account and the environment |
sync_provider |
POST /kahunacart/providers/{slug}/sync |
kahunacart.settings |
slug required; direction optional |
Push this store's catalog to a payment provider, or pull the provider's into it, and report how many products moved |
setup_provider_webhook |
POST /kahunacart/providers/{slug}/webhook |
kahunacart.settings |
slug required; reset optional |
Ask a provider to register this store's webhook endpoint with it, subscribed to exactly the events it handles, and write whatever signing secret comes back into that provider's own plugin settings |
make_default_provider |
POST /kahunacart/providers/{slug}/default |
kahunacart.settings |
slug, required |
Write checkout.default_provider so checkout starts on this payment method |
list_provider_readers |
GET /kahunacart/providers/{slug}/readers |
kahunacart.settings |
slug, required |
The card readers on a payment provider's account, as the provider reports them now: each with its id, the label it was given, its status (online, offline or busy), its model and the provider's location reference, and how the provider pairs readers: pairing device_code (the reader shows a code, send it with pair_provider_reader) or app_code (get a code with begin_reader_pairing and type it into the reader) |
pair_provider_reader |
POST /kahunacart/providers/{slug}/readers |
kahunacart.settings |
slug, code required; label, location_ref optional |
Register a card reader with a payment provider from the code the reader shows on its screen (a Stripe registration code, a SumUp pairing code), and answer the reader |
begin_reader_pairing |
POST /kahunacart/providers/{slug}/readers/pairing |
kahunacart.settings |
slug required; label, location_ref optional |
For a provider whose readers pair from a code the provider makes (pairing app_code on list_provider_readers, like Square): ask the provider for a code, and answer the pairing with its id, the code to type into the reader, its status (waiting) and when it expires |
get_reader_pairing |
GET /kahunacart/providers/{slug}/readers/pairing/{pairing} |
kahunacart.settings |
slug, pairing, all required |
How a pairing from begin_reader_pairing stands: waiting for the code to be typed into a reader, expired (start again), or paired, with the reader that took it |
forget_provider_reader |
DELETE /kahunacart/providers/{slug}/readers/{reader} |
kahunacart.settings |
slug, reader, all required |
Take a card reader off a payment provider's account |
start_reader_payment |
POST /kahunacart/orders/{id}/reader-payments |
kahunacart.orders.manage |
id, provider, reader required; amount_minor, amount, idempotency_key optional |
Put an amount on a card reader for an order and come back at once with the payment pending; the customer then taps, inserts or swipes on the reader |
get_reader_payment |
GET /kahunacart/orders/{id}/reader-payments/{tid} |
kahunacart.orders.manage |
id, tid, all required |
How a card payment on a reader stands as the store has it: pending, checking, cancelling, succeeded, failed (with the reason in plain words), cancelled or timed_out, with the amount, the reader, the provider's reference and order_settled (false while the money is recorded and the order is still being completed) |
refresh_reader_payment |
POST /kahunacart/orders/{id}/reader-payments/{tid}/refresh |
kahunacart.orders.manage |
id, tid, all required |
Ask the provider how a card payment on a reader stands and apply the answer exactly as its webhook would, so a store the provider cannot reach still settles; then answer the payment |
cancel_reader_payment |
POST /kahunacart/orders/{id}/reader-payments/{tid}/cancel |
kahunacart.orders.manage |
id, tid, all required |
Clear the reader while the payment is still pending, because the customer changed their mind or the wrong amount was sent |
abandon_reader_payment |
POST /kahunacart/orders/{id}/reader-payments/{tid}/abandon |
kahunacart.orders.manage |
id, tid, all required |
Give up on a card payment on a reader that the provider cannot say anything about: a start whose answer never came back (checking), or a payment whose provider cannot be reached |
list_checkout_fields |
GET /kahunacart/checkout-fields |
kahunacart.products.manage |
none | Every field the checkout form draws, in the order it draws them, with the three condition documents that decide when each is required, when it is hidden and what a valid answer is — plus the attribute vocabulary those conditions may be written against |
create_checkout_field |
POST /kahunacart/checkout-fields |
kahunacart.products.manage |
key, label_key required; type, section, position, options, required, hidden, validation, enabled optional |
Add a field to the checkout form |
update_checkout_field |
PUT /kahunacart/checkout-fields/{id} |
kahunacart.products.manage |
id required; key, label_key, type, section, position, options, required, hidden, validation, enabled optional |
Change a checkout field |
delete_checkout_field |
DELETE /kahunacart/checkout-fields/{id} |
kahunacart.products.manage |
id, required |
Delete a field a merchant or an add-on added |
get_country_locale |
GET /kahunacart/countries/{code}/locale |
kahunacart.products.manage |
code, required |
What one country expects of a postal address: the layout it prints in, the postcode grammar, the phone grammar and which address fields it will not do without |
update_country_locale |
PUT /kahunacart/countries/{code}/locale |
kahunacart.products.manage |
code required; address_format, postcode_pattern, phone_pattern, required_fields, enabled, restore optional |
Fix one country without waiting for a release |
get_report_comparison |
GET /kahunacart/reports/comparison |
kahunacart.reports |
currency_picker, currency, from, to, comparison, segment, filters |
Every published metric for a date range, compared with the period before it or with the same period a year earlier, and the change in basis points (250 means +2.5%) |
get_report_vocabulary |
GET /kahunacart/reports/vocabulary |
kahunacart.reports |
none | Every field a report can be filtered or broken down by, with its type, the operators it accepts, and whether a breakdown on it can be read from the rollup |
list_report_views |
GET /kahunacart/report-views |
kahunacart.reports |
report_code, required |
The views the calling user has saved for one report, with their filters, columns, range and comparison mode, and which one is their default |
create_report_view |
POST /kahunacart/report-views |
kahunacart.reports |
report_code, name required; filters, columns, range, is_default optional |
Save a named set of filters, columns, range and comparison mode for a report |
update_report_view |
PATCH /kahunacart/report-views/{id} |
kahunacart.reports |
id required; report_code, name, filters, columns, range, is_default optional |
Change a saved view's filters, columns, range or default flag |
delete_report_view |
DELETE /kahunacart/report-views/{id} |
kahunacart.reports |
id, required |
Delete one of your own saved views |
get_view_config |
GET /kahunacart/view-configs/{entity} |
kahunacart.reports |
entity, required |
Every column an admin list screen offers, with whether the calling user sees it, and the store's own names for those fields |
set_view_config |
PUT /kahunacart/view-configs/{entity} |
kahunacart.reports |
entity required; columns optional |
Save the calling user's column choice for a list screen |
set_property_labels |
PUT /kahunacart/property-labels |
kahunacart.settings |
labels |
Replace the store's own names for fields |
list_dashboard_alerts |
GET /kahunacart/alerts |
kahunacart.orders.view |
dismissed |
What needs attention right now, for the calling user: orders waiting to be paid, background jobs that are not running, stock running out, a report rollup that should be rebuilt, and anything other plugins have registered |
dismiss_dashboard_alert |
POST /kahunacart/alerts/{id}/dismiss |
kahunacart.orders.view |
id required; restore optional |
Hide one alert for the calling user |
rebuild_dashboard_alerts |
POST /kahunacart/alerts/rebuild |
kahunacart.settings |
none | Clear every cached alert check so they all run again on the next read |
import_config |
POST /kahunacart/config/import |
kahunacart.settings |
document required; dry_run optional |
Apply a configuration export: the plugin's YAML, plus whichever of the tax zones and rates, shipping zones, methods and collection points, coupons, campaigns, sales, settings rows and saved views the document carries |
detect_config_contradictions |
GET /kahunacart/config/contradictions |
kahunacart.settings |
none | Settings that are each valid on their own but wrong together: prices marked as including tax when no tax rate is set, tax-inclusive prices with delivery left untaxed, or checkout still on after the basket was turned off |
apply_recommended_settings |
POST /kahunacart/config/contradictions/apply |
kahunacart.settings |
code, required |
Write one rule's recommended values into the store's own configuration file |
get_agentic_status |
GET /kahunacart/agentic/status |
kahunacart.settings |
none | Whether this store is ready for AI shopping agents |
rotate_ucp_signing_key |
POST /kahunacart/agentic/keys/rotate |
kahunacart.settings |
none | Make a new ES256 signing key and retire the current one |
list_agent_sessions |
GET /kahunacart/agentic/sessions |
kahunacart.orders.view |
status, sort, dir, q, page, per_page |
UCP checkout and cart sessions and MCP carts, newest first: the kind, the status, the protocol, the platform it belongs to, the cart or order it points at (with the order number once the order is placed) and its dates |
list_feeds |
GET /kahunacart/feeds |
kahunacart.settings |
none | The Google Merchant Center and OpenAI product feed files, one per language (and, on a store that sells in more than one currency, one per currency, each row naming its currency): whether each is switched on, the URL to register it at, whether that URL needs ?key= (the key itself is not returned), when it was last built, how many rows it has and whether it is older than feeds.max_age |
rebuild_feeds |
POST /kahunacart/feeds/rebuild |
kahunacart.settings |
none | Queue a build of every enabled feed for every language, due now |
mark_shipment_delivered |
POST /kahunacart/orders/{id}/shipments/{sid}/delivered |
kahunacart.orders.manage |
id, sid, all required |
Record that a parcel arrived |
list_exchange_rates |
GET /kahunacart/currencies/rates |
kahunacart.settings |
none | The exchange rates between the store's base currency and each currency it sells in |
look_up_exchange_rates |
GET /kahunacart/currencies/rates/lookup |
kahunacart.settings |
none | Ask Frankfurter once for today's market rates between the store's base currency and each currency it sells in, and write nothing |
set_exchange_rate |
PUT /kahunacart/currencies/rates/{quote} |
kahunacart.settings |
quote, rate, all required |
Set the rate between the base currency and one of the store's other currencies by hand |
delete_exchange_rate |
DELETE /kahunacart/currencies/rates/{quote} |
kahunacart.settings |
quote, required |
Remove the stored rate for one currency, whether a person typed it or a source fetched it |
refresh_exchange_rates |
POST /kahunacart/currencies/rates/refresh |
kahunacart.settings |
none | Fetch the rates from the configured source now, instead of waiting for the daily job |
accept_exchange_rate |
POST /kahunacart/currencies/rates/{quote}/accept |
kahunacart.settings |
quote, new, all required |
Accept a rate a refresh held because it moved more than currencies.rates.hold_move_percent from the rate in use |
keep_old_exchange_rate |
DELETE /kahunacart/currencies/rates/{quote}/held |
kahunacart.settings |
quote, required |
Turn down a held rate and keep the one in use |
set_currency_pricing |
PUT /kahunacart/currencies/pricing |
kahunacart.settings |
pricing required; missing_price optional |
Save manual or converted pricing per enabled currency, with markup and rounding |
get_report_currency |
GET /kahunacart/reports/currency |
kahunacart.reports |
none | Read the saved currency selection and available completed-order currencies, base first |
set_report_currency |
PUT /kahunacart/reports/currency |
kahunacart.reports |
currency, required |
Remember a known currency code or all for exact frozen-rate totals in base currency |
check_payment_method_account |
POST /kahunacart/payment-methods/{id}/check-account |
kahunacart.settings |
id, required |
Check the saved credentials for this method’s account without returning secrets |
setup_payment_method_webhook |
POST /kahunacart/payment-methods/{id}/setup-webhook |
kahunacart.settings |
id required; reset optional |
Register this method’s own endpoint and store its signing secret encrypted |
Six things you can ask for
| What you type | What the assistant calls |
|---|---|
| "How many orders are unpaid from last week?" | kahunacart_list_orders. Every row carries payment_status and created_at, and the assistant filters and counts. The list has no status filter of its own |
| "Add a Material attribute with cotton and linen, and set it on every tee." | kahunacart_create_attribute with type: select, then kahunacart_list_products with q=tee, then kahunacart_update_product on each. attributes merges, so nothing else on the product changes |
| "Refund order 482 in full." | kahunacart_list_orders with q=482 to turn the order number into an id, then kahunacart_refund_order with no amount |
| "Which products are low on stock?" | kahunacart_report_low_stock, threshold 5 unless you name another |
| "Create a 20% summer sale on the Apparel category until 31 August." | kahunacart_list_categories for the id, then kahunacart_create_sale with one rule of scope: category, mode: percent, percent: "20" and an expires_at timestamp |
| "Is Stripe still connected?" | kahunacart_check_provider. A failed check is a successful call reporting ok: false with the provider's message |
Permissions and safety
Three controls govern an assistant's access to your store.
The key decides which tools exist. GET /mcp/tools leaves out every tool whose permission the key does not hold. A key with only kahunacart.reports gets no refund tool. A super admin sees everything.
Every call is checked twice. grav-mcp checks the permission before calling, and the API plugin checks it again on the route. A tool that slipped through is still refused with 403.
Annotations tell the client what a tool is. Every GET is read-only. Deletes are destructive, and so are refund_order, cancel_order and revoke_download. A client can then ask before running one.
A store with demo.readonly on refuses every write with 403 before the handler runs, whatever the key holds. Reads are never blocked.
Tip
Give the assistant its own API key rather than reusing yours, with only the permissions the job needs. You can then revoke it without affecting your own access.
| Goal | Permissions on the key's account |
|---|---|
| Answer questions about the numbers | api.access, kahunacart.reports |
| Answer questions about orders and customers | api.access, kahunacart.orders.view, kahunacart.customers.view |
| Run the catalog | api.access, kahunacart.products.manage |
| Run promotions | api.access, kahunacart.sales.manage, kahunacart.settings |
| Process orders, without refunds | api.access, kahunacart.orders.view, kahunacart.orders.manage |
| Process orders, refunds included | the row above plus kahunacart.orders.refund |
| Export or restore the store | api.access, kahunacart.export or kahunacart.restore, and each part's own permission, such as kahunacart.customers.view for the customers. See Export and restore |
| Everything the admin can do | api.access and every kahunacart.* permission |
To limit which plugins publish tools, set --plugin-tools or GRAV_MCP_PLUGIN_TOOLS. all is the default, none turns plugin tools off, and a comma-separated list of plugin slugs loads only those:
npx grav-mcp --url https://mysite.com/api --key grav_abc123 --plugin-tools kahunacart
Two conventions matter when reading the results.
Money goes in as decimal strings in the store currency ("19.99") and comes back as minor-unit integers (price_minor, total_minor, refunded_minor). Read currency_exponent from get_config to convert. See Money conventions.
attributes on a product update is a merge keyed by attribute slug. A slug you do not send keeps its value; a slug sent as null is removed. Every other list-valued field replaces its whole set.
Selling a grouped product as a set takes sell_as_set. create_product and update_product accept sell_as_set (true or false) on a grouped product or a kit. On, the product's own variant price is the bundle price of a full set of its children, and the children stay separate lines. Set the price through variants, as for any product. get_product answers set_price for these products: parts_minor, price_minor, saving_minor and sells_as_set, in the store's own currency. search_variants also takes ids, a comma-separated list such as 24,25, and answers those variants in that order instead of searching.
Add-ons
The Licenses, Subscriptions, Newsletters, Shipping, Bookings and Rentals add-ons ship their own mcp.yaml manifests. Installing one adds its tools to the same session, once the plugin is enabled and the tools are refreshed.
Licenses
The admin tools need kahunacart.licenses. Three of them call the public validation routes your software already uses, so an assistant can check a customer's key with no permission at all. What each does to a key is on Licenses.
Licenses publishes 21 tools over 32 admin routes, behind 1 permission. An assistant calls each one by its name below with kahunacart_licenses_ in front of it.
These 4 routes are not published as tools: POST /kahunacart/licenses/public/activate, POST /kahunacart/licenses/public/deactivate, GET /kahunacart/licenses/public/file, GET /kahunacart/licenses/public/release. Nor are the 7 OPTIONS preflight routes.
| Tool | Route | Permission | Arguments | What it does |
|---|---|---|---|---|
list_licenses |
GET /kahunacart/licenses |
kahunacart.licenses |
q, status, product_id, variant_id, customer_id, order_id, page, per_page, sort, dir |
Search issued license keys, newest first, each with its status, its project name, the seats in use against the seats allowed, its product, its order, its customer and its two dates |
get_license |
GET /kahunacart/licenses/{id} |
kahunacart.licenses |
id, required |
One key with everything it knows about itself: the row and its effective status, every machine it has been activated on including the seats already freed, the product, variant, customer and order behind it, the licensing rule that governs it, every add-on that has been bought onto it, and a preview of the delivery copy and license files the customer receives |
issue_license |
POST /kahunacart/licenses |
kahunacart.licenses |
product_id required; variant_id, customer_id, email, seats, term_days, updates_days, meta, note optional |
Mint a key with no order behind it, for a comp, a review copy or a replacement for somebody who bought through another channel, and return it the way get_license does |
update_license |
PATCH /kahunacart/licenses/{id} |
kahunacart.licenses |
id required; label, seats, expires_at, updates_until, customer_id, meta, note optional |
Change what a merchant is allowed to change on an issued key: the project name, the seat allowance, the expiry, the updates window, the private note, the metadata and who it belongs to |
suspend_license |
POST /kahunacart/licenses/{id}/suspend |
kahunacart.licenses |
id required; note optional |
Switch a key off for now, which is the one to use for a chargeback under investigation or a customer in arrears |
reinstate_license |
POST /kahunacart/licenses/{id}/reinstate |
kahunacart.licenses |
id, required |
Undo a suspension and put the key back to active with its activations untouched |
revoke_license |
POST /kahunacart/licenses/{id}/revoke |
kahunacart.licenses |
id, reason, all required |
Kill a key permanently and free every seat it holds |
regenerate_license |
POST /kahunacart/licenses/{id}/regenerate |
kahunacart.licenses |
id, required |
Replace a leaked key |
free_license_seat |
DELETE /kahunacart/licenses/{id}/activations/{activationId} |
kahunacart.licenses |
id, activationId, all required |
Release one seat on the customer's behalf, which is the support action for a machine that died still holding one |
list_license_rules |
GET /kahunacart/licenses/products |
kahunacart.licenses |
none | Every licensing rule in the store with the catalog beside it, plus the rule kinds, the subscription endings, the placeholder legend the delivery copy is written in and the store's default key layout with a sample key |
order_issuance |
GET /kahunacart/licenses/issuance/{orderId} |
kahunacart.licenses |
orderId, required |
Whether one order's lines earn license keys, how many it has been given already, and which event the store mints them on — paid waits for the money to land, completed issues the moment the order is placed |
save_product_rule |
PUT /kahunacart/licenses/products/{productId} |
kahunacart.licenses |
productId required; enabled, kind, renews_product_id, attaches_product_id, keys_per_qty, seats, seats_per_qty, term_days, updates_days, on_subscription_end, key_prefix, key_format, instructions, meta, grants, requires, includes, files optional |
Create or replace the product-wide licensing rule, which governs every variant that has no rule of its own |
save_variant_rule |
PUT /kahunacart/licenses/products/{productId}/{variantId} |
kahunacart.licenses |
productId, variantId required; enabled, kind, renews_product_id, attaches_product_id, keys_per_qty, seats, seats_per_qty, term_days, updates_days, on_subscription_end, key_prefix, key_format, instructions, meta, grants, requires, includes, files optional |
Create or replace one variant's own licensing rule, which replaces the product-wide rule for that variant and leaves every other variant on the product's |
delete_product_rule |
DELETE /kahunacart/licenses/products/{productId} |
kahunacart.licenses |
productId, required |
Remove the product-wide licensing rule, so the product stops earning keys unless one of its variants has a rule of its own |
delete_variant_rule |
DELETE /kahunacart/licenses/products/{productId}/{variantId} |
kahunacart.licenses |
productId, variantId, all required |
Remove one variant's own licensing rule, which puts that variant back on the product-wide rule where there is one |
get_license_settings |
GET /kahunacart/licenses/settings |
kahunacart.licenses |
none | The store-wide licensing settings every rule falls back to: whether licensing is on, whether keys are issued when payment lands or when the order completes, the issuance defaults, the key layout with a sample key drawn by the class that mints them, the public API switches and rate limits, and the delivery copy and files |
update_license_settings |
PATCH /kahunacart/licenses/settings |
kahunacart.licenses |
enabled, issue_on, order_data_key, key, defaults, api, delivery |
Save store-wide licensing settings to the plugin's config file |
rotate_signing_secret |
POST /kahunacart/licenses/settings/rotate-secret |
kahunacart.licenses |
none | Mint a new API signing secret, write it to the plugin config and return it once |
validate_key |
POST /kahunacart/licenses/public/validate |
none (public route) | key required; instance, product optional |
Ask whether a license key is good, the way a customer's own software does, and get back the status, the product, the seat counts, the two dates, the key's metadata and covers, every product and package slug the key is good for |
check_updates |
GET /kahunacart/licenses/public/updates |
none (public route) | key, released_at, all required |
Ask whether a key covers a release that came out at a given moment, which is the question an updater asks before it downloads |
latest_release |
GET /kahunacart/licenses/public/latest |
none (public route) | key, required |
Ask which store-hosted build a key is entitled to, with its version, its changelog, and each file's size, download address and sha256: checksum |
Subscriptions
Reads need kahunacart.subscriptions.view, writes kahunacart.subscriptions.manage. A renewal is an ordinary order, so marking one paid is kahunacart_mark_order_paid from the base plugin. What each action means is on Subscriptions.
Subscriptions publishes 17 tools over 17 admin routes, behind 2 permissions. An assistant calls each one by its name below with kahunacart_subscriptions_ in front of it.
| Tool | Route | Permission | Arguments | What it does |
|---|---|---|---|---|
list_plans |
GET /kahunacart/subscription-plans |
kahunacart.subscriptions.view |
none | Every subscription plan in the store, each with the product and variant it sells, its billing interval, its free trial, its billing mode, its renewal variant, its grace period, its own price and label if it has them, what happens to its place when a subscription ends, the customer group it puts live subscribers in, its position among the variant's billing options, its per-provider price references and how many live subscriptions are running on it |
get_plan |
GET /kahunacart/subscription-plans/{id} |
kahunacart.subscriptions.view |
id, required |
One plan with the variant it sells, the variant a renewal carries, its interval, trial, billing mode, grace period, price, label, stock setting, customer group, sort position, per-provider references and its live subscription count, plus the list of registered payment providers |
list_plan_variants |
GET /kahunacart/subscription-plans/variants |
kahunacart.subscriptions.view |
q |
Every purchasable variant in the catalog with its price in minor units, its product status, plan_ids listing every plan it already carries with the default first, and plan_id, the default plan, or null when it carries none |
create_plan |
POST /kahunacart/subscription-plans |
kahunacart.subscriptions.manage |
variant_id required; interval_unit, interval_count, trial_days, billing_mode, renewal_variant_id, grace_days, price_minor, currency_amounts, label, stock_on_end, sort, customer_group, provider_refs, enabled optional |
Start selling an existing product variant on a calendar |
update_plan |
PATCH /kahunacart/subscription-plans/{id} |
kahunacart.subscriptions.manage |
id required; interval_unit, interval_count, trial_days, billing_mode, renewal_variant_id, grace_days, price_minor, currency_amounts, label, stock_on_end, sort, customer_group, enabled optional |
Change a plan's calendar |
delete_plan |
DELETE /kahunacart/subscription-plans/{id} |
kahunacart.subscriptions.manage |
id, required |
Remove a plan |
set_plan_provider_refs |
PUT /kahunacart/subscription-plans/{id}/provider-refs/{provider} |
kahunacart.subscriptions.manage |
id, provider, refs, all required |
Write one payment platform's price or product ids onto a plan, so a provider-mode checkout opens the agreement against a price created by hand at the platform |
list_subscriptions |
GET /kahunacart/subscriptions |
kahunacart.subscriptions.view |
q, status, billing_mode, provider, plan_id, customer_id, sort, dir, page, per_page |
A page of subscriptions, each with its status, plan, customer, billing mode, payment provider, current period, next billing date and frozen amount |
get_subscription |
GET /kahunacart/subscriptions/{id} |
kahunacart.subscriptions.view |
id, required |
One subscription in full: the row with its status, frozen amount, current period, next billing date, billing mode, provider and failed-attempt count; the plan behind it; every order it has produced, which are ordinary KahunaCart orders with numbers, receipts and invoices of their own; and the whole timeline |
get_metrics |
GET /kahunacart/subscriptions/metrics |
kahunacart.subscriptions.view |
none | The store's recurring-revenue figures right now: monthly and annual recurring revenue per currency, the counts of active, trialing, past-due and paused subscriptions, and how many plans exist |
cancel_subscription |
POST /kahunacart/subscriptions/{id}/cancel |
kahunacart.subscriptions.manage |
id required; at_period_end, reason optional |
End a subscription, at the end of the period already paid for or immediately |
uncancel_subscription |
POST /kahunacart/subscriptions/{id}/uncancel |
kahunacart.subscriptions.manage |
id, required |
Clear a cancellation scheduled for the end of the period, which is what a retention call ends in |
pause_subscription |
POST /kahunacart/subscriptions/{id}/pause |
kahunacart.subscriptions.manage |
id required; reason optional |
Stop the calendar without ending the subscription |
resume_subscription |
POST /kahunacart/subscriptions/{id}/resume |
kahunacart.subscriptions.manage |
id, required |
Start the calendar again on a paused subscription, returning it to active — or call off a pause that was scheduled and has not taken effect, which is the same call at the platform and leaves the subscription running as it was |
extend_subscription |
POST /kahunacart/subscriptions/{id}/extend |
kahunacart.subscriptions.manage |
id, days, note, all required |
Push the current period out by a number of days without billing for them, moving the next billing date with it |
add_subscription_note |
POST /kahunacart/subscriptions/{id}/note |
kahunacart.subscriptions.manage |
id, note, all required |
Write a free-text note onto a subscription's timeline |
resend_pay_link |
POST /kahunacart/subscriptions/{id}/resend-pay-link |
kahunacart.subscriptions.manage |
id, required |
Queue the pay-link email again for the renewal invoice a customer already owes, and record the resend on the timeline |
Newsletters
Reads need kahunacart.newsletters.view, writes kahunacart.newsletters.manage. Starting, resuming or test-sending a campaign needs kahunacart.newsletters.send on top of .manage. What each action means is on Newsletters.
Newsletters publishes 77 tools over 85 admin routes, behind 3 permissions. An assistant calls each one by its name below with kahunacart_newsletters_ in front of it.
These 8 routes are not published as tools: GET /kahunacart/newsletters/campaigns/{id}/preview, GET /kahunacart/newsletters/campaigns/{id}/sends/{send}/view, POST /kahunacart/newsletters/imports, POST /kahunacart/newsletters/imports/preview, GET /kahunacart/newsletters/subscribers/export, GET /kahunacart/newsletters/labels, GET /kahunacart/newsletters/reports/{type}.csv, GET /kahunacart/newsletters/templates/{id}/preview.
| Tool | Route | Permission | Arguments | What it does |
|---|---|---|---|---|
list_imports |
GET /kahunacart/newsletters/imports |
kahunacart.newsletters.view |
none | The recent runs of the CSV importer, newest first, with the file each one read, the list it wrote to, where the run got to and the four counters that say what it did: how many addresses were new, how many were already here, how many were skipped and how many ended in a suppression rather than a subscriber |
get_import |
GET /kahunacart/newsletters/imports/{id} |
kahunacart.newsletters.view |
id, required |
One import run with its counters, its status, the reason it failed if it did, and how far through the file it is as a fraction — which is null until it has read its first chunk, because a run that has not started knows nothing and reporting zero would be reporting something |
preview_customer_import |
POST /kahunacart/newsletters/imports/customers/preview |
kahunacart.newsletters.view |
filter, limit |
How many of the store's own customers match a filter and what would happen to each of them, without writing anything |
start_customer_import |
POST /kahunacart/newsletters/imports/customers |
kahunacart.newsletters.manage |
list_id, mode, tag, filter |
Put the store's own customers on a list, as one import run with the same counters and the same history as a CSV import |
get_overview |
GET /kahunacart/newsletters/overview |
kahunacart.newsletters.view |
none | The front screen of the newsletter in one call: how many people are in each subscriber status and how many of them a campaign could actually reach, how many addresses are suppressed, thirty daily buckets of new signups, the last five campaigns with their open and click rates, the last twenty that actually went out with their open, click, unsubscribe, bounce and complaint rates side by side, how many campaigns are in each status, and every alert the store should act on before it sends anything else |
get_settings |
GET /kahunacart/newsletters/settings |
kahunacart.newsletters.view |
none | What this store is configured to send and whether it can |
list_campaigns |
GET /kahunacart/newsletters/campaigns |
kahunacart.newsletters.view |
status, sort, dir, q, page, per_page |
Every campaign, newest first, with its status, the list it goes to, its counters and its open and click rates |
get_campaign |
GET /kahunacart/newsletters/campaigns/{id} |
kahunacart.newsletters.view |
id, required |
One campaign with everything above its tables: the subject, the preheader, the Markdown body, the layout, the list, the sender overrides, the coupon, whether opens and clicks are counted, every counter, the open and click rates, and a breakdown of its recipients by what happened to each of them |
count_campaign_audience |
GET /kahunacart/newsletters/campaigns/audience |
kahunacart.newsletters.view |
lists, segment |
How many people a campaign to these lists, narrowed by this segment, would mail right now, and up to twenty of them by name, before any campaign exists |
create_campaign |
POST /kahunacart/newsletters/campaigns |
kahunacart.newsletters.manage |
name required; subject, preheader, body_md, layout, list_id, list_ids, segment_id, template_id, from_name, from_email, reply_to, coupon_code, track_opens, track_clicks, subject_b, ab_sample_percent, ab_window_minutes, utm optional |
Create a campaign in draft |
update_campaign |
PATCH /kahunacart/newsletters/campaigns/{id} |
kahunacart.newsletters.manage |
id required; name, subject, preheader, body_md, layout, list_id, list_ids, segment_id, from_name, from_email, reply_to, coupon_code, track_opens, track_clicks, subject_b, ab_sample_percent, ab_window_minutes, utm optional |
Change a campaign that has not gone anywhere yet |
delete_campaign |
DELETE /kahunacart/newsletters/campaigns/{id} |
kahunacart.newsletters.manage |
id, required |
Remove a campaign and everything it recorded |
list_campaign_recipients |
GET /kahunacart/newsletters/campaigns/{id}/sends |
kahunacart.newsletters.view |
id required; status, sort, dir, q, page, per_page optional |
Who this campaign was sent to and what happened to each of them, a page at a time |
list_campaign_links |
GET /kahunacart/newsletters/campaigns/{id}/links |
kahunacart.newsletters.view |
id, required |
Every link in the campaign with how many times it was pressed and what share of the people it reached that is, ordered by clicks so the link that worked is at the top rather than buried under the header image nobody pressed |
get_campaign_timeline |
GET /kahunacart/newsletters/campaigns/{id}/timeline |
kahunacart.newsletters.view |
id, required |
Four cumulative series — sent, delivered, opened, clicked — in one-hour buckets from the moment the first message went out, over the first forty-eight hours |
preview_campaign |
POST /kahunacart/newsletters/campaigns/{id}/preview |
kahunacart.newsletters.manage |
id required; subject, preheader, body_md, layout, coupon_code optional |
Render a campaign the way one person would receive it, through the real layout with the real Markdown and the real merge pass, and answer the HTML, the plain-text part and the subject |
send_campaign_test |
POST /kahunacart/newsletters/campaigns/{id}/test |
kahunacart.newsletters.send |
id, email, all required |
Mail one copy of a campaign to an address, with the subject prefixed and nothing counted, so it can be looked at in a real inbox before anybody else sees it |
start_campaign |
POST /kahunacart/newsletters/campaigns/{id}/start |
kahunacart.newsletters.send |
id required; scheduled_at optional |
Put this campaign in inboxes, now or at a moment |
pause_campaign |
POST /kahunacart/newsletters/campaigns/{id}/pause |
kahunacart.newsletters.manage |
id, required |
Stop a sending or scheduled campaign within one message |
resume_campaign |
POST /kahunacart/newsletters/campaigns/{id}/resume |
kahunacart.newsletters.send |
id, required |
Carry on with a paused campaign, or restart one that stopped itself after too many refused messages |
cancel_campaign |
POST /kahunacart/newsletters/campaigns/{id}/cancel |
kahunacart.newsletters.manage |
id, required |
Stop a campaign for good |
catch_up_campaign |
POST /kahunacart/newsletters/campaigns/{id}/catch-up |
kahunacart.newsletters.send |
id, required |
Send a campaign that has already gone out to the people who joined its list afterwards and never received it |
preview_campaign_catch_up |
GET /kahunacart/newsletters/campaigns/{id}/catch-up |
kahunacart.newsletters.view |
id, required |
How many people would be mailed if this campaign were sent on to the people who joined its list after it went out, and up to twenty of them by name |
retry_failed_campaign_sends |
POST /kahunacart/newsletters/campaigns/{id}/retry-failed |
kahunacart.newsletters.send |
id required; send_id optional |
Try the failed sends of a campaign that has gone out again: every failed send, or only the one named by send_id |
list_subscribers |
GET /kahunacart/newsletters/subscribers |
kahunacart.newsletters.view |
q, status, source, list, list_status, tag, campaign_id, campaign_received, sort, dir, page, per_page |
Who is on the mailing list, with the lists and tags each person carries, how they joined, when they agreed and what state they are in |
get_subscriber |
GET /kahunacart/newsletters/subscribers/{id} |
kahunacart.newsletters.view |
id, required |
One person with the whole record behind them: the consent — when they agreed, from what address, and whether the store still holds the sentence they were shown — every list they hold a membership on and its state, their tags, the import they came in with if they came in with one, the customer record they are linked to if there is one, whether the store has been told never to mail the address, and every campaign they were part of with what happened to it |
export_subscriber_data |
GET /kahunacart/newsletters/subscribers/{id}/export |
kahunacart.newsletters.manage |
id, required |
Everything held about one person, as one JSON document, for answering somebody who asks what is kept about them: their profile, every list membership with its status and dates, their tags, the full consent history (each signup, confirmation, admin add or invite, import, unsubscribe and return, with the address and browser for the changes they made themselves and the note for an admin's), every message they were sent with when it was opened and each link they pressed, what the mail provider reported about them (deliveries, bounces, complaints), their automation walks, any suppression on their address, the import runs that brought them in, and any records the site adds of its own |
add_subscriber |
POST /kahunacart/newsletters/subscribers |
kahunacart.newsletters.manage |
email required; name, list_id, tags, language, mode, note optional |
Add one person to a list by address, as source "admin" |
update_subscriber |
PATCH /kahunacart/newsletters/subscribers/{id} |
kahunacart.newsletters.manage |
id required; name, language, lists, tags optional |
Change somebody's name, language, lists or tags |
unsubscribe_subscriber |
POST /kahunacart/newsletters/subscribers/{id}/unsubscribe |
kahunacart.newsletters.manage |
id, required |
Take one person off every list, recorded as done by the store rather than by them |
resubscribe_subscriber |
POST /kahunacart/newsletters/subscribers/{id}/resubscribe |
kahunacart.newsletters.manage |
id required; list_id, note optional |
Undo an unsubscribe, for somebody who has asked you to |
confirm_subscriber |
POST /kahunacart/newsletters/subscribers/{id}/confirm |
kahunacart.newsletters.manage |
id, required |
Move a person who is waiting to confirm onto their lists, for the one whose confirmation email went to spam or never arrived and who has since asked, in writing, to be on the list |
delete_subscriber |
DELETE /kahunacart/newsletters/subscribers/{id} |
kahunacart.newsletters.manage |
id, required |
Remove one person from the mailing list entirely — their memberships, their tags, every send they were part of and every click they made |
bulk_subscribers |
POST /kahunacart/newsletters/subscribers/bulk |
kahunacart.newsletters.manage |
action required; ids, filter, all, after, max_id, addresses, list_id, mode, note, tag, tag_id optional |
One action over many people: add them to a list, take them off one, add or remove a tag, unsubscribe them from everything, or delete them |
list_lists |
GET /kahunacart/newsletters/lists |
kahunacart.newsletters.view |
none | Every list in the store with the number of people a campaign to it would reach — the subscribed ones, not the pending or the departed |
create_list |
POST /kahunacart/newsletters/lists |
kahunacart.newsletters.manage |
code, name required; description, double_opt_in, is_default, public, position, from_name, from_email optional |
Add a topic people can subscribe to and leave separately |
update_list |
PATCH /kahunacart/newsletters/lists/{id} |
kahunacart.newsletters.manage |
id required; name, description, double_opt_in, is_default, public, position, from_name, from_email optional |
Change a list's name, description, confirmation setting, default flag, whether it is offered on the preference center, or sender overrides |
list_tags |
GET /kahunacart/newsletters/tags |
kahunacart.newsletters.view |
none | Every tag in the store with how many people carry it |
create_tag |
POST /kahunacart/newsletters/tags |
kahunacart.newsletters.manage |
code required; label optional |
Add a tag |
delete_tag |
DELETE /kahunacart/newsletters/tags/{id} |
kahunacart.newsletters.manage |
id, required |
Remove a tag from the store and from everybody carrying it |
list_templates |
GET /kahunacart/newsletters/templates |
kahunacart.newsletters.view |
active |
The starting points a merchant keeps — a subject, a preheader, some Markdown and a layout |
get_template |
GET /kahunacart/newsletters/templates/{id} |
kahunacart.newsletters.view |
id, required |
One template with its subject, preheader, Markdown body and layout, plus the merge variables a campaign made from it may use |
create_template |
POST /kahunacart/newsletters/templates |
kahunacart.newsletters.manage |
name required; subject, preheader, body_md, layout, is_active optional |
Save the wording and layout worth reusing, so every campaign after this one starts from it |
update_template |
PATCH /kahunacart/newsletters/templates/{id} |
kahunacart.newsletters.manage |
id required; name, subject, preheader, body_md, layout, is_active optional |
Change a template |
delete_template |
DELETE /kahunacart/newsletters/templates/{id} |
kahunacart.newsletters.manage |
id, required |
Remove a starting point |
preview_template |
POST /kahunacart/newsletters/templates/{id}/preview |
kahunacart.newsletters.manage |
id required; subject, preheader, body_md, layout optional |
Render a template the way a campaign made from it would arrive, through the real layout with a sample subscriber and tracking off, and answer the HTML, the plain-text part and the subject |
list_suppressions |
GET /kahunacart/newsletters/suppressions |
kahunacart.newsletters.view |
reason, sort, dir, q, page, per_page |
The addresses this store will never mail again, whatever any list or import says about them, with why each one is here and when |
add_suppression |
POST /kahunacart/newsletters/suppressions |
kahunacart.newsletters.manage |
email required; reason, note optional |
Tell the store never to mail an address again, for somebody who asked directly rather than through a link |
remove_suppression |
DELETE /kahunacart/newsletters/suppressions/{hash} |
kahunacart.newsletters.manage |
hash, required |
Remove one suppression, addressed by the SHA-256 of the address — which is how the row is keyed, because that is the half that survives an erasure |
list_segments |
GET /kahunacart/newsletters/segments |
kahunacart.newsletters.view |
none | Every saved segment, with the count of people it is about, when that count was taken, and how many campaigns are pointed at it |
get_segment |
GET /kahunacart/newsletters/segments/{id} |
kahunacart.newsletters.view |
id, required |
One segment with its full condition document, its cached count and the list it is narrowed to |
get_segment_vocabulary |
GET /kahunacart/newsletters/segments/vocabulary |
kahunacart.newsletters.view |
none | Every field a segment condition may name, with its type, the operators it accepts, the group it belongs to and — for the fields that are chosen from a list — the options themselves |
preview_segment |
POST /kahunacart/newsletters/segments/preview |
kahunacart.newsletters.view |
list_id, definition |
Answer a condition document straight away: how many people match it, ten of them, and how many matched but cannot be mailed — suppressed, gone from the list, or never confirmed |
create_segment |
POST /kahunacart/newsletters/segments |
kahunacart.newsletters.manage |
name required; description, list_id, definition optional |
Save a condition document under a name |
update_segment |
PATCH /kahunacart/newsletters/segments/{id} |
kahunacart.newsletters.manage |
id required; name, description, list_id, definition optional |
Edit a segment's name, description, list or conditions |
delete_segment |
DELETE /kahunacart/newsletters/segments/{id} |
kahunacart.newsletters.manage |
id, required |
Remove a segment |
count_segment |
POST /kahunacart/newsletters/segments/{id}/count |
kahunacart.newsletters.manage |
id, required |
Work out how many people a saved segment is about, right now, and store the answer with the moment it was taken |
get_customer_subscriber |
GET /kahunacart/newsletters/customers/{id}/subscriber |
kahunacart.newsletters.view |
id, required |
Given a KahunaCart customer id, the subscriber record linked to it: their address and status, whether the store has been told never to mail them, every list they are on with how they got there, and the last campaign they were actually sent — with the date, and whether they opened it, clicked in it or it bounced |
check_deliverability |
GET /kahunacart/newsletters/deliverability |
kahunacart.newsletters.view |
none | Nine checks over the store's sending setup, each with a status, a sentence saying what is true and a sentence saying what to do about it |
recheck_deliverability |
POST /kahunacart/newsletters/deliverability/recheck |
kahunacart.newsletters.manage |
none | Throw away the cached DNS answers and look everything up again |
generate_provider_secret |
POST /kahunacart/newsletters/providers/{name}/secret |
kahunacart.newsletters.manage |
name, required |
Mints a fresh random path secret for one of the seven email providers (smtp2go, ses, postmark, mailgun, sendgrid, resend or mailersend), writes it into the plugin's configuration, and answers the whole webhook URL to paste into that provider's dashboard |
set_up_provider_webhook |
POST /kahunacart/newsletters/providers/{name}/setup |
kahunacart.newsletters.manage |
name, required |
Registers this store's webhook URL with the provider through its own API, for the delivered, bounce, spam complaint, open and click events, using the API key already in that provider's transport settings, so a merchant does not have to find the right screen in somebody else's dashboard |
send_provider_webhook_test |
POST /kahunacart/newsletters/providers/{name}/test |
kahunacart.newsletters.manage |
name, required |
Sends one small test message through the site's own transport to check that the provider's webhook reports back, the same as the Send a test button on the provider's Settings card |
get_provider_webhook_test |
GET /kahunacart/newsletters/providers/{name}/test |
kahunacart.newsletters.view |
name, required |
Where the provider's last webhook test stands: none (never tested), waiting, received (with the seconds it took), refused (the event arrived and its signature did not check out, nearly always a missing or wrong signing secret), or nothing (two minutes passed with no event, with url_changed when a new secret was made after Set up) |
list_automations |
GET /kahunacart/newsletters/flows |
kahunacart.newsletters.view |
none | Every automation this store has, with what starts it, how many steps it has, and where the people in it are: how many are moving, how many are waiting on a timer, how many have finished and how many left early |
get_automation |
GET /kahunacart/newsletters/flows/{id} |
kahunacart.newsletters.view |
id required; status, page, sort, dir, q optional |
One automation with its steps in order, each email step's message summarized with what it has sent, opened and clicked, and a page of the people walking it — which step each of them is on, when they are due to move again, and why they left if they have |
create_automation |
POST /kahunacart/newsletters/flows |
kahunacart.newsletters.manage |
name, trigger required; trigger_json, list_id, segment_id, allow_reentry, exit_on_purchase, steps optional |
Creates an automation in draft, with its steps |
update_automation |
PATCH /kahunacart/newsletters/flows/{id} |
kahunacart.newsletters.manage |
id required; name, trigger, trigger_json, list_id, segment_id, allow_reentry, exit_on_purchase, steps optional |
Changes an automation's settings, its steps, or both |
delete_automation |
DELETE /kahunacart/newsletters/flows/{id} |
kahunacart.newsletters.manage |
id required; force optional |
Deletes an automation, its steps and the messages its email steps own, sends and all |
activate_automation |
POST /kahunacart/newsletters/flows/{id}/activate |
kahunacart.newsletters.manage |
id, required |
Starts an automation running |
pause_automation |
POST /kahunacart/newsletters/flows/{id}/pause |
kahunacart.newsletters.manage |
id, required |
Holds everybody in an automation exactly where they are: no step runs, no mail goes out, nobody new is put in, and nobody is taken out |
resume_automation |
POST /kahunacart/newsletters/flows/{id}/resume |
kahunacart.newsletters.manage |
id, required |
Sets a paused automation running again |
enrol_in_automation |
POST /kahunacart/newsletters/flows/{id}/enrol |
kahunacart.newsletters.manage |
id, subscriber_id, all required |
Starts one subscriber on an automation by hand, under exactly the same rules a trigger applies: they have to be subscribed, on the automation's list if it names one, matching its segment if it names one, and new to it unless it allows re-entry |
exit_automation_enrolment |
POST /kahunacart/newsletters/flows/{id}/enrolments/{eid}/exit |
kahunacart.newsletters.manage |
id, eid, all required |
Ends one person's walk through one automation, with "taken out by hand" against it |
list_automation_recipes |
GET /kahunacart/newsletters/flows/recipes |
kahunacart.newsletters.view |
none | The automations that come with the add-on ready to use — a welcome series, a thank you after somebody buys, a win-back — each with a key, a sentence saying what it does, its ladder written out a step a line, and what to change before switching it on |
use_automation_recipe |
POST /kahunacart/newsletters/flows/recipes/{key} |
kahunacart.newsletters.manage |
key, required |
Builds a draft automation from one of the ready-made ones, with its steps and its messages already written |
get_newsletters_report |
GET /kahunacart/newsletters/reports/{type} |
kahunacart.newsletters.view |
type required; from, to, preset, bucket, list, campaign, flow, refresh optional |
The Reports tab, as a call |
Shipping
Setting up carriers, boxes and addresses needs kahunacart.shipping.manage. Quoting, buying and voiding a label needs kahunacart.shipping.labels, because it spends the store's money. Reading a label, its tracking or a packing preview needs kahunacart.orders.view. What each action does is on Shipping carriers.
Shipping publishes 27 tools over 29 admin routes, behind 3 permissions. An assistant calls each one by its name below with kahunacart_shipping_ in front of it.
These 2 routes are not published as tools: GET /kahunacart/shipping/labels/{id}/file, POST /kahunacart/shipping/public/webhook/{aggregator}/{secret}.
| Tool | Route | Permission | Arguments | What it does |
|---|---|---|---|---|
list_shipping_aggregators |
GET /kahunacart/shipping/aggregators |
kahunacart.shipping.manage |
none | The carrier aggregator this store is configured for, with its capability matrix, whether a key has been pasted, and when the carrier list was last refreshed |
check_shipping_aggregator |
POST /kahunacart/shipping/aggregators/{code}/check |
kahunacart.shipping.manage |
code, required |
Make a real call to the aggregator with the configured key and report whether it works, which account it reached and how many carrier accounts that account holds |
get_shipping_webhook |
GET /kahunacart/shipping/aggregators/webhook |
kahunacart.shipping.manage |
none | The inbound tracking webhook as this store has it: whether a secret is set, the public address it makes with the secret masked to its last four characters, whether the configured driver can register that address itself, and the last few deliveries the endpoint received |
generate_shipping_webhook_secret |
POST /kahunacart/shipping/aggregators/webhook/secret |
kahunacart.shipping.manage |
none | Generate the tracking webhook's secret and save it |
register_shipping_webhook |
POST /kahunacart/shipping/aggregators/webhook/register |
kahunacart.shipping.manage |
reset |
Register this store's inbound tracking address with the aggregator |
list_carrier_accounts |
GET /kahunacart/shipping/accounts |
kahunacart.shipping.manage |
none | The carrier accounts this store holds at its aggregator — USPS, UPS, FedEx, Royal Mail, DPD and so on — with the services each one offers and whether this store offers it |
sync_carrier_accounts |
POST /kahunacart/shipping/accounts |
kahunacart.shipping.manage |
none | Read the carrier list from the aggregator again and store it, so a carrier switched on at the aggregator this morning is bindable here this afternoon |
update_carrier_account |
PATCH /kahunacart/shipping/accounts/{id} |
kahunacart.shipping.manage |
id, enabled, all required |
Switch a carrier on or off for this store |
list_shipping_boxes |
GET /kahunacart/shipping/boxes |
kahunacart.shipping.manage |
none | The box catalogue, with the usable space inside each box after the packing-material allowance, and the built-in fallback box a store with no boxes is quoted for |
create_shipping_box |
POST /kahunacart/shipping/boxes |
kahunacart.shipping.manage |
code, length_mm, width_mm, height_mm required; name, inner_length_mm, inner_width_mm, inner_height_mm, empty_weight_grams, max_weight_grams, carrier_template, enabled, position optional |
Add a box to the catalogue |
update_shipping_box |
PUT /kahunacart/shipping/boxes/{id} |
kahunacart.shipping.manage |
id required; code, name, length_mm, width_mm, height_mm, inner_length_mm, inner_width_mm, inner_height_mm, empty_weight_grams, max_weight_grams, carrier_template, enabled, position optional |
Change a box in the catalogue |
delete_shipping_box |
DELETE /kahunacart/shipping/boxes/{id} |
kahunacart.shipping.manage |
id, required |
Take a box out of the catalogue |
list_shipping_locations |
GET /kahunacart/shipping/locations |
kahunacart.shipping.manage |
none | The ship-from addresses, and whether the store has one at all — a store with none cannot be quoted by any carrier |
create_shipping_location |
POST /kahunacart/shipping/locations |
kahunacart.shipping.manage |
code, address_1, city, country required; name, company, address_2, state, postcode, phone, email, is_default, enabled, position optional |
Add a ship-from address |
update_shipping_location |
PUT /kahunacart/shipping/locations/{id} |
kahunacart.shipping.manage |
id required; code, name, company, address_1, address_2, city, state, postcode, country, phone, email, is_default, enabled, position optional |
Change a ship-from address |
delete_shipping_location |
DELETE /kahunacart/shipping/locations/{id} |
kahunacart.shipping.manage |
id, required |
Take a ship-from address out |
preview_packing |
GET /kahunacart/shipping/pack |
kahunacart.orders.view |
order, order_id, items |
What the packer would do with an order, or with a made-up basket: the boxes, what goes in each, the actual and dimensional weights, which of the two the carrier would bill, and anything the packer had to guess at |
list_shipping_labels |
GET /kahunacart/shipping/labels |
kahunacart.orders.view |
status, order_id, sort, dir, q, page, per_page |
Every label this store has bought, tried to buy, or asked to have refunded, newest first |
quote_order_label |
GET /kahunacart/orders/{id}/shipping/quote |
kahunacart.shipping.labels |
id, required |
Everything needed before buying a label for an order, in one call: the parcels the packer made, the box each one is in, the actual and billable weights, the services the carrier quoted with their prices and transit estimates, what the carrier makes of the delivery address, and which customs facts are missing per line |
buy_shipping_label |
POST /kahunacart/orders/{id}/shipping/labels |
kahunacart.shipping.labels |
id required; service, notify optional |
Buy a label for what is still owed on an order, and record the parcel on it |
void_shipping_label |
POST /kahunacart/shipping/labels/{id}/void |
kahunacart.shipping.labels |
id, required |
Ask the carrier to refund a label |
record_shipping_parcel |
POST /kahunacart/shipping/labels/{id}/record |
kahunacart.shipping.labels |
id required; notify optional |
Write the shipment for a label that was bought and never recorded on its order — the state where the money is spent and the order does not know |
get_label_tracking |
GET /kahunacart/shipping/labels/{id}/tracking |
kahunacart.orders.view |
id, required |
Every tracking event recorded for one parcel, oldest first, with the carrier's own wording beside each normalised status and a flag saying whether the event needs somebody to act on it |
refresh_label_tracking |
POST /kahunacart/shipping/labels/{id}/tracking/refresh |
kahunacart.orders.view |
id, required |
Ask the carrier where a parcel is, now, and write anything new |
validate_shipping_address |
POST /kahunacart/shipping/addresses/validate |
kahunacart.shipping.manage |
address1, city, country required; name, company, address2, region, postcode, phone, email, residential optional |
What the carrier makes of a delivery address, with its own corrected version where it offers one |
get_product_customs |
GET /kahunacart/shipping/products/{id}/customs |
kahunacart.shipping.manage |
id required; variant_id optional |
The customs facts recorded against a product — its harmonised code, where it was made, and what it is called on a customs form |
set_product_customs |
PUT /kahunacart/shipping/products/{id}/customs |
kahunacart.shipping.manage |
id required; variant_id, hs_code, origin_country, customs_description optional |
Record the customs facts for a product, or for one of its variants |
Bookings
Reads need kahunacart.bookings.view and changes need kahunacart.bookings.manage. "What is free on Thursday" and "book Mrs Patel in at three" are both here. What each action means is on Bookings.
Bookings publishes 52 tools over 78 admin routes, behind 2 permissions. An assistant calls each one by its name below with kahunacart_bookings_ in front of it.
These 26 routes are not published as tools: GET /kahunacart/bookings/admin-slots/{id}.js, GET /kahunacart/bookings/export, POST /kahunacart/bookings/desk/bookings, GET /kahunacart/bookings/desk/bookings/{id}, POST /kahunacart/bookings/desk/bookings/{id}/{action}, GET /kahunacart/bookings/desk/customers, POST /kahunacart/bookings/desk/customers, GET /kahunacart/bookings/desk/customers/{id}, POST /kahunacart/bookings/desk/customers/{id}, GET /kahunacart/bookings/desk/diary, GET /kahunacart/bookings/desk/me, GET /kahunacart/bookings/desk/services, GET /kahunacart/bookings/desk/slots, GET /kahunacart/bookings/public/availability, POST /kahunacart/bookings/public/book, GET /kahunacart/bookings/public/channels/{token}.ics, GET /kahunacart/bookings/public/feeds/{token}.ics, POST /kahunacart/bookings/public/holds, DELETE /kahunacart/bookings/public/holds/{id}, GET /kahunacart/bookings/public/manage/{token}, POST /kahunacart/bookings/public/manage/{token}/cancel, POST /kahunacart/bookings/public/manage/{token}/claim-offer, POST /kahunacart/bookings/public/manage/{token}/reschedule, GET /kahunacart/bookings/public/me, GET /kahunacart/bookings/public/services, GET /kahunacart/bookings/public/sessions.
| Tool | Route | Permission | Arguments | What it does |
|---|---|---|---|---|
list_bookings |
GET /kahunacart/bookings |
kahunacart.bookings.view |
from, to, status, customer_id, product_id, order_id, bookable_id, conflict, page, per_page, sort, dir |
List bookings, newest first, with filters for a date range, a status, a customer, a product, an order or one person's column |
create_booking |
POST /kahunacart/bookings |
kahunacart.bookings.manage |
product_id, start required; variant_id, seats, with, tz, email, name, notes, take_payment, paid optional |
Put somebody in the diary |
read_booking |
GET /kahunacart/bookings/{id} |
kahunacart.bookings.view |
id, required |
One booking in full, with the times it is sitting on and everything that has been done to it |
approve_booking |
POST /kahunacart/bookings/{id}/approve |
kahunacart.bookings.manage |
id, required |
Say yes to a booking that was waiting for the shop to agree to it |
reject_booking |
POST /kahunacart/bookings/{id}/reject |
kahunacart.bookings.manage |
id required; reason optional |
Say no to a booking that was waiting |
cancel_booking |
POST /kahunacart/bookings/{id}/cancel |
kahunacart.bookings.manage |
id required; reason optional |
Call a booking off and give the time back |
complete_booking |
POST /kahunacart/bookings/{id}/complete |
kahunacart.bookings.manage |
id, required |
Mark a booking as done |
mark_booking_no_show |
POST /kahunacart/bookings/{id}/no-show |
kahunacart.bookings.manage |
id required; no_show optional |
Record that somebody did not turn up, or take that mark off again |
reschedule_booking |
POST /kahunacart/bookings/{id}/reschedule |
kahunacart.bookings.manage |
id, start, all required |
Move a booking to a different time |
list_booking_holds |
GET /kahunacart/bookings/holds |
kahunacart.bookings.view |
limit |
Every time somebody is standing on right now while they finish paying |
booking_overview |
GET /kahunacart/bookings/overview |
kahunacart.bookings.view |
from, to |
The diary over a span, as the owner's calendar draws it — everyone and everything that can be booked with the hours they work and the days they are away, every booking between the two instants, and how many are waiting, in conflict, no-shows or cancelled |
troubleshoot_booking_slots |
GET /kahunacart/bookings/troubleshoot |
kahunacart.bookings.view |
product_id required; variant_id, with, date, tz optional |
Every candidate start time on one day for one service, with a tick or the exact reason it is not on offer — too soon, too far ahead, full, booked, held by somebody else, time off, closed, busy on another calendar, or which need failed and why |
get_booking_settings |
GET /kahunacart/bookings/settings |
kahunacart.bookings.view |
none | What this add-on is set to do, store-wide — whether the front desk is switched on and where it answers, whether a place that comes free on a full class is offered automatically or by hand and for how long it is held, what a published calendar says about each booking, how long a slot is held in the basket and at the payment page, how the shop writes a time of day, and the defaults a service falls back to when it says nothing of its own |
list_booking_products |
GET /kahunacart/bookings/setup/products |
kahunacart.bookings.view |
none | Every product of type Booking, with its options and whether a booking rule has been written for each |
list_booking_accounts |
GET /kahunacart/bookings/setup/accounts |
kahunacart.bookings.view |
none | The sign-in accounts a person can be tied to, so somebody who works here can be given the account they use at the front desk |
list_bookables |
GET /kahunacart/bookings/setup/bookables |
kahunacart.bookings.view |
enabled_only, type |
Everyone and everything whose time can be taken — the people, the rooms, the equipment — with how many identical ones there are of each |
create_bookable |
POST /kahunacart/bookings/setup/bookables |
kahunacart.bookings.manage |
type, slug required; name, units, timezone, schedule_id, location_id, user_id, enabled, position optional |
Add somebody or something whose time can be booked |
update_bookable |
PATCH /kahunacart/bookings/setup/bookables/{id} |
kahunacart.bookings.manage |
id required; name, units, timezone, schedule_id, location_id, user_id, enabled, position optional |
Change somebody or something that can be booked |
delete_bookable |
DELETE /kahunacart/bookings/setup/bookables/{id} |
kahunacart.bookings.manage |
id, required |
Remove somebody or something that can be booked |
list_booking_schedules |
GET /kahunacart/bookings/setup/schedules |
kahunacart.bookings.view |
none | The named sets of working hours, each with its own weekly pattern and the dates that are different |
create_booking_schedule |
POST /kahunacart/bookings/setup/schedules |
kahunacart.bookings.manage |
slug required; name, timezone, days, overrides optional |
Add a set of working hours |
update_booking_schedule |
PATCH /kahunacart/bookings/setup/schedules/{id} |
kahunacart.bookings.manage |
id required; name, timezone, days, overrides optional |
Change a set of working hours |
delete_booking_schedule |
DELETE /kahunacart/bookings/setup/schedules/{id} |
kahunacart.bookings.manage |
id, required |
Remove a set of working hours |
list_booking_time_off |
GET /kahunacart/bookings/setup/time-off |
kahunacart.bookings.view |
bookable_id, required |
The whole days one person or thing is away |
create_booking_time_off |
POST /kahunacart/bookings/setup/time-off |
kahunacart.bookings.manage |
bookable_id, from, to required; note optional |
Book somebody or something out for whole days |
update_booking_time_off |
PATCH /kahunacart/bookings/setup/time-off/{id} |
kahunacart.bookings.manage |
id required; from, to, note optional |
Change a stretch of time off |
delete_booking_time_off |
DELETE /kahunacart/bookings/setup/time-off/{id} |
kahunacart.bookings.manage |
id, required |
Take a stretch of time off back out, so those days can be booked again |
list_booking_locations |
GET /kahunacart/bookings/setup/locations |
kahunacart.bookings.view |
none | Where bookings happen, each with its address and its own hours |
create_booking_location |
POST /kahunacart/bookings/setup/locations |
kahunacart.bookings.manage |
slug required; name, address, timezone, schedule_id optional |
Add a place bookings happen |
update_booking_location |
PATCH /kahunacart/bookings/setup/locations/{id} |
kahunacart.bookings.manage |
id required; name, address, timezone, schedule_id optional |
Change a place bookings happen |
delete_booking_location |
DELETE /kahunacart/bookings/setup/locations/{id} |
kahunacart.bookings.manage |
id, required |
Remove a place bookings happen |
list_booking_services |
GET /kahunacart/bookings/setup/services |
kahunacart.bookings.view |
none | Every product the shop books, with how long it takes, how often a start is offered, what it needs, and the product it hangs off |
save_booking_service |
PUT /kahunacart/bookings/setup/services/{productId} |
kahunacart.bookings.manage |
productId required; duration, slot_interval, buffer_before, buffer_after, capacity, party_size, names_say_length, notice_hours, window_days, cutoff_hours, workflow, payment, mode, schedule_id, location_id, enabled optional |
Write the booking rules for a product — how long, how often, how much notice, how far ahead, and whether it is confirmed straight away or waits for the shop to agree |
save_booking_variant_service |
PUT /kahunacart/bookings/setup/services/{productId}/{variantId} |
kahunacart.bookings.manage |
productId, variantId required; duration, slot_interval, buffer_before, buffer_after, capacity, party_size, names_say_length, notice_hours, window_days, cutoff_hours, workflow, payment, schedule_id, location_id, enabled optional |
Write the booking rules for one option of a product, replacing the product's own rule for that option only |
delete_booking_service |
DELETE /kahunacart/bookings/setup/services/{id} |
kahunacart.bookings.manage |
id, required |
Stop booking a product, or stop one of its options having a rule of its own |
list_booking_needs |
GET /kahunacart/bookings/setup/services/{serviceId}/needs |
kahunacart.bookings.view |
serviceId, required |
What a service occupies — one groomer from Ann or Bo with the customer choosing, and the tub with no choice — each with the pool it draws on |
create_booking_need |
POST /kahunacart/bookings/setup/services/{serviceId}/needs |
kahunacart.bookings.manage |
serviceId, label required; units, customer_chooses, position, bookables optional |
Say that a service occupies one more thing |
update_booking_need |
PATCH /kahunacart/bookings/setup/needs/{id} |
kahunacart.bookings.manage |
id required; label, units, customer_chooses, position, bookables optional |
Change what a service occupies, or who is in the pool |
delete_booking_need |
DELETE /kahunacart/bookings/setup/needs/{id} |
kahunacart.bookings.manage |
id, required |
Stop a service occupying something |
create_bookable_feed_token |
POST /kahunacart/bookings/setup/bookables/{id}/feed-token |
kahunacart.bookings.manage |
id, required |
Make a calendar link for one person, room or piece of equipment, so it can be subscribed to from a phone |
delete_bookable_feed_token |
DELETE /kahunacart/bookings/setup/bookables/{id}/feed-token |
kahunacart.bookings.manage |
id, required |
Take a calendar link away with nothing put back |
create_bookable_channel_link |
POST /kahunacart/bookings/setup/bookables/{id}/channel-token |
kahunacart.bookings.manage |
id, required |
Make the calendar link a channel gets — Airbnb, VRBO, Booking.com — for one property, room or pitch |
delete_bookable_channel_link |
DELETE /kahunacart/bookings/setup/bookables/{id}/channel-token |
kahunacart.bookings.manage |
id, required |
Take a channel link away with nothing put back |
test_bookable_busy_feed |
POST /kahunacart/bookings/setup/bookables/{id}/busy-feeds/test |
kahunacart.bookings.manage |
id, url required; days optional |
Fetch an outside calendar right now and say how many busy times are in it over the next few weeks, or what went wrong in plain words |
list_booking_sessions |
GET /kahunacart/bookings/sessions |
kahunacart.bookings.view |
days |
Every class and workshop the shop runs, with the dates coming up, how many places are left on each and whether a date is open, full or closed |
get_booking_session |
GET /kahunacart/bookings/sessions/{serviceId} |
kahunacart.bookings.view |
serviceId required; from, to, days optional |
One class, its recurrence rule and its exceptions, with every date in the range and the places left on each |
get_session_register |
GET /kahunacart/bookings/sessions/{serviceId}/register |
kahunacart.bookings.view |
serviceId required; occurrence optional |
Who is coming on one date of a class and who is waiting behind them, in the order places would be offered |
run_session_waitlist |
POST /kahunacart/bookings/sessions/{serviceId}/promote |
kahunacart.bookings.manage |
serviceId required; occurrence optional |
Give away or offer every place that has come free on one date of a class |
cancel_session_date |
POST /kahunacart/bookings/sessions/{serviceId}/cancel-date |
kahunacart.bookings.manage |
serviceId required; occurrence, reason optional |
Call one date of a class off |
mark_session_room |
POST /kahunacart/bookings/sessions/{serviceId}/mark |
kahunacart.bookings.manage |
serviceId required; occurrence, action optional |
Mark everybody on one date of a class at once, as having come or as not having come |
offer_booking_seat |
POST /kahunacart/bookings/{id}/offer |
kahunacart.bookings.manage |
id, required |
Hold a place for one person who is waiting and tell them it is theirs if they pay for it |
promote_booking |
POST /kahunacart/bookings/{id}/promote |
kahunacart.bookings.manage |
id, required |
Give one person who is waiting a place outright, whatever the class costs |
Rentals
Rentals uses the Bookings permissions: kahunacart.bookings.view to read a rate card or quote a stay, kahunacart.bookings.manage to change one. Anything a guest does, such as asking what is free next August, is a Bookings tool. See Rentals.
Rentals publishes 9 tools over 9 admin routes, behind 2 permissions. An assistant calls each one by its name below with kahunacart_rentals_ in front of it.
| Tool | Route | Permission | Arguments | What it does |
|---|---|---|---|---|
list_rental_properties |
GET /kahunacart/rentals/properties |
kahunacart.bookings.view |
none | Every property in this store that books by the night: its Bookings service id, what the product and option are called, how many it sleeps, the zone its dates are in, what a night costs in the store's own currency before any rate band, and how many bands its rate card has |
get_rental_rules |
GET /kahunacart/rentals/rules/{serviceId} |
kahunacart.bookings.view |
serviceId, required |
One property's stay rules: what is stored, the same thing with the defaults filled in, how many it sleeps, its timezone and what it is called |
save_rental_rules |
PUT /kahunacart/rentals/rules/{serviceId} |
kahunacart.bookings.manage |
serviceId required; check_in_time, check_out_time, min_nights, max_nights, arrival_days, departure_days, guests_included, cleaning_fee_minor, extra_guest_minor, damage_deposit_minor, monthly_minor, avoid_gaps, deposit_percent, balance_days_before, pre_arrival_text, currency_amounts optional |
Change a property's stay rules — check-in and check-out times, the shortest and longest stay, which weekdays a stay may start and end on, how many guests the nightly rate covers, the fees, the deposit terms, the cancellation policy and the pre-arrival note |
list_rental_rates |
GET /kahunacart/rentals/rates |
kahunacart.bookings.view |
service_id, required |
One property's rate card: every dated band in position order, lowest first, with the property's own nightly price and currency beside them |
create_rental_rate |
POST /kahunacart/rentals/rates |
kahunacart.bookings.manage |
service_id, starts_on, ends_on required; label, nightly_minor, percent, weekend_minor, weekend_days, min_nights, arrival_days, position, currency_amounts optional |
Add a dated band to a property's rate card |
update_rental_rate |
PATCH /kahunacart/rentals/rates/{id} |
kahunacart.bookings.manage |
id required; starts_on, ends_on, label, nightly_minor, percent, weekend_minor, weekend_days, min_nights, arrival_days, position, currency_amounts optional |
Change one band on a rate card |
delete_rental_rate |
DELETE /kahunacart/rentals/rates/{id} |
kahunacart.bookings.manage |
id, required |
Take one band off a rate card |
reorder_rental_rates |
POST /kahunacart/rentals/rates/reorder |
kahunacart.bookings.manage |
service_id, ids, all required |
Renumber one property's rate bands in the order the ids are given, which is what decides who wins a night two bands both cover |
quote_rental_stay |
GET /kahunacart/rentals/quote |
kahunacart.bookings.view |
product_id, arrival, departure required; variant_id, guests, exclude_booking_id, currency optional |
What a stay at a property would come to — every night with the band it fell in, the extra guests, the cleaning, the total, what is due today and when the balance falls due |
Refresh after installing a plugin
Install or enable a plugin while the assistant is connected, then ask it to run refresh_plugin_tools. It re-reads every manifest and updates the tool list in place: new tools added, changed ones updated, tools from a disabled or removed plugin dropped. It reports added, updated, removed and any manifest warnings, and tells the client the list changed.
No restart is required. refresh_plugin_tools requires only api.access.
Note
refresh_plugin_tools still works when plugin tools were started with --plugin-tools none. It reports that they are disabled rather than loading them.
Write tools for your own plugin
Any Grav plugin can publish its own routes. Add an mcp.yaml in the plugin root, beside blueprints.yaml and permissions.yaml. Describe each route with a name, a description written for the model, the method and path, the permission it enforces, and a JSON Schema for the arguments. The API plugin serves it and grav-mcp registers it, with no code on either side.
version: 1
prefix: kahunacart # optional; defaults to the plugin slug. Tool name = "{prefix}_{name}"
tools:
- name: list_products
title: List products
description: >
List catalog products with paging, search and status filters. Returns product rows with their
variants and attributes. Use get_product for one product with everything on it.
method: GET
path: /kahunacart/products
permission: kahunacart.products.manage
annotations:
readOnly: true
input:
type: object
properties:
q: { type: string, description: "Search title, slug or SKU" }
status: { type: string, enum: [draft, published, archived] }
page: { type: integer, minimum: 1, default: 1 }
per_page: { type: integer, minimum: 1, maximum: 100, default: 20 }
- name: update_product
title: Update a product
description: Change one or more fields of a product. Only the fields sent are changed.
method: PATCH
path: /kahunacart/products/{id}
permission: kahunacart.products.manage
annotations:
idempotent: true
input:
type: object
required: [id]
properties:
id: { type: integer, description: "Product id" }
title: { type: string }
status: { type: string, enum: [draft, published, archived] }
Manifest fields
| Key | Required | Meaning |
|---|---|---|
version |
yes | Manifest format version. Only 1 exists |
prefix |
no | Tool-name prefix. Defaults to the plugin slug with - replaced by _ |
tools[].name |
yes | ^[a-z][a-z0-9_]*$. The final name is {prefix}_{name} and must be 64 characters or fewer |
tools[].title |
no | Human title an MCP client shows |
tools[].description |
yes | What the tool does and returns, when to use it, and anything the model must know to call it well. One to four sentences. Do not repeat the permission; the loader appends [Requires: <permission>] itself |
tools[].method |
yes | GET, POST, PATCH, PUT or DELETE |
tools[].path |
yes | Route path relative to the API base, starting with /. Each {name} placeholder must exist in input.properties and is treated as required |
tools[].permission |
no | The permission the route enforces. A caller who lacks it never sees the tool |
tools[].annotations |
no | readOnly, destructive and idempotent booleans |
tools[].input |
no | A JSON Schema object in the subset below. Omit for a tool with no arguments |
tools[].query |
no | For POST, PATCH, PUT and DELETE: the properties to send as query-string parameters instead of in the body. GET sends every non-path property as a query parameter |
Annotations default by method: GET is read-only and idempotent, DELETE destructive and idempotent, PUT and PATCH idempotent, POST none of them. Setting a key overrides that one default. Only JSON bodies work, so leave multipart routes out.
The JSON Schema subset
grav-mcp converts input to a validator at load time, so use only what the converter understands.
Allowed per property: type (string, integer, number, boolean, array, object), description, default, enum, nullable, minimum, maximum, minLength, maxLength, pattern, format (date, date-time, email, uri), items for arrays, and properties with required and additionalProperties for nested objects. An object with no properties is a free-form map.
Not allowed: $ref, oneOf, anyOf, allOf, not, if/then, tuple items, patternProperties, const and dependencies. A tool that uses one of these is skipped and reported under warnings.
Tools built in code
If your tool list depends on data rather than a file, add entries in code. The API plugin fires onApiMcpTools with an McpToolCollector under $event['tools']:
public function onApiMcpTools(Event $event): void
{
$event['tools']->add('kahunacart', [
'name' => 'sync_stripe',
'description' => 'Push the catalog to Stripe.',
'method' => 'POST',
'path' => '/kahunacart/providers/stripe/sync',
'permission' => 'kahunacart.settings',
]);
}
The first argument is your plugin slug. Entries are validated like manifest entries. Manifest files are read first, so a name your mcp.yaml already claimed wins and the duplicate is dropped with a warning.
What the endpoint answers
GET /mcp/tools requires api.access and returns the usual response envelope with three keys inside data:
| Key | What it holds |
|---|---|
tools |
One entry per tool the caller may see, with annotations fully populated and input_schema always present |
plugins |
One entry per enabled plugin that contributed tools: slug, name, version, and how many of its tools this caller can see |
warnings |
Every entry that was skipped and why. Safe to show to any authenticated caller; it holds no secrets |
data.fingerprint hashes the enabled-plugin set and the manifest mtimes and is sent as the ETag, so a client can tell whether anything changed without diffing. A YAML parse error becomes one warning for that plugin, and the rest of the response is still served.
The full contract is in the API plugin README.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
No kahunacart_ tools in the assistant's list |
grav-mcp could not load plugin tools at startup, or they are switched off | Read grav-mcp's stderr for the warning. Check that --plugin-tools is not none, and that a slug list includes kahunacart. Then run refresh_plugin_tools |
Warning: Failed to load plugin tools: … Plugin tools need the 'api.access' permission. |
The key's account has no api.access, so GET /mcp/tools answered 401 or 403 |
Grant api.access to that account, or use a key from an account that has it. A key created with a scope list is capped to those scopes whatever the account may do |
Warning: Failed to load plugin tools: this site's API plugin has no /mcp/tools endpoint… |
The API plugin predates 1.0.22, so /mcp/tools answers 404 |
Update the API plugin, then run refresh_plugin_tools |
Some kahunacart_ tools are there and others are not |
The key holds some kahunacart.* permissions and not others |
Grant the missing permission. The tool list is filtered per caller |
A tool answers 403 |
The key lacks that permission, or the store has demo.readonly on and the tool writes |
Grant the permission, or turn demo.readonly off on a store that is not a demo |
delete_attribute answers 409 |
Products still carry a value for that attribute. The message names the count | Send force: true to delete the values too, or clear them first |
| No tools from a plugin you just enabled | grav-mcp registered its tools at startup, before the plugin existed | Run refresh_plugin_tools. Only enabled plugins are read |