Skip to content

Powered by Grav CMS + Helios Theme

Price lists, bulk edits and imports

Price lists and quantity tiers, the bulk product editor, and the CSV import pipeline.

The price-list model, its rules and the per-variant quantity ladders, together with the two ways a merchant changes many products at once: the bulk editor and the CSV importer. Nothing on the storefront or in the checkout reads a price list in this release. See Price lists and quantity tiers for exactly what does and does not take effect, and Importing for the whole import procedure.

List price lists

GET /api/v1/kahunacart/price-lists Permission kahunacart.sales.manage

Every price list with its header and its rules. A price list is a named set of prices that applies to the shoppers its rules name — a trade list, a wholesale list, a members' list — which is what makes it a different thing from a sale: a sale is a discount every shopper gets and the catalog advertises, a price list is a price. kind is override (what this variant costs for this shopper) or sale (a cut, which is barred from supplying the strikethrough reference price). live says whether the list is active and inside its window right now. Price rows are not listed here; ask get_price_list for one list's.

Get a price list

GET /api/v1/kahunacart/price-lists/{id} Permission kahunacart.sales.manage

One price list with its rules in order and every price row it holds, variant by variant and rung by rung. Amounts come back both as amount_minor and as a decimal amount.

Parameters

Name Type Description
id required integer Price list id

Create a price list

POST /api/v1/kahunacart/price-lists Permission kahunacart.sales.manage

Create a named set of prices. name is required and is never shown to a shopper. Rules decide who the list applies to and are joined with AND, so a list with no rules applies to everyone; each rule is an attribute from list_rule_attributes, an operator that attribute offers, and a value. An attribute the vocabulary does not publish is refused, naming the key. Prices are the rows the list charges: one per variant per quantity range, with min_quantity starting the rung and max_quantity ending it or omitted for an open-ended top rung. A rung is a price, not a discount. Rungs for one variant may not overlap. A row names its variant by variant_id or by sku in its place, never both; a SKU no variant carries, or one two variants share, is refused with 422 naming it. On a store that sells in more than one currency, currency makes the list in that currency, and every price on it is in it.

Parameters

Name Type Description
name required string The merchant-facing name of the list
currency optional string On a store that sells in more than one currency, the currency this list's prices are in (the store's base currency when left out). Fixed once the list is made: a price list is only used for a cart in its own currency, so for trade prices in dollars and in euro make two lists. A currency the store does not sell in is refused. Amounts are read with that currency's own decimal places, so yen takes whole numbers
kind optional string
One of: override, sale
Default: override
priority optional integer Higher wins when two lists match a variant equally well
starts_at optional integer or null UTC epoch seconds, inclusive
expires_at optional integer or null UTC epoch seconds, exclusive
active optional boolean
Default: true
show_in_catalog optional boolean Show this list's price in the catalog as a discount off the catalog price — struck through with a badge, like a sale — for a shopper the list applies to. Off for a trade list whose price should read as the price.
Default: false
catalog_label optional string The badge beside that price, e.g. "Founders price". Empty uses the list name.
rules optional array of object Joined with AND; an empty list applies the price list to everyone. Keys: `attribute`, `operator`, `value`
prices optional array of object Keys: `variant_id`, `sku`, `min_quantity`, `max_quantity`, `amount`, `amount_minor`

Update a price list

PATCH /api/v1/kahunacart/price-lists/{id} Permission kahunacart.sales.manage

Change a price list's header, its rules, its prices, or any combination. Sending rules replaces the whole set, empty list included, which is how the last rule is removed; omitting it leaves the rules standing. prices follows the same three-state contract, so switching a list off is one field and not a round trip through every price. A price row names its variant by variant_id or by sku, as create_price_list describes. A list's currency cannot be changed once it is made: its prices are in it, and a currency naming a different one is refused with 422.

Parameters

Name Type Description
id required integer Price list id
name optional string
currency optional string Only on a store that sells in more than one currency. Naming the list's own currency changes nothing; a different one is refused, because a list stays in the currency it was made in
kind optional string
One of: override, sale
priority optional integer
starts_at optional integer or null
expires_at optional integer or null
active optional boolean
show_in_catalog optional boolean
catalog_label optional string
rules optional array of object Replaces the whole set when present; an empty list removes every rule. Keys: `attribute`, `operator`, `value`
prices optional array of object Replaces every price row when present; an empty list removes them all. Keys: `variant_id`, `sku`, `min_quantity`, `max_quantity`, `amount`, `amount_minor`

Delete a price list

DELETE /api/v1/kahunacart/price-lists/{id} Permission kahunacart.sales.manage

Delete a price list, its rules and every price row it holds. Prices go back to what the variant says the moment it is gone.

Parameters

Name Type Description
id required integer Price list id

List a variant's quantity tiers

GET /api/v1/kahunacart/price-tiers Permission kahunacart.sales.manage

One variant's own quantity ladder — the "buy ten and save" breaks that belong to no price list and apply to every shopper. Each rung is a unit price at a quantity range, lowest rung first. A price list's rows beat these; these beat the variant's own price. Name the variant by variant_id or by sku. On a store that sells in more than one currency each currency has its own ladder, and currency says which one (the base currency when left out).

Parameters

Name Type Description
variant_id optional integer Variant id; send this or sku
sku optional string The variant's SKU, in place of variant_id
currency optional string The currency of the ladder to read, on a store that sells in more than one. Left out, the base currency. A currency the store does not sell in is refused

Set a variant's quantity tiers

PUT /api/v1/kahunacart/price-tiers Permission kahunacart.sales.manage

Replace one variant's quantity ladder. An empty tiers list removes it. Rungs may not overlap and a rung starts at 1 or more; leave the top rung's max_quantity out so it covers every larger order. A gap between rungs is allowed and means a quantity in the gap is priced by the variant's own price. Name the variant by variant_id or by sku. On a store that sells in more than one currency this replaces the ladder in one currency, named by currency (the base currency when left out), and leaves the ladders in the others standing. A variant with no ladder in a currency has no quantity breaks in it, and nothing is worked out from another currency's ladder.

Parameters

Name Type Description
variant_id optional integer Variant id; send this or sku
sku optional string The variant's SKU, in place of variant_id
currency optional string The currency of the ladder being replaced, on a store that sells in more than one. Left out, the base currency. Amounts are read with that currency's decimal places and a number with more decimals than it has is refused, not rounded
tiers required array of object Keys: `min_quantity`, `max_quantity`, `amount`, `amount_minor`

Preview a bulk edit

POST /api/v1/kahunacart/products/bulk/preview Permission kahunacart.products.manage

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. Writes nothing at all. Same body as bulk_update_products.

Parameters

Name Type Description
product_ids required array of integer The products to change.
operations required array of object What to change. Each entry names a field (price, compare_price, stock_quantity, status, visibility, featured, min_qty, max_qty, qty_step, sold_individually, exclude_order_rules, exclude_category_rules), a mode (set, increase, decrease), a unit (fixed or percent — numeric fields only, and percent cannot be used with set) and a value. Money is a decimal string in the store currency; a percentage is a decimal string such as "10" or "12.5". A quantity limit can only be set, and 0 removes it. Two instructions may not name the same field. On a store that sells in more than one currency, currency_price sets a price in one of them and currency_fill works it out from each variant's base price (see `currency`, `target`, `rate` and `rounding` below); two instructions may not write the same price in the same currency. Keys: `field`, `mode`, `unit`, `value`, `currency`, `target`, `rate`, `rounding`

Bulk edit products

POST /api/v1/kahunacart/products/bulk Permission kahunacart.products.manage

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. Writes one history entry per product recording what changed from what to what. Stock changes go through the stock service so they fire the usual events. Refused past admin.bulk.max_rows rather than truncated. Preview it with preview_bulk_update first.

Parameters

Name Type Description
product_ids required array of integer The products to change.
operations required array of object What to change. Each entry names a field (price, compare_price, stock_quantity, status, visibility, featured, min_qty, max_qty, qty_step, sold_individually, exclude_order_rules, exclude_category_rules), a mode (set, increase, decrease), a unit (fixed or percent — numeric fields only, and percent cannot be used with set) and a value. Money is a decimal string in the store currency; a percentage is a decimal string such as "10" or "12.5". A quantity limit can only be set, and 0 removes it. Two instructions may not name the same field. On a store that sells in more than one currency, currency_price sets a price in one of them and currency_fill works it out from each variant's base price (see `currency`, `target`, `rate` and `rounding` below); two instructions may not write the same price in the same currency. Keys: `field`, `mode`, `unit`, `value`, `currency`, `target`, `rate`, `rounding`

Read a product's history

GET /api/v1/kahunacart/products/{id}/history Permission kahunacart.products.manage

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. Useful for answering "why is this price different".

Parameters

Name Type Description
id required integer The product to read.
per_page optional integer How many entries to return (default 50).

List importable fields

GET /api/v1/kahunacart/import/fields Permission kahunacart.products.manage

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. Fields contributed by add-on plugins are included. Call this before writing a mapping. In an orders file the currency field says which currency each order is in (the store's own when the cell is empty, or any currency the store sells in), and that row's amounts are read in it, so 1500 in a yen row is 1500 yen. A row in a currency the store does not sell in fails on its own.

List saved import mappings

GET /api/v1/kahunacart/import/mappings Permission kahunacart.products.manage

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. The same mappings the admin importer and bin/plugin kahunacart import --mapping= both use.

Parameters

Name Type Description
entity optional string Narrow to one entity.
One of: categories, products, variants, customers, orders

Save an import mapping

POST /api/v1/kahunacart/import/mappings Permission kahunacart.products.manage

Save a reusable column mapping so the same export can be imported again next month without describing the file twice. columns maps each column heading in the file to one field name from list_import_fields; a heading you leave out or map to an empty string is ignored. The match key must be one of the columns, and every required field for the entity must be mapped.

Parameters

Name Type Description
name required string What to call it. Unique across the store.
entity required string
One of: categories, products, variants, customers, orders
match_key required string The field rows are looked up by, which is what makes the import idempotent. Products accept sku, slug or id; categories slug or id; variants sku or id; customers email or id; orders number or id.
on_existing optional string What to do with a row whose match key is already in the store.
One of: update, skip
Default: update
columns required object Column heading in the file to field name, for example {"Regular price": "compare_price", "SKU": "sku"}. On a store that sells in more than one currency, products and variants files may also map columns to price_EUR and compare_price_EUR (one pair per currency the store sells in); an empty cell leaves that amount alone and the cell "remove" takes it away.

Update an import mapping

PATCH /api/v1/kahunacart/import/mappings/{id} Permission kahunacart.products.manage

Change a saved mapping. Anything you leave out keeps its stored value, so renaming one does not mean posting its whole column map back.

Parameters

Name Type Description
id required integer
name optional string
entity optional string
One of: categories, products, variants, customers, orders
match_key optional string
on_existing optional string
One of: update, skip
columns optional object Column heading in the file to field name.

Delete an import mapping

DELETE /api/v1/kahunacart/import/mappings/{id} Permission kahunacart.products.manage

Delete a saved column mapping. Import runs that used it keep their own copy of the columns, so nothing already run or running is affected.

Parameters

Name Type Description
id required integer

Rehearse an import

POST /api/v1/kahunacart/import/dry-run Permission kahunacart.products.manage

Rehearse an import of an already-uploaded file: the same writers, the same values, inside a transaction that is always rolled back. Answers what would be created, updated, skipped and failed, with each failure carrying the line number of the merchant's own file. Writes nothing at all — no rows, no run record. Give upload (the token a file upload answered with) and either mapping_id or an inline mapping. An orders row keeps its own currency, and a currency the store does not sell in is reported as that row's failure.

Parameters

Name Type Description
upload required string The upload token, from the admin's file upload.
mapping_id optional integer A saved mapping to use.
mapping optional object An inline mapping instead of a saved one. Keys: `entity`, `match_key`, `on_existing`, `columns`
limit optional integer Rehearse only the first N rows.

Start an import

POST /api/v1/kahunacart/import/runs Permission kahunacart.products.manage

Queue an import of an already-uploaded file. The work happens in the background job queue a batch at a time, so read its progress with get_import_run. Nothing is ever deleted and the run is idempotent on the mapping's match key, so running the same file twice does what running it once does. Importing customers or orders needs the orders permission as well.

Parameters

Name Type Description
upload required string The upload token, from the admin's file upload.
mapping_id optional integer A saved mapping to use.
mapping optional object An inline mapping instead of a saved one. Keys: `entity`, `match_key`, `on_existing`, `columns`
filename optional string The merchant's own name for the file, for the runs list.

List import runs

GET /api/v1/kahunacart/import/runs Permission kahunacart.products.manage

List recent import runs, newest first, with their entity, status, progress and counts of rows created, updated, skipped and failed.

Parameters

Name Type Description
per_page optional integer How many runs to return (default 25).

Read an import run

GET /api/v1/kahunacart/import/runs/{id} Permission kahunacart.products.manage

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. The full error report is downloadable as a CSV from the admin.

Parameters

Name Type Description
id required integer The run to read.

Upload an import CSV

POST /api/v1/kahunacart/import/uploads Permission kahunacart.products.manage

Store a CSV and answer with its header row and a guess at each column

Download an import error report

GET /api/v1/kahunacart/import/runs/{id}/errors.csv Permission kahunacart.products.manage

The error report, as a CSV to correct and import again

Saving a price list queues every product for reindexing, because its rules are evaluated against a cart rather than matched by a query. Replacing a variant's tier ladder queues that variant's product alone.

preview answers with what a bulk edit would change and writes nothing, which is the call to make first. admin.bulk.max_rows caps how many products one request may touch.

A dry run of an import writes nothing. A run that stops can be continued rather than restarted, and the errors route answers a CSV carrying your own file's line numbers.