Skip to content

Powered by Grav CMS + Helios Theme

CLI

KahunaCart ships forty console commands under bin/plugin kahunacart. This page documents all of them: arguments, options, output and exit codes.

Before you begin

  • Run every command from the Grav root, the directory that contains bin/.
  • export, import, seed and index refuse an out-of-date schema. They print The KahunaCart schema is out of date. Run bin/plugin kahunacart migrate first. and exit 1.

Command reference

Command Arguments and options What it does
migrate --status Apply pending database migrations.
status none Health check: database, migrations, job queue, providers.
work --budget=<seconds> Process pending jobs, then run maintenance.
store --status=<open\|coming_soon\|paused>, --message Show whether the store is open, or open, pause or announce it.
sync --provider=<slug>, --push, --pull, --status Push or pull the catalog against a sync-capable provider.
export file (optional), --orders, --scope, --set, --columns, --format, --from, --to, --status, --list-columns, --bundle, --only, --except, --no-media, --no-files, --passwords, --secrets, --list-sets Write the catalog to a YAML file, write orders to CSV or JSON, or write the whole store into one bundle.
import file (required), --dry-run, --strict, --mapping=<name>, --merge, --full, --only, --except Read a catalog file back in, upserting by slug, read a CSV through a saved mapping, or restore a store bundle.
squarespace folder (required), --dry-run, --weight-unit, --dimension-unit, --only, --no-images, --tidy-titles, --products, --orders, --customers, --site Bring a whole Squarespace store and site across from a folder of its exports.
seed --purge, --strict, --zones, --currencies=<codes> Fill the catalog with a demo store, or remove it again.
webhooks --provider=<slug>, --limit=<n>, --purge Recent webhook deliveries, per-provider totals, failure streaks.
coupons:generate --list, --group=<id\|name>, --count=<n>, --csv Mint a batch of unique discount codes into a campaign.
downloads:check --all Report every product file a customer could not be handed.
retention:run --dry-run, --limit=<n> Run the four data-retention rules now.
orders:reassign --from=<id\|email>, --to=<id\|email>, --dry-run Move every order from one customer to another.
orders:reindex --text, --index Rebuild the admin order search.
index --rebuild, --dirty, --chunk=<n>, --json Build, drain or report on the catalog index.
search:reindex none Rebuild the storefront product search index from the catalog.
search:embed --reset Embed the product search index for semantic search now.
search:calibrate none Measure semantic search's noise gate on this catalog and print the numbers.
reports:rebuild --from=<date>, --to=<date> Recompute the daily report rollup.
bench:seed --products=<n>, --customers=<n>, --orders=<n>, --seed=<n>, --now=<iso>, --batch=<n>, --purge, --force, --json Fill the store with a benchmark-sized catalog, customer list and order history.
bench:queries --iterations=<n>, --warmup=<n>, --json Time the queries behind the storefront and the admin.
check --offline, --json, --report The installation doctor: up to sixteen checks with a suggested fix each.
reset --orders, --customers, --keep-settings, --keep-numbers, --force Delete the test data before going live and keep every setting.
templates:drift --all, --json List theme template overrides that are behind the shipped templates.
jobs:list --state=<state>, --type=<type>, --limit=<n>, --json The job queue from a shell.
jobs:run id (required) Run one queued job now, in this process.
jobs:cancel id (required) Cancel a pending job, or ask a running one to stop.
webhooks:subscriptions --topics, --limit=<n>, --create, --name, --topic, --url, --ping, --pause, --delete, --deliveries, --redeliver, --purge Outbound webhook subscriptions and their delivery log.
lang:export file (optional), --format, --status, --missing=<code> Write the translation source, or report how complete each language pack is.
config:export --out=<file>, --include=<sections>, --exclude=<sections> Write the store's configuration to a JSON document.
config:import file (required), --dry-run Apply a document written by config:export.
feeds:build --feed=<name>, --lang=<code>, --currency=<code>, --out=<dir> Build the Google and OpenAI product feeds now.
agentic:keys --rotate Show the UCP signing keys, make the first one, or rotate them.
rates --refresh, --source=<code>, --json, --accept=<code>, --keep=<code> Show the exchange rates between the store's currencies, fetch them now, or answer a rate that was held.
make:provider name (required), --dir, --title, --description, --author, --vendor, --force Scaffold a payment provider add-on.
make:shipping-method the same seven Scaffold a shipping rate calculator add-on.
make:discount-operation the same seven Scaffold a discount operation add-on.
make:admin-section the same seven Scaffold an admin screen add-on.
make:migration name (required), --plugin, --dir, --force Write the next numbered, up-only migration file.

migrate

Apply pending database migrations.

BASH
bin/plugin kahunacart migrate
bin/plugin kahunacart migrate --status
Option Default Effect
--status off List pending steps without applying them.

The command prints the database engine, then every pending step grouped by migration. Without --status it applies them and finishes with a count. Nothing pending prints Schema is up to date — nothing to migrate. and exits 0.

Migrations are step-level and idempotent, so re-running a partially applied migration picks up where it left off. There are no down migrations; recovery means restoring from backup. A database lock keeps two runs apart.

Run this in your deploy pipeline on a MySQL or PostgreSQL store, where database.auto_migrate does not auto-apply. See Configuration → The auto_migrate policy.

BASH
# Typical deploy step
bin/plugin kahunacart migrate --status   # see what is coming
bin/plugin kahunacart migrate            # apply it

status

Health check across the database, migrations, job queue and providers.

BASH
bin/plugin kahunacart status

It prints:

  • Database — the dialect in use.
  • A MySQL strict-mode warning if the server is not strict. A non-strict MySQL silently truncates data.
  • Migrations — up to date, or a count of pending steps.
  • Catalog index, other currencies — only on a store that sells in more than one currency: whether the catalog index has prices for each of them, or which currencies were added since it was built (shoppers in those cannot sort or filter by price until index --rebuild), or that the index is off or has never been built.
  • Jobs — pending, failed and completed counts.
  • Providers — slug, label and declared capabilities for each.
  • Payment methods by currency — only on a store that sells in more than one currency: one line per currency, the base first, naming the payment methods that take it, and a warning for any currency that nothing takes (a shopper in it cannot check out). Store credit is not counted.
  • Exchange rates: only on a store that sells in more than one currency: the rate source in use, and a warning naming any currency whose fetched rate is older than currencies.rates.max_age_days working days or has no rate. A rate you typed yourself never counts as old, and a store on the manual source is never warned. bin/plugin kahunacart rates has the detail.
  • Currency and caching — only on a store that sells in more than one currency and reads the country header (currencies.header is set and header is one of the detect steps): one line saying that a plain catalogue URL is always private, that links carry ?currency=, and that a CDN has to follow Cache-Control and bypass its cache when the kc_currency cookie is present. It can't see your CDN, so it sends you to Caching and CDNs.
TXT
KahunaCart Status
=================

Database: sqlite
Migrations: up to date
Jobs: 0 pending, 0 failed, 47 completed
Providers:
  offline (Bank Transfer) []
  stripe (Credit / Debit Card (Stripe)) [hosted_checkout, refunds, partial_refunds]

Run this first when something is not working. A provider missing from the list is not configured: providers register only once their credentials are present. Pending migrations explain most "column not found" errors, and a growing failed-job count means the queue is choking.

store

Says whether the store is open, coming soon or paused, and sets it.

BASH
bin/plugin kahunacart store                                   # read it
bin/plugin kahunacart store --status=paused --message="We're moving to a new home. Back on Monday."
bin/plugin kahunacart store --status=coming_soon
bin/plugin kahunacart store --status=open
bin/plugin kahunacart store --message=""                      # back to the standard sentence
Option Effect
--status open, coming_soon or paused. Anything else is refused, with exit code 1.
--message What the banner and every refusal say while the store isn't open. Empty goes back to the standard sentence. Given without --status, it keeps the status the store has.

It writes store.status and store.status_message to user/config/plugins/kahunacart.yaml, clears Grav's cache the way saving the settings does, and fires onKahunaCartStoreStatusChanged when the status changed. Then it prints the status, and while the store isn't open, what shoppers see. A deploy script pauses a store before its final export with --status=paused and opens the new one with --status=open. See Configuration → Store status.

rates

The exchange rates between the store's currencies, and a way to fetch them now.

BASH
bin/plugin kahunacart rates
bin/plugin kahunacart rates --refresh
bin/plugin kahunacart rates --refresh --source=ecb
bin/plugin kahunacart rates --json
bin/plugin kahunacart rates --accept=EGP
bin/plugin kahunacart rates --keep=EGP
Option Effect
--refresh Fetch the rates from the configured source now, which is what the daily job does, and then print the list.
--source=<code> With --refresh, fetch from another source for this run instead of currencies.rates.source.
--json Print the list as JSON, the same document as GET /kahunacart/currencies/rates.
--accept=<code> Write the rate a refresh held for that currency, with its source and day. It isn't pinned, so it keeps updating. The same as Accept on the Currencies screen.
--keep=<code> Keep the old rate for that currency and let the held rate go. The same as Keep old rate on the screen.

With no option it prints the base currency, the rate source and its fallback, and how many working days old a fetched rate may get, then one line per currency the store sells in: its rate (how many of that currency one unit of the base buys), where it came from (ecb, frankfurter, another source, or typed by hand), the day its source published it (rate of 2026-10-09) and when the store last checked it. A rate that counts as old is marked, and a currency with no rate says so. Under the list it says how the last refresh went, and why when it failed.

--refresh also names every rate that changed and how far it moved. A rate you typed is left alone and named. A currency the source doesn't list keeps its old rate and is named, so you know to type one. A rate the fallback answered is named with from frankfurter (or whichever fallback is set). When the source can't answer and the fallback does, the source's reason is printed and the refresh still counts as having worked. When neither can answer, the old rates are kept, the reason is printed and the command exits with 1.

A rate a refresh held because it moved more than currencies.rates.hold_move_percent is listed under the rates, with the new rate and how far it moved. --accept=EGP writes the held rate with its source and day, unpinned, and --keep=EGP lets the hold go and keeps the old rate. Either one exits with 1 and says why when there is no held rate for that currency. See A big move is held for you to check.

On a store that sells in one currency it says there are no rates and does nothing. See Currencies → Exchange rates.

work

Process pending jobs, then run maintenance.

BASH
bin/plugin kahunacart work
bin/plugin kahunacart work --budget=120
Option Default Effect
--budget=<seconds> 50 How long the runner keeps claiming jobs before stopping.

Important

Grav's scheduler runs this every minute, registered as kahunacart-jobs on a * * * * * schedule. Run it by hand only when debugging, or when the scheduler is not set up.

Job types it handles:

Type Purpose
order.confirmation_email Order confirmation to the customer, plus the BCC copy.
order.payment_received_email Receipt for an order that was awaiting payment and is now paid.
order.shipped_email "Your order is on its way", when an order is marked as shipped.
order.refund_email Refund notice, queued when a provider confirms a refund.
order.quote_email The quote message, with the deadline the offer stands until.
order.gift_card_email One gift card's code, one message per card.
orders.bulk_status One bulk status change across a whole selection of orders.
order.grant_downloads Mint download grants for an order's digital lines.
customer.set_password_email The "choose a password" link for a new store account. Mints the token as it sends.
stock.release_expired Release expired stock holds.
cart.abandoned_sweep Abandoned-cart reminders.
notifications.dispatch One merchant notification, fanned out across matching channels.
notifications.low_stock_sweep Low-stock merchant warnings.
sync.push_product Push one product to a provider.
sync.pull_catalog Pull a provider's whole catalog.

After the queue drains, or the budget runs out, maintenance tasks run on the same tick:

  1. Expired stock holds are released.
  2. The abandoned-cart sweep runs when cart.abandoned_enabled is on, capped at 25 carts per tick and stamping each one.
  3. The low-stock sweep runs, stamping each variant so it warns once per slide under the threshold.
  4. Inactive carts are purged per cart.purge_days, default 90, and webhook log rows are swept per payments.webhook_log_days, default 30. Set either to 0 to keep the rows forever.
  5. The report rollup is kept up. The last 3 closed days are recomputed, so an overnight refund or correction is in the reports by morning. A store whose rollup is behind catches up 60 days per tick until it is current. See Reports and the rollup.
  6. The review-request sweep runs when reviews.request_after_days is set, capped at 25 orders per tick.
  7. The retention schedule runs when any window is set. Every window is off until you set one. Run rows older than a year are purged. See The retention schedule.
  8. Stale anonymous wishlists are swept per wishlists.anonymous_days: lists whose cookie is long gone.
  9. Expired quotes are settled, so the outstanding list agrees with the page. Nothing depends on it; a quote past its deadline reads as expired as soon as anybody looks.

A sweep or rollup failure is reported and logged, never fatal. Output looks like:

TXT
Jobs: 3 processed, 0 failed. Holds released: 1. Carts purged: 0. Webhook rows swept: 0. Report days rolled up: 3.

The exit code is 1 if any job failed, 0 otherwise, so it works as a monitoring check.

Failed jobs are retried with a doubling backoff, capped at one hour, up to max_attempts, default 3. After that they sit in the failed bucket with last_error recording why.

Running two workers is safe: each job is processed exactly once. A lock held for more than 300 seconds is treated as stale and the job can be claimed again.

sync

Push or pull the catalog against a sync-capable provider. See Sync for what syncs.

BASH
bin/plugin kahunacart sync
bin/plugin kahunacart sync --provider=polar
bin/plugin kahunacart sync --provider=polar --push
bin/plugin kahunacart sync --provider=polar --pull
bin/plugin kahunacart sync --provider=polar --push --status
Option Effect
--provider=<slug> Which provider. Required with --push or --pull; filters the status tables otherwise.
--push Push the local catalog to the provider.
--pull Pull the provider's catalog into the local one.
--status Show mappings, capabilities and directions.

sync with no action flag behaves as --status. Passing --push and --pull together is an error, and so is either one without --provider. Manual runs ignore the configured direction, which only governs automatic syncing.

The status output has two tables:

TXT
Mappings
--------
 Provider  Mapped  By state
 polar     12      synced: 11, stale: 1

Providers
---------
 Provider  Sync capabilities              Implements sync  Direction
 offline   —                              no               off
 polar     sync_products, sync_orders     yes              remote

stale mappings mean the provider reported a change that was not applied because it is not master here. Implements sync: no on a provider that declares sync capabilities is a bug in that plugin. A push or pull failure prints the error and exits 1.

export

Write the catalog to a YAML file, write orders to CSV or JSON with --orders, or write the whole store into one bundle with --bundle.

BASH
bin/plugin kahunacart export catalog.yaml
bin/plugin kahunacart export            # to stdout, for piping or redirecting
bin/plugin kahunacart export - > catalog.yaml
Argument Effect
file Where to write it. Omit it, or pass -, to print to stdout with no decoration.

The file holds categories with their nesting, landing-page setting and picture; attribute definitions; products; every variant; option axes and their values; category, tag and attribute assignments; image references; and translated text. Orders, customers, coupons and settings are not included.

Four things matter before you edit an export by hand:

  • Everything is referenced by slug, never by row id.
  • Amounts are quoted decimal strings — price: '28.00', never 28.00. Unquote one and the import refuses the file.
  • Images are paths, not bytes, relative to the store's images root. A category's picture rides under image:.
  • Tags are labels matched on their derived slug, so Organic finds an existing organic.

display: is written only when it is not auto, and an empty translations: block is omitted. Output is ordered by slug, so the same catalog always exports to the same file.

YAML
# KahunaCart catalog export
# Amounts are decimal strings in the currency named above; import refuses a bare number.
version: 1
currency: USD
categories:
  - slug: apparel
    title: Apparel
    position: 0
    display: both
    image:
      path: 3/apparel-8c21f0a91b3e.jpg
      alt: A rail of shirts
  - slug: shirts
    title: Shirts
    parent: apparel
    position: 0
products:
  - slug: reef-runner-tee
    title: Reef Runner Tee
    type: physical
    status: published
    tax_class: standard
    categories:
      - apparel
      - shirts
    tags:
      - organic
      - sale
    images:
      - path: products/tee.jpg
        alt: A tee
        position: 0
    variants:
      - sku: KC-TEE-M
        title: Medium
        is_default: true
        position: 0
        price: '28.00'
        compare_price: '35.00'
        stock_managed: true
        stock_qty: 4
        weight_grams: 180

Export orders

BASH
bin/plugin kahunacart export --orders orders.csv
bin/plugin kahunacart export --orders --scope=line_items --format=json lines.json
bin/plugin kahunacart export --orders --set=Accounts --from=2026-01-01 --to=2026-01-31 january.csv
bin/plugin kahunacart export --orders --list-columns --scope=orders
Option Effect
--orders Export orders instead of the catalog.
--scope orders, one row per order, or line_items, one row per line. Default orders.
--set The name of a saved column set. Wins over --columns; using both is refused.
--columns Comma-separated column names, in the order you want them.
--format csv or json. Default csv.
--from, --to Completion dates, YYYY-MM-DD. --to includes the whole of that day.
--status Comma-separated order statuses. Every status but cart by default.
--list-columns Print the column vocabulary for --scope and stop.

This is the same exporter the admin's Export button calls, so both produce the same columns in the same order. Refunded orders are included, money is decimal strings in each order's own currency, dates are ISO 8601 UTC, and orders.export.max_rows caps a run. See Export orders with your own columns.

Export the whole store

BASH
bin/plugin kahunacart export --bundle=store.zip --passwords --secrets
bin/plugin kahunacart export --bundle=store.zip --except=orders,credit --no-files
bin/plugin kahunacart export --bundle=2026.zip --only=orders --from=2026-01-01 --to=2026-12-31
bin/plugin kahunacart export --list-sets
Option Effect
--bundle Write the whole store into this .zip instead of the catalog. .zip is added when the name has none.
--only Comma-separated part ids to export. Every part by default.
--except Comma-separated part ids to leave out. An id that does not exist is refused, so a typo never quietly exports less.
--no-media Leave the product and category picture files out, which leaves out the media part. The bundle keeps their file names.
--no-files Leave the digital product files out, which leaves out the files part.
--passwords Include customers' hashed sign-in passwords and their own Grav accounts. The hashes are not sealed with the passphrase, so handle the bundle like a database backup.
--secrets Include every key and secret, sealed in secrets.enc with a passphrase of at least 8 characters. The command asks for it twice, or reads KAHUNACART_EXPORT_PASSPHRASE.
--from, --to With --bundle, only orders placed between these dates, YYYY-MM-DD.
--list-sets Print every part this store can export, grouped, with its id, row count and the parts it depends on, and stop.

The bundle is written in one go, part by part, with a line printed for each. That makes the command the way to export a store too big for the job worker's time limit. The export is recorded in Import & Export → History as run from the command line. A bundle written inside user/data/kahunacart/exports can be downloaded from the admin and is deleted after export.keep_days. One written anywhere else is yours to keep. What a bundle holds, and what it leaves out, is on Export and restore.

import

Read a catalog file back in.

BASH
bin/plugin kahunacart import catalog.yaml
bin/plugin kahunacart import catalog.yaml --dry-run
Argument or option Effect
file The catalog file to read. Required.
--dry-run Validate the file and report what it holds without writing anything.
--strict Refuse a product that names an attribute the file never declared, instead of creating it as a text attribute. See Attributes in seed and catalog files.
--mapping=<name> Read the file as a CSV through a saved column mapping, named as it is named in the admin, instead of as a YAML catalog file.

Important

Import never deletes. A slug in the file that exists here is updated, one that does not is created, and anything in the store and not in the file is left as it was. Running the same file twice is the same as running it once.

In practice:

  • Products and categories are matched on slug.
  • Variants are matched on SKU when they have one, and on position when they do not.
  • Category assignments are added, not replaced. A file that puts a product in apparel does not take it out of sale.
  • Tag assignments are added too. Remove a tag in the admin instead.
  • Categories are imported in two passes, rows first and parent: second, so a file may list a child before its parent.
  • Existing images are left alone, alt text included.
  • Attribute definitions are brought into line with the file: created if missing, updated to match if already here. The type is the one field that can be refused; an attribute products already answer keeps its type, and the rest of the row is still updated.
  • Attribute answers are merged per product. A slug in the file is written, one the file does not mention is left alone.

The whole import runs in one transaction, so a malformed file leaves the catalog as it found it. On success it reports how many categories, products, variants, images, options, option values, translations, sales, attribute definitions and attribute values it wrote.

A file exported in a different currency is refused, not converted.

Validation happens up front and fails with a sentence naming the problem: an unknown category parent, a duplicate slug, or a price that is not a decimal string. Run --dry-run first on any file somebody sent you.

Pictures the store doesn't have are named before anything is written. The file records where each product's pictures and media files sit (media:) and where each category's picture sits (image:), never the files themselves. Before a dry run reports, and before a real import starts, the command looks for each of those paths in user/images/kahunacart/products and user/images/kahunacart/categories, and warns about the ones that are missing, by product or category slug. It lists the first twenty and says how many more there are. It is a warning, not a refusal: the import goes ahead, and those products show no picture until the files are copied into place. An embedded video has no file and is not checked.

A file with no products still imports when it carries categories, attributes, manufacturers, custom fields or sales. A file with none of these is refused.

The YAML both commands read and write is a narrow subset: block mappings and sequences, quoted and plain scalars, comments. Anchors, flow collections, block scalars and tab indentation are refused by name.

Import a CSV through a saved mapping

--mapping= switches import to the CSV importer: the same engine and the same saved mappings the admin importer uses.

BASH
bin/plugin kahunacart import products.csv --mapping="Woo products"
bin/plugin kahunacart import products.csv --mapping="Woo products" --dry-run

The mapping says which entity the file describes, which column is which field, which key rows are matched on, and whether an existing row is updated or skipped. Save one from KahunaCart → Import & Export in the admin. A name that does not exist makes the command list the saved names instead.

A command-line import runs in the foreground, prints its progress, and records a run exactly as the admin's queued import does. An interrupted run resumes when you run the same command again, without duplicating anything. It exits 1 when any row failed, and prints the first ten failures with their line numbers.

--dry-run rehearses the import inside a transaction that is always rolled back, then prints a per-row report. Nothing is written.

See Importing for the entities, the fields, the match keys and the value formats.

Restore a store bundle

A file ending in .zip is read as a store bundle.

BASH
bin/plugin kahunacart import store.zip --full --dry-run
bin/plugin kahunacart import store.zip --full
bin/plugin kahunacart import staging.zip --merge --only=catalog,media,promotions
Option Effect
--merge Add what the bundle has to this store. Rows matched by their natural key are updated, the rest are created with new ids, and nothing is deleted. Rows with no key of their own, such as a customer's addresses and group memberships or the shipping methods and tax rates under a zone, are only added under a parent the same merge created. An existing customer keeps their addresses, and an existing zone keeps its methods and rates. A catalog merge needs the same base currency on both stores.
--full Restore everything exactly as it was, ids included, into a store with no orders and no customers. It runs once: afterwards the store has both, and another full restore is refused.
--only Comma-separated part ids to restore. Every part in the bundle that can be restored here, by default.
--except Comma-separated part ids to leave out. Leaving out every part is refused, rather than read as "restore everything".
--dry-run Rehearse the database work inside a transaction that is rolled back, and print the report. Nothing is written. Files, accounts, settings files and the rebuilds only happen on the real run, so read its report too.

--merge or --full is required, and naming both is refused: adding to a store and replacing an empty one are different jobs. When the bundle carries secrets, the command asks for the passphrase or reads KAHUNACART_EXPORT_PASSPHRASE. Leave it empty to restore without them.

The report prints what was created, updated, skipped and failed for each part, what could not run, the keys to enter again, and, when the store moved, the old site's checklist and its redirects: block. The command exits 1 when any row failed. See Export and restore.

squarespace

Bring a whole Squarespace store and site across from a folder of its exports: products, customers, orders, pages and the blog.

BASH
bin/plugin kahunacart squarespace ~/Downloads/squarespace --dry-run
bin/plugin kahunacart squarespace ~/Downloads/squarespace --weight-unit=lb --dimension-unit=in
Flag Effect
--dry-run Convert and check everything, and write nothing to the store or the pages.
--weight-unit= The unit the Squarespace store weighs in: lb, oz, kg or g. Required when the products file has weights.
--dimension-unit= The unit it measures in: in, cm or mm. Required when the products file has sizes.
--only= Comma-separated steps to run: products, customers, orders, site.
--no-images Don't download pictures.
--tidy-titles Title Case the product titles, move a quoted name to the front and drop "Available in …" tails. Give it your brand (--tidy-titles="Acme") to take the brand off the front of every title.
--products=, --orders=, --customers=, --site= Name the file for a step when the folder has more than one of that kind.

The converted files (catalog.yaml, customers.csv, orders.csv and redirects.yaml) go in a kahunacart/ folder inside the export folder, so you can open them and see exactly what was imported. It exits 1 when any step failed. See Importing → Moving from Squarespace for which exports to download and what comes across.

seed

Fill an empty catalog with a demo store.

BASH
bin/plugin kahunacart seed
bin/plugin kahunacart seed --purge
bin/plugin kahunacart seed --zones --currencies=EUR,GBP
Option Effect
--purge Remove what the seed file names instead of creating it. Lists everything first and asks.
--zones Also seed tax and shipping zones for a US store with EU and UK customers. With --purge, remove exactly those zones again.
--strict Refuse a product that names an attribute the file never declared, instead of creating it as a text attribute. See Attributes in seed and catalog files.
--currencies <codes> Also price the demo in these other currencies, a comma-separated list from EUR, GBP, CAD, AUD, CHF and JPY. Writes the prices and one fixed-amount sale, never configuration. See below.

It creates a nine-category tree — Clothing → Mens → Tops/Bottoms/Accessories, Clothing → Womens → Tops/Accessories, and a flat Digital Goods — plus eleven products filed on the leaves and tagged organic, new and sale.

Two of the products show a backorder: the Tide Line Beanie's Kelp colour at zero with a backorder note, and the Saltwater Socks with one pair left, so two in the cart read "1 of 2 on backorder". See Telling the shopper it is on backorder.

--currencies. seed --currencies=EUR,GBP gives the demo a price in each of those currencies, so a store that sells in more than one has something to look at. It works on a store whose base currency is USD, and it refuses a code it has no rate for, naming the ones it has. Each amount is the base amount times one fixed rate (EUR 0.92, GBP 0.79, CAD 1.37, AUD 1.52, CHF 0.88, JPY 150), tidied so it looks typed and not converted: a price goes to the nearest price ending .99, a coupon amount or a sale amount to a whole number, and in yen both go to the nearest ten. The rate isn't kept and nothing converts later. It prices every variant and its compare-at price, the pay-what-you-want maximum, the seeded coupons' fixed amounts and minimum order totals (the FREESHIP minimum is the demo's free-shipping threshold), and the cost of the shipping methods --zones seeds. It adds one sale with a fixed amount off, Presets Deal, because the demo's own sales are percentages and a percentage needs nothing in another currency. Three products, coldwater-cool-box, saltwater-socks and tide-chart-2027, are left without a euro price on purpose, so "Not available in EUR" is on show. The seed has no priced add-ons, deposits or collection points, so there is nothing to give those.

It never edits configuration. The amounts are written either way and stay hidden until the store sells in the currency, so when a currency isn't in currencies.enabled yet the command prints the currencies: block to add. Run bin/plugin kahunacart index --rebuild once the currencies are on. It fills and never overwrites: an amount the store already has is left alone, so running it twice changes nothing and a price somebody typed survives, and it adds the other currencies to products the seed made on an earlier run. Without the flag the command writes exactly what it always wrote. --purge takes the amounts and the sale away with the rest. See Demo store.

The command is idempotent: a slug that already exists is skipped and never updated, so it cannot overwrite a real product.

It is also reversible. --purge removes exactly the slugs the seed file names, deepest categories first, with their pictures. Seeded tags and seeded attribute definitions are removed only when nothing else uses them. Existing orders are unaffected either way.

Caution

--purge deletes catalog rows. The confirmation defaults to no, so running it with --no-interaction aborts rather than wiping a catalog.

Digital products are seeded with no files attached, and the command says so when it finishes. Until you upload one, a digital order completes normally and grants nothing.

No photographs ship either. Drop a picture named after a product's slug into seed/images/, or after a category's slug into seed/images/categories/, and the seeder imports it. Both directories are optional. See seed/README.md.

Attributes in seed and catalog files

seed and import read product attributes from the same two places: an attributes: list at the top of the file that declares the definitions, and an attributes: map on each product that answers them by slug.

YAML
attributes:
  - slug: material
    label: Material
    type: select
    group: Materials
    filterable: true
    options:
      - Organic Cotton
      - Merino Wool

  - slug: fabric-weight
    label: Fabric Weight
    type: number
    unit: gsm
    group: Materials
    filterable: true

products:
  - slug: reef-runner-tee
    attributes:
      material: organic-cotton
      fabric-weight: 185
      machine-washable: true
      care: |
        Machine wash cold, hang dry.

The attributes: block is optional.

Key Behaviour
slug Optional. Derived from label when it is absent.
label Required unless slug is given.
type One of the nine attribute types. Defaults to text.
options Required for select and multiselect. A plain label per entry, or a {slug, label} mapping.
unit A number's unit. Nulled on the other eight types.
group The specification-table heading. group in the file, group_label in the database.
position Optional. Defaults to the order the file lists them in.
filterable Defaults to false. Refused on textarea and url.
filter_panel Defaults to true. Whether a filterable attribute is drawn in the filter panel; off keeps it linked from product pages and honoured in URLs.
show_on_product Defaults to true.
translations Per-language label and group_label overrides. Written by export, read by import.

A product's attributes: map is keyed by attribute slug. Values go through the same validation as the admin and the API, so a value the type will not take stops the run.

export writes filterable, filter_panel and show_on_product on every attribute, even at their defaults.

An attribute the file never declared

A product may name an attribute with no entry in the attributes: block. By default the command creates it as a text attribute, labels it from the slug, and says so:

TXT
note     created attribute 'part-number' as text (named by reef-runner-tee, not declared in the attributes block)

A minted definition is always text, so a part number that is all digits does not become a number attribute. Change the type in the admin while the attribute is still unanswered.

--strict turns that warning into a refusal on both seed and import, naming the slug and what the file does define. Use it for a CI import of a file that is supposed to be complete.

Re-running the seeder

seed creates definitions and never updates them, so a definition a merchant has edited is left alone on a second run.

A product that already exists is still skipped, with one exception: the answers it does not yet have are filled in, and it reports filled {slug} (N attribute value(s)). Answers somebody typed are never overwritten.

What --purge removes

The seeded products go first, taking their answers with them. Then each attribute definition the seed file declares is checked:

  • A definition nothing answers any more is deleted, and the command prints removed attribute {slug}.
  • A definition your own products still answer is kept, values and all, and the command prints kept attribute {slug} (N product(s) of your own still answer it).

The final line reports products, categories, tags, attribute definitions and attribute values removed.

webhooks

Show what the payment providers have been sending.

BASH
bin/plugin kahunacart webhooks
bin/plugin kahunacart webhooks --provider=stripe --limit=50
bin/plugin kahunacart webhooks --purge
Option Default Effect
--provider=<slug> all Only show deliveries for this provider.
--limit=<n> 25 How many deliveries to list. Capped at 200.
--purge off Delete deliveries past the retention window and stop.

It prints recent deliveries with their time, provider, action, status, reference and error, then per-provider totals with the current failure streak. A streak counts consecutive failed or invalid deliveries since the last success.

A streak is reported, never acted on: KahunaCart never disables a provider automatically. Check the provider's webhook secret and endpoint URL first.

--purge sweeps rows older than payments.webhook_log_days, default 30. At 0, deliveries are kept forever and --purge deletes nothing. The worker tick does the same sweep on its own.

With no deliveries recorded, the command names the registered providers instead.

coupons:generate

Mint a batch of unique discount codes into a campaign.

BASH
bin/plugin kahunacart coupons:generate --list
bin/plugin kahunacart coupons:generate --group=3 --count=500
bin/plugin kahunacart coupons:generate --group="Autumn insert" --count=10000 --csv > codes.csv
Option Default Effect
--list off Print the campaigns with their code counts and redemptions, then exit.
--group, -g none Which campaign to mint into: an id, or the campaign's name. A name survives across environments where an id does not.
--count, -c 1 How many codes, between 1 and 50,000.
--csv off Print only the codes, one per line under a code header, to redirect straight into a file.

Codes are drawn from Crockford base32 (the alphabet less I, L, O and U, so a code survives being handwritten and read back) using the campaign's prefix and pattern. The whole batch is checked for collisions and written in one transaction.

A batch larger than half the pattern's keyspace is refused, and the refusal names the pattern so you can widen it.

The campaign itself is created in the admin or through the API, not here.

The exit code is 1 on an unknown campaign, a count out of range, or a pattern too narrow to hold the batch. See Coupons.

orders:reindex

Rebuild the admin order search: the search_text column, the engine text index, or both.

BASH
bin/plugin kahunacart orders:reindex
bin/plugin kahunacart orders:reindex --text
bin/plugin kahunacart orders:reindex --index
Option Default Effect
--text off Only rewrite kahunacart_orders.search_text from the order rows, a thousand at a time.
--index off Only re-decide which text index this engine can offer, and build it.
neither — Both, which is what you want after a restore or an import.

Run it when an importer or a SQL script wrote orders directly into the tables, when a release changed what is searchable, or when you restored the database onto a different engine. After a restore onto a different engine, the recorded index no longer belongs to the engine it is sitting on, so search falls back to a LIKE scan until this runs.

The last line names the index the store ended up with, or warns that the store is on the portable LIKE scan. bin/plugin kahunacart status says the same thing without doing any work.

Safe on a live store. The text rewrite is one transaction per batch, so an interrupted run leaves every finished row correct; re-running it is the whole recovery. See Orders and the admin.

index

Build, drain or report on the catalog index: the table behind every listing, facet count, sort and search.

BASH
bin/plugin kahunacart index
bin/plugin kahunacart index --rebuild
bin/plugin kahunacart index --dirty --chunk=250
Option Default Effect
--rebuild off Empty the index and build it again from the catalog, in chunks.
--dirty off Rebuild only the products marked dirty, then stop.
--chunk=<n> 1000 Products rebuilt per transaction, the same size catalog.index.chunk sets for the background rebuild. Clamped between 1 and 10,000.
--json off Print a machine-readable summary instead of a report.
neither --rebuild nor --dirty — Report what is there and change nothing.

The no-flag report is a table: whether the index is enabled, the catalog.index.mode in force, whether it has ever been built, whether any listing is reading it, its rows and products, how many products are queued, and the text index this database engine gave the store.

TXT
 Enabled             yes
 Mode                auto
 Built               yes
 Answering listings  no (no sale, no price list, no search term)
 Rows                12,480
 Products            9,910
 Queued              0
 Search index        sqlite_fts5

The search index is one of sqlite_fts5, mysql_fulltext, pgsql_tsvector, or prefix on an engine that offers none of the three.

Note

Answering listings: no is usually correct rather than broken. In the default catalog.index.mode: auto the index is joined only when a sale, a price list or a search term needs it, so a store running no promotions answers its listings with live queries. See The catalog index.

Run --rebuild when:

  • You have just updated and want the index for the first time.
  • A big import, a SQL script or another tool wrote products directly into the tables.
  • You restored the database, especially onto a different engine, whose recorded text index no longer applies.

Run --dirty after a bulk edit or a saved sale when you do not want to wait for the next work tick. That tick drains catalog.index.per_tick products a minute on its own.

A rebuild finishes with a count of products indexed, the rows and products in the index, anything still queued, the search index in use, and the elapsed time. The exit code is 1 only when the schema is out of date.

On a store that sells in more than one currency it adds a line for the extra currencies: whether the index has prices for every one of them, or which were added since it was built, with the command to run. The --json report carries the same under currencies, and a rebuild reports how many price rows it wrote. A store with one currency prints none of this. See More than one currency.

Safe on a live store. A listing hitting a half-built index falls back to live queries rather than to an empty shop, and a killed run leaves finished products correct and the rest queued; run it again to finish.

search:reindex

Rebuild the storefront product search index from the catalog.

BASH
bin/plugin kahunacart search:reindex

The command takes no arguments and no options.

Most stores never need it. The index follows every product write on its own, and a store whose index has gone missing rebuilds it on the next search. Run it when none of that applies:

  • Something wrote products around the plugin: a SQL script, a restored dump, a migration from another platform, bench:seed. Detection is a high-water mark on the highest product id, not a checksum, so a bulk UPDATE in place goes unnoticed.
  • A release changed what goes into a document. The index holds the text as it was read, so a version that starts indexing a new field covers only products saved after the update.
  • You would rather pay for the build now than on a shopper's first search. A deploy that does not carry user/data across arrives with no index at all. Put this in the deploy step.

It prints the path it is building, a progress line every 2,500 products, then how many products went in and how long it took. Drafts and unlisted products are not indexed, so the count is the number of products a shopper could browse to. A store with nothing published gets a note saying so, not an error.

On a store that is not on the yetisearch rung — search.products.driver set to like or sql, or a PHP build with no pdo_sqlite and FTS5 — it warns and does nothing: there is no file index to build. bin/plugin kahunacart index --rebuild rebuilds the catalog index's own search instead. See Storefront for the whole ladder.

Safe on a live store, with one caveat: the old index file is removed before the new one is built, so for the second or so the build takes, another request's search falls through to the portable LIKE scan.

search:embed

Embed the product search index for semantic search now, in this shell.

BASH
bin/plugin kahunacart search:embed
bin/plugin kahunacart search:embed --reset
Flag Default Effect
--reset off Forget every stored vector first, so the whole catalog is embedded again.

You don't have to run this. The job worker embeds products by itself after a reindex or a product save. The command is for when you'd rather watch: the first run after you switch semantic search on, a quick check that the API key works, or a deploy step.

It sends the waiting products to the provider in batches of search.products.semantic.batch_limit, prints each batch, and keeps going until nothing is waiting. It has no time limit, unlike the job. On the first error the provider returns, it prints the error and exits 1, which makes it the fastest way to find out what's wrong with a key or a model name.

Changing model, dimensions or a prefix doesn't need --reset, because every product is marked waiting by itself. --reset is for a provider that sent back bad vectors under a model name that didn't change. Every product sent is a call to the provider, and a hosted API charges for those.

When nothing is left waiting, it measures the noise gate if it needs measuring, as search:calibrate does, and prints the min_margin in use. It warns and does nothing when semantic search is off or the store isn't on the yetisearch engine.

search:calibrate

Measure semantic search's noise gate on this catalog now, and print the working.

BASH
bin/plugin kahunacart search:calibrate

A search that means nothing scores higher against a big catalog than a small one, so the min_margin that keeps nonsense out of a small store lets it into a big one. This command searches for about ninety nonsense strings (made-up words, keyboard mashes, placeholder Latin and everyday phrases nobody shops for), each one embedded exactly as a shopper's search would be, and measures how far each one's best product card stands above the median card. A string that keywords actually find in your catalog isn't nonsense there, so it's left out, and the command names it.

It prints the spread of those margins (minimum, median, mean, standard deviation, 95th percentile and maximum) and the min_margin chosen from them: the mean plus calibration_strictness standard deviations, two by default. It also says whether the gate is using it. It is while search.products.semantic.calibration is auto. With off, your configured min_margin applies and the measurement is only reported.

You don't need to run it. The worker calibrates by itself at the end of an embedding run whenever the numbers are out of date: there are none yet, the model or query_frame changed, or the catalog grew or shrank by more than a quarter. This command is for seeing the numbers.

The first run with a model embeds the probes in one call to the provider, a fraction of a cent on a hosted API, and caches them, so running it again costs nothing until the index is rebuilt. It exits 1 while products are still waiting to be embedded or on a provider error, and does nothing on a catalog of fewer than ten products, where the gate never applies. See Storefront → Tuning semantic search.

reports:rebuild

Recompute the daily report rollup: kahunacart_order_stats and kahunacart_product_stats.

BASH
bin/plugin kahunacart reports:rebuild
bin/plugin kahunacart reports:rebuild --from=2026-01-01 --to=2026-01-31
Option Default Effect
--from=YYYY-MM-DD The store's first trading day First day to recompute, inclusive.
--to=YYYY-MM-DD Yesterday Last day to recompute, inclusive.

Today is never rolled up, because the reports scan it live, so --to is clamped to yesterday.

Recomputing is idempotent: a day's row is rebuilt from that day's rows rather than adjusted by a delta. Safe on a live store and in a deploy script.

Run it after an import, after a restore that carried the orders and not the rollup, or after editing the lines of an order on a day that had already closed. See Reports and the rollup for when that is needed.

The last line says how many days were recomputed and the date the rollup is now authoritative through.

downloads:check

Report every product file a customer could not actually be handed.

BASH
bin/plugin kahunacart downloads:check
bin/plugin kahunacart downloads:check --all
Option Default Effect
--all off Also list the files that are fine, one per line.

It prints the allowed roots first — downloads.path plus anything in downloads.allowed_roots — then a table of the problems, with the file id, product, name, stored path and reason:

Problem What happened
missing The row points at a path with nothing on the end of it. A customer clicking that link spends a download and gets an error page.
outside roots The bytes exist, but somewhere the allowlist does not name, so delivery refuses them. Usually a store that moved its files and changed downloads.path without adding the old location.

The command is read-only. It exits 1 when it found a problem and 0 otherwise, including on a store with no product files, so it works as a deploy check.

Tip

Run it after moving the files root, and after any deploy that changes downloads.path or downloads.allowed_roots. See Hardening → Restrict where downloadable files may live.

retention:run

Run the data-retention sweep now: archive placed orders whose payment never arrived, archive orders whose payment failed, anonymise completed sales, and erase inactive customers.

BASH
bin/plugin kahunacart retention:run --dry-run
bin/plugin kahunacart retention:run
bin/plugin kahunacart retention:run --limit=1000
Option Effect
--dry-run Report what would happen and change nothing. The log rows are still written.
--limit=N Rows one rule may touch this run, overriding retention.chunk.

Every window is off until you set one, so on an unconfigured store this prints which keys to set and exits 0. Otherwise the output is a table of rule, window, cutoff date, rows examined, rows affected and any error, followed by the order numbers each rule touched.

Completed orders are anonymised rather than deleted, so the store's revenue figures do not move. Trashing means archiving, and the two trash rules delete nothing.

Caution

A store switching the schedule on for the first time has years of orders in scope, and there is no undo. Always --dry-run first.

The same four rules run from the worker tick, so a store with the scheduler installed needs this command only to check its settings or to catch up in a hurry. See The retention schedule.

orders:reassign

Move every order from one customer to another, with its history, its notes, its refunds and its download grants.

BASH
bin/plugin kahunacart orders:reassign --from=42 --to=57 --dry-run
bin/plugin kahunacart orders:reassign -[email protected] -[email protected]
Option Effect
--from The customer to move orders from: an id or an email address.
--to The customer to move them to: an id or an email address.
--dry-run List exactly what would move and write nothing.

Transactions, refund lines and everything else hanging off an order travel with it. Each order gains a history entry naming the move.

The exit code is 1 on an unmatched --from or --to, or when the two resolve to the same customer. See Reassign orders to another customer.

bench:seed

Fill the store with a benchmark-sized catalog, customer list and order history.

BASH
bin/plugin kahunacart bench:seed
bin/plugin kahunacart bench:seed --products=1000 --customers=5000 --orders=20000
bin/plugin kahunacart bench:seed --json > seed.json
bin/plugin kahunacart bench:seed --purge
Option Default Effect
--products 5000 How many products to create.
--customers 25000 How many customers to create.
--orders 250000 How many orders to create.
--seed 20260830 The integer every value derives from.
--now 2026-08-30T00:00:00Z The instant the store is seeded as of. Every date is an offset from it.
--batch 1000 Rows per INSERT transaction.
--purge off Remove everything the benchmark seeded, and stop.
--force off Seed even though the store holds orders the benchmark did not write.
--json off Print a machine-readable summary instead of a report.

The default size takes about 30 seconds on an M-series Mac with SQLite and writes roughly 4.5 million rows, about 1.5 GB. --purge takes it back out in about 12 seconds.

Every row the seeder writes carries a bench- slug, a BENCH- order number, a BENCH coupon code or an address at bench.kahunacart.test, so --purge finds exactly its own rows and leaves everything else alone. The command counts the orders that are not its own before it starts and exits 1 when it finds any, unless you pass --force.

Caution

Never run this against a live store. It is reversible, but a quarter of a million orders is not something to discover by accident.

See Benchmarking for what the seeded store contains and how to compare the three database engines.

bench:queries

Time the queries behind the storefront and the admin against whatever the store now holds.

BASH
bin/plugin kahunacart bench:queries
bin/plugin kahunacart bench:queries --iterations=100 --warmup=10
bin/plugin kahunacart bench:queries --json > queries.json
Option Default Effect
--iterations 50 How many timed runs per query.
--warmup 5 How many untimed runs first.
--json off Print a machine-readable document instead of a table.

It prints fourteen queries with min, p50, p95 and max in milliseconds and the row count each returned. Each calls the same repository and service methods the storefront and the admin call, in process, so the figure is the database alone, without PHP, Twig or the web server.

The command is read-only in effect: the cart it prices is removed afterwards and the stock it takes is put back, so a second run measures the same store as the first.

Both --json documents carry the plugin version, the git commit, the engine and its version, the PHP version, the seed and the instant.

check

The installation doctor: up to sixteen checks, each with a suggested fix. Every check has its own section in Going live, which is where a finding points.

BASH
bin/plugin kahunacart check
bin/plugin kahunacart check --offline
bin/plugin kahunacart check --json
bin/plugin kahunacart check --report > support.txt
Option Default Effect
--offline off Skip the two network probes: the provider connection checks, and a ping per active outbound subscription.
--json off Print the findings as JSON.
--report off Print the redacted support report — findings, environment and configuration with every secret-looking key replaced — to paste into a ticket.

Thirteen always run: the database engine and its strict mode, pending migrations, the scheduler, the mail transport, the worker environment, download roots, trusted proxies, the site address, the store status, provider credentials, webhook reachability, template drift and the report rollup.

While the shop is mounted at the site root, the store route check joins them. While agentic.enabled is on, agentic readiness joins them, and while semantic product search is on, so does the semantic search check.

The report rollup is the one check that can only warn. The rollup was last built against an older set of measures, usually because the store upgraded past the migration that added cost of goods. The figures stay correct, just worked out from the orders table rather than read from a day row. Fix it with bin/plugin kahunacart reports:rebuild. The dashboard says the same through its The report rollup needs rebuilding alert. See Analytics and Dashboard alerts.

The exit code is 0 when every check passes or warns and 1 when any fails, so a deploy script can gate on it. A store with demo.readonly on skips the network probes whether you asked for --offline or not. Nothing writes apart from the pings, which land in the delivery log, so it is safe to re-run. The admin's Health screen runs the same checks.

reset

Wipe the test data before going live and keep every setting. See Going live → Before the checks.

BASH
bin/plugin kahunacart reset --orders --keep-settings
bin/plugin kahunacart reset --orders --customers --keep-settings
bin/plugin kahunacart reset --orders --keep-settings --keep-numbers --force
Option Default Effect
--orders off Empty every table that hangs off an order.
--customers off Empty the customer rows with their sign-in accounts, addresses, wishlists and reviews.
--keep-settings — Required. It makes the command line say out loud what survives.
--keep-numbers off Leave the order-number sequence where it is instead of restarting it.
--force off Answer the confirmation yes.

--orders empties orders, lines, adjustments, transactions, refund items, shipments, history, order changes, download grants, stock holds, coupon usages, credit notes, gift cards, the ledger, the report rollups, the job queue, both webhook logs, rate-limit counters, and the retention and import runs. With --customers alone the orders stay and become guest orders.

Products, categories, tax, shipping, coupons, payment methods and settings are never on either list. Coupon usage counters go back to zero, the merchant-notification dedupe stamps are cleared, and the order-number sequence restarts at order_number_start unless you pass --keep-numbers.

The plan — every table with its row count — is printed first, and the question defaults to no.

Caution

This is not reversible. Take a backup first. It is refused outright on a store with demo.readonly on.

templates:drift

List every theme override of a shipped template that has fallen behind it.

BASH
bin/plugin kahunacart templates:drift
bin/plugin kahunacart templates:drift --all
bin/plugin kahunacart templates:drift --json
Option Default Effect
--all off List every shipped template with the state of its override, not only the drifted ones.
--json off Print the rows as JSON.

An override is drifted when its @version stamp is lower than the shipped template's, or when it carries no stamp at all. Both versions are printed. The exit code is 1 when anything is drifted, so it can gate a deploy after a plugin update. See Theming.

jobs:list

The job queue from a shell. It reads the same rows as the admin's Jobs screen.

BASH
bin/plugin kahunacart jobs:list
bin/plugin kahunacart jobs:list --state=failed --limit=100
bin/plugin kahunacart jobs:list --type=order.confirmation_email --json
Option Default Effect
--state=<state> every state One of pending, running, complete, failed or cancelled.
--type=<type> every type One job type, such as order.confirmation_email.
--limit=<n> 25 How many jobs to list.
--json off Print the page as JSON, which is the only form that includes each job's arguments.

Each row carries the state, when the job was queued and started, how long it took, how many attempts it has had, and the last error. Finished jobs are swept after jobs.retention_days, so a job that has aged out of this list is not a job that vanished.

jobs:run

Run one queued job now, in this process, whatever it was waiting for.

BASH
bin/plugin kahunacart jobs:run 4821
Argument Effect
id The job id, from jobs:list. Required.

A failed job is granted one more attempt, and the outcome is printed. Use it when a customer says they never got their confirmation and you have the job id.

jobs:cancel

Cancel a pending job, or ask a running one to stop.

BASH
bin/plugin kahunacart jobs:cancel 4821
Argument Effect
id The job id, from jobs:list. Required.

A pending job is cancelled outright. A running one is asked to stop at the next transaction boundary, so a bulk status change stops between orders rather than mid-write.

webhooks:subscriptions

Outbound webhook subscriptions and their delivery log. This is the other direction from webhooks, which is the log of what payment providers send to the store. See Outbound webhooks.

BASH
bin/plugin kahunacart webhooks:subscriptions
bin/plugin kahunacart webhooks:subscriptions --topics
bin/plugin kahunacart webhooks:subscriptions --create --name="Warehouse" --topic=order.completed --url=https://wms.example/hook
bin/plugin kahunacart webhooks:subscriptions --ping=3
bin/plugin kahunacart webhooks:subscriptions --deliveries=3 --limit=100
bin/plugin kahunacart webhooks:subscriptions --redeliver=9912
bin/plugin kahunacart webhooks:subscriptions --purge
Option Default Effect
--topics off Print every topic a subscription may name, and stop.
--limit=<n> 25 How many rows to list.
--create off Create a subscription from --name, --topic and --url.
--name=<text> — With --create: what to call it.
--topic=<topic> — With --create: order.completed, order.*, or event.<Name>.
--url=<url> — With --create: the https endpoint to POST to.
--ping=<id> — Send a signed ping to that subscription. A 2xx makes it active.
--pause=<id> — Pause that subscription.
--delete=<id> — Remove that subscription.
--deliveries[=<id>] off List recent delivery attempts, for every subscription or for one.
--redeliver=<id> — Queue that delivery row's occurrence again, as a new attempt.
--purge off Delete delivery attempts past webhooks.retention_days and stop.

--create prints the signing secret once, as the admin does; copy it before the command exits. --deliveries is one row per attempt rather than per occurrence. --purge does now what the worker's tick does on its own.

lang:export

Write the translation source — languages/en.yaml with its comments stripped — for upload to the community translation project, or report how complete each language pack is.

BASH
bin/plugin kahunacart lang:export                          # the source as YAML, to stdout
bin/plugin kahunacart lang:export source.yaml              # or to a file
bin/plugin kahunacart lang:export --format=json            # as a flat key-to-string map
bin/plugin kahunacart lang:export --status                 # one row per pack
bin/plugin kahunacart lang:export --status --missing=de    # the keys de.yaml still lacks
Argument or option Default Effect
file stdout Where to write it. Omit it, or pass -, to print to stdout.
--format yaml yaml or json.
--status off Print the coverage table — translated, missing, stale, per language — and stop.
--missing=<code> — With --status, list the untranslated keys for one language, and any stale keys it carries.

A key whose English value is intentionally blank is not counted as a string to translate. See Translating KahunaCart.

config:export

Write this store's configuration — the plugin's YAML and the commercial policy the database holds — to a JSON document.

BASH
bin/plugin kahunacart config:export                              # to stdout, so it can be piped
bin/plugin kahunacart config:export --out=store.json
bin/plugin kahunacart config:export --out=tax.json --include=tax,shipping
bin/plugin kahunacart config:export --out=store.json --exclude=report_views
Option Default Effect
--out=<file> stdout Where to write it.
--include=<sections> every section Only these sections, comma-separated.
--exclude=<sections> none Everything but these sections, comma-separated.

The six sections are plugin, settings, tax, shipping, promotions and report_views. A name that is not one of those is refused, with the list.

Section Holds
plugin The plugin's own YAML configuration, every key on Configuration
settings The kahunacart_settings table: invoice counters, feature flags, property labels and the rest
tax Tax zones, rates and their rules
shipping Shipping zones, methods, rates and pickup locations
promotions Coupons, sales and price lists
report_views Saved report views and per-user column choices

This is not the catalog, which is export, and not orders or customers. Nor is it the store status (store.status and its message), which belongs to the site. See Configuration → Store status.

Important

Secrets leave as environment references, never as values: every key the exporter recognises as a secret is written as $KAHUNACART_…. The document is safe to commit, and the store it lands on reads the real value from its own environment. See Configuration → Environment variables for the eight patterns that count as a secret.

Two rows are never exported: reports.stats_through and reports.stats_schema. They describe the state of this store's own rollup.

config:import

Apply a document written by config:export.

BASH
bin/plugin kahunacart config:import store.json --dry-run   # say what would happen, write nothing
bin/plugin kahunacart config:import store.json
Argument or option Default Effect
file — The export document to apply. Required.
--dry-run off Report the plan and write nothing at all.

Four guarantees:

  • --dry-run writes nothing. The transaction is never opened and the YAML file is not touched. Run it first on anything you did not export yourself.
  • The document is validated end to end before anything is written. Every problem is reported together, and a document with any problem writes nothing.
  • The import runs inside one transaction, and is idempotent. Applying the same document twice gives the same store as applying it once.
  • A section the document does not carry is not touched. A document holding only tax replaces only tax.

And the store stays as open as it was. Whatever the plugin section says under store, the file keeps this site's store.status and store.status_message, so importing staging's configuration into the live store neither pauses nor opens it.

The YAML half gets a stamped backup beside it before it is written, and the command prints where. A file cannot join a database transaction, so that is the half to put back by hand. Comments in the configuration file are not preserved; the values are.

feeds:build

Build the Google Merchant Center and OpenAI product feeds now, into the same place the background job builds them, so the files are served straight away.

BASH
bin/plugin kahunacart feeds:build                          # every enabled feed, every language
bin/plugin kahunacart feeds:build --feed=google            # one feed: google, google-checkout or openai
bin/plugin kahunacart feeds:build --lang=de                # one language
bin/plugin kahunacart feeds:build --currency=EUR           # one currency, on a store that sells in more than one
bin/plugin kahunacart feeds:build --out=/var/feeds         # also copy the files there, for sftp or rsync
Option Default Effect
--feed every enabled feed Build one feed: google, google-checkout or openai. A feed named here is built even while it is switched off, which is how a store that only pushes the OpenAI file runs with feeds.enabled off.
--lang every language Build one of the site's languages.
--currency every currency Build one currency's files, on a store that sells in more than one currency.
--out none Also copy the files to this directory, one subdirectory per language on a multilanguage site and one per currency other than the store's own.

It prints each file with its language, row count, the start of its SHA-256 and where it was written. Only a build with none of --feed, --lang and --currency deletes the files of feeds, languages and currencies that are no longer on. See Product feeds.

agentic:keys

Show the UCP signing keys, and make the first one when there is none.

BASH
bin/plugin kahunacart agentic:keys
bin/plugin kahunacart agentic:keys --rotate

--rotate adds a new key and retires the current one, which stays in /.well-known/ucp for seven days so platforms can still verify webhooks it signed. The command prints each key's id, when it was made and whether it is current or retired, then either the profile URL or every readiness problem still keeping /.well-known/ucp at 404. The keys are encrypted with the site secret. If that secret changed and the keys can no longer be read, the command says so and --rotate replaces them. See UCP checkout → Signing keys.

Scaffolding: the make: commands

Five commands that write a working starting point, so you edit code rather than create it.

make:provider, make:shipping-method, make:discount-operation, make:admin-section

Scaffold a complete, installable KahunaCart add-on plugin.

BASH
bin/plugin kahunacart make:provider warranty
bin/plugin kahunacart make:shipping-method regional --title="Regional Courier"
bin/plugin kahunacart make:discount-operation bundle --vendor=acme --author="A Merchant"
bin/plugin kahunacart make:admin-section returns --dir=user/plugins/kahunacart-returns --force
Argument or option Default Effect
name — The name after kahunacart-, such as warranty. Lowercase letters, digits and single dashes. Required.
--dir=<path> user/plugins/kahunacart-<name> Where to write it.
--title=<text> the name in title case The human name.
--description=<text> — One sentence for the blueprint and composer.json.
--author=<name> — The author name for the blueprint.
--vendor=<name> — The Composer vendor name.
--force off Overwrite files that already exist.

All four take the same arguments and write the same skeleton: the plugin class, its config defaults, a blueprint with the admin form, a language file, a composer.json, a phpunit.xml, a test bootstrap that works before composer install, a README, a CHANGELOG and a .gitignore. Each also writes the one class that does the work, and a test that proves it does.

Command The class it writes What it registers
make:provider classes/<Name>Provider.php A payment provider, on onKahunaCartRegisterProviders
make:shipping-method classes/<Name>ShippingMethod.php A shipping rate calculator
make:discount-operation classes/<Name>Operation.php A discount operation, on the operation registry
make:admin-section classes/<Name>Section.php and admin-next/sections/<name>.js An admin screen, on onKahunaCartAdminSections

The generated test passes as written. Run it, then replace the TODO markers:

BASH
bin/plugin kahunacart make:provider warranty
cd user/plugins/kahunacart-warranty && composer install && vendor/bin/phpunit

Then run bin/grav clear, enable the plugin under Plugins, and check bin/plugin kahunacart status for what KahunaCart now sees registered. See Provider developer guide for what each kind has to implement.

make:migration

Write the next numbered, up-only migration file.

BASH
bin/plugin kahunacart make:migration warranty_registrations
bin/plugin kahunacart make:migration warranty_registrations --plugin=kahunacart-warranty
Argument or option Default Effect
name — What it does, in lowercase and underscores, such as warranty_registrations. Required.
--plugin=<slug> the base plugin The add-on slug whose migrations/ directory gets the file.
--dir=<path> — The migrations directory to write into. Overrides the default.
--force off Overwrite a file that already exists.

The number is the next after the highest already in that directory, and a gap left by a reserved number is never filled.

An add-on's migration carries the add-on's own prefix, warranty_0001_registrations.php, because migration names are global across every plugin sharing the store's tracking table.

Apply it with bin/plugin kahunacart migrate. An add-on has to register its own migrations/ directory on onKahunaCartRegisterMigrations first.

Run the scheduler

If bin/plugin kahunacart work produces results by hand but nothing happens on its own, Grav's scheduler is not installed in cron:

BASH
bin/grav scheduler --jobs      # is kahunacart-jobs registered?
bin/grav scheduler --install   # print the crontab line to add
bin/grav scheduler --run       # force a run now, to prove it works

Warning

Without the scheduler in cron, order confirmations never send, download grants are never minted, stock holds never expire, and carts are never purged. This is the most common cause of "the customer never got their download". See Troubleshooting.

On a store with a user/env/<host>/ folder, the cron line --install prints runs the scheduler, and the worker it starts, as the cli environment, which reads user/config and never that folder. Admin Next saves settings into the folder when it exists, so the worker can end up sending email with settings the site does not use. Start the cron line with GRAV_ENVIRONMENT=<host>, set it in the root .env, or keep the settings in user/config:

BASH
* * * * * cd /path/to/grav && GRAV_ENVIRONMENT=shop.example.com php bin/grav scheduler 1>> /dev/null 2>&1

bin/plugin kahunacart check says when this is happening. See Going live → Worker environment.

Newsletters add-on commands

The Newsletters add-on has its own command namespace, bin/plugin kahunacart-newsletters. Everything else it needs is already a base plugin command: migrations are migrate, the worker that runs its imports and sends its campaigns is work, and its tables appear in the plan reset prints.

Command What it does
bin/plugin kahunacart-newsletters deliverability:check Check why this store's mail is or is not landing, and answer 1 if anything failed
bin/plugin kahunacart-newsletters export:csv Write the subscriber list as a CSV, with the same filters the admin table takes
bin/plugin kahunacart-newsletters import:csv Import a contact list from a CSV, deduplicating by address and honoring suppressions
bin/plugin kahunacart-newsletters import:customers Put the store's own customers on a list, inviting them or adding them as subscribed
bin/plugin kahunacart-newsletters seed Fill the newsletter with a demo audience, campaigns and automations (idempotent; --purge removes it again)
bin/plugin kahunacart-newsletters segments:count Recount segments now, rather than waiting for the worker
bin/plugin kahunacart-newsletters segments:preview Show how many people a segment is about, who is left out, and ten of them
bin/plugin kahunacart-newsletters webhook:replay Run a saved provider webhook payload through its provider and say what it would do

Warning

reset --customers empties everything the newsletter owns except the suppression list: the addresses this store has been told never to mail again. Remove one on the Suppressions screen instead.

Subscriptions add-on commands

The Subscriptions add-on has one command of its own, for demo stores.

Command What it does
bin/plugin kahunacart-subscriptions seed Fill the store with demo subscriptions, plans and renewal orders (idempotent; --purge removes them again)

Shipping add-on commands

The Shipping add-on's commands are under bin/plugin kahunacart-shipping. Its migrations, jobs and resets go through the base plugin's migrate, work and reset. See Developers.

Command What it does
bin/plugin kahunacart-shipping demo:tick Move every open demo parcel to wherever its clock says it is
bin/plugin kahunacart-shipping seed Fill the store with the demo carrier's parcels — boxes, a ship-from address, two carriers, and two months of orders with tracking on them (idempotent; --purge removes it again)
bin/plugin kahunacart-shipping webhook:register Register this store's inbound tracking webhook with the aggregator (adopts an existing one; --reset replaces it)
bin/plugin kahunacart-shipping webhook:secret Generate and save the tracking webhook's secret — the random part of its address, and what the aggregator's signature is derived from

Bookings add-on commands

The Bookings add-on's commands are under bin/plugin kahunacart-bookings. See Developers.

Command What it does
bin/plugin kahunacart-bookings seed Set a whole business up from nothing — people, hours, products and rules
bin/plugin kahunacart-bookings slots When a booking product can be booked, and with --explain, why a time is not offered

Rentals add-on commands

The Rentals add-on's commands are under bin/plugin kahunacart-rentals. See Developers.

Command What it does
bin/plugin kahunacart-rentals nights Which nights a property can be slept in, and what a stay would cost
bin/plugin kahunacart-rentals seed Set a whole property up from nothing — hours, product, rules, rate card and a few stays

Catalogue export and import keep sale operations’ named money arguments in argument_amounts. The amount argument uses amounts, and fixed quantity-tier amounts stay beside their rungs. See Currencies → In the catalogue file.

seed --purge also checks and removes seeded foreign amounts written before a currency was enabled or after it was disabled. It does this even on a store currently selling only in the base currency.