Skip to content

Powered by Grav CMS + Helios Theme

Orders

The commercial record: reading an order, moving it through its statuses, refunding it, and its notes, emails and downloads.

The commercial record. Statuses, the timeline and the refund rules are on Orders and the admin.

List orders

GET /api/v1/kahunacart/orders Permission kahunacart.orders.view

Every order that got past the cart, newest first, canceled ones included, with its number, email, totals, payment status and fulfillment status. A cart nobody completed is never listed. Money is in minor units, in the order's own currency. On a store that sells in more than one currency never add up the totals of orders without checking their currencies match, and such a store's answer also carries currencies (every currency the store sells in or has orders in, base currency first, each with how many orders it has) and currency (the one the list is narrowed to, or null). A store with one currency gets neither key.

Parameters

Name Type Description
q optional string Substring match on the order number or the email address
currency optional string Only the orders in this currency, a three-letter code such as EUR. Any code is taken, so an order in a currency the store has stopped selling in can still be found. A store with one currency ignores it, and anything that is not a three-letter code is refused with a 422
sort optional string A column to sort the whole list by before it is paged: order, date, email, total, invoice, provider, source, status, payment or fulfillment, with dir for the direction. Or cf:<name> or cf:<name>:desc to order by a custom field declared sortable on the order, orders with no value last. Omit for newest first. A search (q) and a column both apply, with an order the search term names outright kept first; a search wins over a cf: sort
dir optional string The direction of a column sort; a cf: sort carries its own
One of: asc, desc
Default: asc
cf optional object Custom fields to narrow by, name to value, every one required: cf[printful_status]=pending. Only a field declared filterable on this entity; any other name is refused with a 422
product optional integer Only the orders with a line for this product id: who bought it. A deleted product still works. The answer carries `filter` with `product_title`
variant optional integer With product, only the orders with a line for this variant
from optional string Only orders completed on or after this UTC day, YYYY-MM-DD
to optional string Only orders completed on or before this UTC day, YYYY-MM-DD, the whole day included
customer optional integer Only the orders on this customer record. Guest checkouts have none; search q by email for those
status optional string Only orders in this status code. The answer carries `facets`, how many orders each status, payment status and fulfillment status holds over the filtered list
payment optional string Only orders with this payment status code, such as paid, pending or refunded
fulfillment optional string Only orders with this fulfillment status code, such as unfulfilled or digital
channel optional string Only orders that came in this way: web (the storefront), manual (entered by hand), pos (rung up at a till) or agent (a shopping agent)
One of: web, manual, pos, agent
location optional integer Only counter sales made at this pickup location id
refund_request optional string A queue of orders somebody is waiting on, newest request first, composing with nothing else: open (a refund request or return not yet answered), awaiting (returns approved whose goods have not arrived) or received (returns whose goods arrived and are waiting for their refund). Every answer carries refund_requests_open, returns_awaiting and returns_received
One of: open, awaiting, received
page optional integer At least 1.
Default: 1
per_page optional integer At least 1.
Default: 25

Get an order

GET /api/v1/kahunacart/orders/{id} Permission kahunacart.orders.view

One order with its lines, adjustments, payment transactions, its stored history, its notes thread and which of its emails were last queued and sent, plus the invoice and invoice PDF URLs, a pay link while there is still something to pay, and the refunded and pending-refund totals. Money is in minor units of the order's own currency, all of it, except what a product cost the store (cogs_minor on a line), which is typed once in the base currency: on an order in another currency the answer carries cost_currency naming it, and an order in the base currency has no such key. Read the refunded and pending totals before refunding, so the two are not counted twice. history is empty for an order placed before the history table existed, and timeline is the computed fallback for exactly those orders. A transaction the payment provider converted carries settlement (currency, amount_minor, rate, reference, recorded_at): what the provider paid out, in its own currency: rate is the provider's conversion rate (charge amount times rate is the converted amount before the provider's fee) and amount_minor is what reached the account after the fee.

Parameters

Name Type Description
id required integer Order id

Create an order by hand

POST /api/v1/kahunacart/orders Permission kahunacart.orders.manage

Record an order taken somewhere other than the storefront, over the phone, at a stall or by email. Lines are priced through the very engine the storefront uses, so a running sale applies exactly as it would at checkout, and a customer whose email is on file is priced for their customer group (its price list and its sales), and the stock it consumes is real stock: an unpublished or out-of-stock variant is refused with 422. On a store that sells in more than one currency, currency says which one the order is in (the store's own when it is left out) and every product is priced in it. A product with no price in that currency, or a currency the store does not sell in, refuses the whole order with 422 before anything is created. Send billing (and shipping when it differs; left out it is the billing address) and the order is taxed and its postage worked out from them exactly as a checkout's would be, so a store with EU tax zones charges the right VAT. Without an address, a store running tax or shipping zones has nothing for them to match and the order carries no postage; a flat tax rate still applies. A country must be an ISO 3166-1 code (list_countries) and a state one that country has, or the call is refused with 422 naming the address. The order is recorded against the offline provider and arrives awaiting payment, to be settled with mark_order_paid or by the customer through the pay link on the order. A line may carry line_data, which is whatever an add-on wants frozen onto it — the appointment a booking is for, the licence being renewed — and an add-on is free to refuse the line, which refuses the whole order with 422. The order is recorded as channel manual. Send channel: pos with location_id (a pickup location) to ring up a counter sale instead: it is taxed at the location's address, carries no postage, needs no address, and may leave out email for a walk-in customer (then it cannot carry a download or a gift card). The staff member is the signed-in user. Take its payment with add_order_tender.

Parameters

Name Type Description
email optional string The customer's email address, which is their identity. Required, except on a counter sale (channel pos), where leaving it out makes a walk-in sale with no customer
channel optional string manual for an order entered by hand, pos for a sale rung up at a till
One of: manual, pos
Default: manual
location_id optional integer With channel pos, required: the pickup location the sale is made at (list_pickup_locations). Its address is where the sale is taxed
register_ref optional string With channel pos, the till's own reference, kept on the order and printed on the till receipt
name optional string The customer's name
note optional string An internal note recorded on the order
currency optional string The currency the order is in, a three-letter code such as EUR: the store's own or one it sells in. Left out it is the store's own, and every product is priced in it
billing optional object The billing address. An address with no name takes the order's name. Keys: `name`, `company`, `address1`, `address2`, `city`, `region`, `postcode`, `country`, `phone`
shipping optional object The delivery address. Leave it out, or send it empty, when it is the billing address. Keys: `name`, `company`, `address1`, `address2`, `city`, `region`, `postcode`, `country`, `phone`
shipping_method_id optional integer The shipping method to charge. A hint, as the storefront's choice is: when the address's zone does not offer it, the first method the zone offers is charged instead. Leave it out for that first one
save_addresses optional boolean Save the addresses to the customer's address book as their defaults once the order is placed, the way checkout's "save this address" tick does. Needs kahunacart.customers.manage as well; without it the call is refused with 403
Default: false
billing_address_id optional integer When the billing address was picked from the customer's address book, its id, so save_addresses does not file it a second time
shipping_address_id optional integer The same, for the shipping address
tax_exempt optional boolean Charge no tax: for a business in another EU country buying under reverse charge. The order is priced as if the customer were outside every tax zone, so no tax is added and tax a price includes is taken off. Check the VAT number on VIES first; the store does not
Default: false
vat_id optional string The customer's VAT number, printed on the receipt, the invoice and credit notes. Spaces are taken out and it is kept in capitals, e.g. FR12345678901. More than 32 characters, or anything but letters, digits and . + * / -, is refused with 422
items required array of object The lines to bill, by variant. Keys: `variant_id`, `qty`, `line_data`, `parts`

Response

Answers 201 with the order and its line items, recorded against the offline provider as unpaid. An unpublished or out-of-stock variant is a 422 carrying the reason.

List how an order was paid

GET /api/v1/kahunacart/orders/{id}/tenders Permission kahunacart.orders.view

Every payment on an order, oldest first, as a till reads them: type (cash, card, store_credit, gift_card, pay_link or online), the amount, for cash what was handed over (tendered_minor) and the change, any cash rounding, the provider and its reference, who took it (taken_by), the till (register_ref), a card's reader, and what has been refunded against it. Also the order's outstanding_minor (what is still owed), payment_status, channel, location_id, register_ref and staff_user. Money is in minor units of the order's currency.

Parameters

Name Type Description
id required integer Order id

Take a payment at the till

POST /api/v1/kahunacart/orders/{id}/tenders Permission kahunacart.orders.manage

Record one payment on an order that is waiting for money, so a sale can be paid several ways. type: cash takes tendered_minor (what was handed over) and optionally amount_minor (what is being paid; left out, as much of what is owed as was handed over); the answer carries change_minor. Change is only given on the cash that pays the rest, and that payment is rounded to the smallest coin where pos.cash_rounding says so. type: store_credit takes amount_minor (left out, as much as the balance covers) from the order's customer's credit. type: gift_card takes code, redeems the card into the customer's credit and spends from it; a sale with no customer is refused. type: pay_link records nothing and answers the order's signed pay link (pay_url) for what is owed, to show as a QR code. A card on a reader is not a tender: start it with the reader payment tool. A payment for less than what is owed leaves the order partially_paid; the one that covers it marks it paid, and a counter sale's goods are then recorded as handed over. A payment above what is owed is refused with 422, and an order that is paid or cancelled with 409. Send the same idempotency_key when retrying and the first answer comes back with replayed: true and nothing recorded twice.

Parameters

Name Type Description
id required integer Order id
type required string
One of: cash, store_credit, gift_card, pay_link
amount_minor optional integer What is being paid with this tender, in minor units
tendered_minor optional integer Cash only: what the customer handed over, in minor units
code optional string Gift card only: the card's code
register_ref optional string The till taking it; defaults to the order's own
idempotency_key optional string A retried request with the same key answers with the tender already recorded

Take a return at the counter

POST /api/v1/kahunacart/orders/{id}/counter-return Permission kahunacart.orders.refund

The customer brought goods back to the shop: open an in-store return, approve it and receive it in one step, then refund it. Every unit in lines is received and its sound units go back on the shelf; give damaged_qty per line for units that are not fit to sell. The refund is for the lines at what was paid, less the store's return fee, and refund.to says where it goes: original (the card back through its provider, cash out of the drawer, store credit back to credit; a sale paid several ways is refunded card first, then credit, then cash), cash, or store_credit (an order with a customer only). refund.amount_minor overrides the amount and refund.notify emails the customer. Works on any order, online or counter, with no return window: staff decide. A digital line, a final-sale product or more than is left of a line is refused with 422, and an order with a return already open with 409. When the refund fails the goods stay received and the answer says to refund the return from the order.

Parameters

Name Type Description
id required integer Order id
lines required array of object Keys: `item_id`, `qty`, `damaged_qty`
reason optional string
One of: changed_mind, not_working, not_as_described, bought_by_mistake, duplicate, other
Default: other
note optional string A note kept on the return
refund optional object Keys: `to`, `amount_minor`, `notify`

Open a counter cart

POST /api/v1/kahunacart/counter-carts Permission kahunacart.orders.manage

Start a sale at a till as a cart, to be priced as items are added and then completed into a counter-sale order with complete_counter_cart. The cart is sold at a pickup location, which is required and must be switched on: it is taxed at the location's address, with no postage and no collection charge, and needs no address and no customer. Answers 201 with the cart priced: cart_id, open, order, items, adjustments, summary_adjustments, coupon_codes, staff_discounts and totals (items_total_minor, discount_minor, shipping_minor, fee_minor, tax_minor, tax_included_minor, total_minor). Every counter cart call answers the same. 422 for a missing or switched-off location or a currency the store does not sell in.

Parameters

Name Type Description
location_id required integer The pickup location the sale is made at
register_ref optional string The till's own name for itself, printed on the receipt
currency optional string A currency the store sells in; left out, the store's own

Read a counter cart

GET /api/v1/kahunacart/counter-carts/{id} Permission kahunacart.orders.manage

A counter cart priced, or the counter-sale order it became once it was completed (open is then false). 404 for an id that is not a counter cart or a counter sale.

Parameters

Name Type Description
id required integer Counter cart id, from open_counter_cart

Throw a counter cart away

DELETE /api/v1/kahunacart/counter-carts/{id} Permission kahunacart.orders.manage

Void a sale that was never charged, or a parked sale nobody came back for: the cart, its lines and its adjustments are deleted. A cart holds no stock, so nothing is put back. 204. 409 for a sale already completed or one with a payment on it.

Parameters

Name Type Description
id required integer Counter cart id, from open_counter_cart

Add a line to a counter cart

POST /api/v1/kahunacart/counter-carts/{id}/lines Permission kahunacart.orders.manage

Add a variant to the sale, as a buy button would: the price, the title and the stock check are the catalog's, and a variant already on the cart with the same line data gains the quantity. 422 with the catalog's sentence for a variant that is out of stock or not for sale; 409 for a sale already completed.

Parameters

Name Type Description
id required integer Counter cart id, from open_counter_cart
variant_id required integer Variant id
qty optional integer At least 1.
Default: 1
line_data optional object Whatever an add-on wants frozen onto the line, validated as a buy button's fields are
parts optional object For a grouped product or a kit whose parts the shopper picks: kit item id => the chosen variant id, one for each picking part. A missing pick, or one that is not a variant of that part, is a 422

Change a counter cart line's quantity

PATCH /api/v1/kahunacart/counter-carts/{id}/lines/{item} Permission kahunacart.orders.manage

Set a line's quantity; 0 takes the line off, with its staff discount. Answers the cart priced.

Parameters

Name Type Description
id required integer Counter cart id, from open_counter_cart
item required integer The line id, from the cart's items
qty required integer At least 0.

Take a line off a counter cart

DELETE /api/v1/kahunacart/counter-carts/{id}/lines/{item} Permission kahunacart.orders.manage

Take a line off the sale, with its staff discount. Answers the cart priced; 404 for a line that is not on it.

Parameters

Name Type Description
id required integer Counter cart id, from open_counter_cart
item required integer The line id, from the cart's items

Put a discount code on a counter cart

POST /api/v1/kahunacart/counter-carts/{id}/coupons Permission kahunacart.orders.manage

Apply a discount code to the sale through the cart's own coupon path, so it is worked out before tax exactly as at an online checkout. A code the sale does not qualify for is 422 with the reason and leaves the sale as it was; a code that stops another one applying takes that one off.

Parameters

Name Type Description
id required integer Counter cart id, from open_counter_cart
code required string

Take a discount code off a counter cart

DELETE /api/v1/kahunacart/counter-carts/{id}/coupons/{code} Permission kahunacart.orders.manage

Take a discount code off the sale. A code that is not on it changes nothing. Answers the cart priced.

Parameters

Name Type Description
id required integer Counter cart id, from open_counter_cart
code required string

Give a staff discount on a counter cart

POST /api/v1/kahunacart/counter-carts/{id}/discount Permission kahunacart.orders.manage

A discount given by hand at the till, on one line (item_id) or on the whole sale (no item_id). kind percent takes value in basis points (1000 is 10%, up to 10000); kind amount takes value in minor units. The reason is required and is the discount's label on the receipt. One per line and one on the sale: giving another replaces it. It is taken off before tax, where a coupon is, never takes a line or the sale below zero, and a sale's discount is spread over the lines by what each is worth. The signed-in user is recorded as who gave it.

Parameters

Name Type Description
id required integer Counter cart id, from open_counter_cart
item_id optional integer The line it comes off; left out, the whole sale
kind required string
One of: percent, amount
value required integer Basis points for percent, minor units for amount
reason required string Why, printed on the receipt

Take the sale's staff discount off

DELETE /api/v1/kahunacart/counter-carts/{id}/discount Permission kahunacart.orders.manage

Take the staff discount on the whole sale off. A line's own discount stays. Answers the cart priced.

Parameters

Name Type Description
id required integer Counter cart id, from open_counter_cart

Take a line's staff discount off

DELETE /api/v1/kahunacart/counter-carts/{id}/lines/{item}/discount Permission kahunacart.orders.manage

Take the staff discount on one line off. Answers the cart priced.

Parameters

Name Type Description
id required integer Counter cart id, from open_counter_cart
item required integer The line id, from the cart's items

Put a customer on a counter cart

POST /api/v1/kahunacart/counter-carts/{id}/customer Permission kahunacart.orders.manage

Attach a customer to the sale, which is repriced at once for their customer group's price list and group sales, or send customer_id null to make it a walk-in sale again. Find or create the customer first with the customer tools. 404 for a customer that does not exist.

Parameters

Name Type Description
id required integer Counter cart id, from open_counter_cart
customer_id required integer or null Customer id, or null for a walk-in sale

Complete a counter cart into an order

POST /api/v1/kahunacart/counter-carts/{id}/complete Permission kahunacart.orders.manage

Turn the sale into a counter-sale order, payment pending, exactly as create_order with channel pos would have built it for the same lines, customer and location; the signed-in user is its staff_user. Take the payment afterwards with add_order_tender or start_reader_payment on the order (its id is the cart's). 201 with the order priced; completing a sale already completed answers it again with replayed true and 200. 422 for an empty sale, or a walk-in sale carrying a download or a gift card (put a customer on it first).

Parameters

Name Type Description
id required integer Counter cart id, from open_counter_cart

Refund an order

POST /api/v1/kahunacart/orders/{id}/refund Permission kahunacart.orders.refund

Refund an order through the provider that took the payment, either by naming the lines that came back or as a bare amount. Name items and the refund is priced from those lines at what the customer actually paid for them — the frozen line total with its share of any order-level discount and tax allocated onto it — and the refund then also restocks, revokes a digital line's downloads once that line is refunded in full, and reaches add-ons through onKahunaCartRefundApplied. Send amount alone, or nothing at all, and it is an unallocated refund: money moves, nothing is said about the goods, and nothing is given back until the order is refunded in full. shipping_minor refunds postage, which has no line to sit on. amount_minor overrides the computed total without changing what the refund is recorded as being for. Answers status confirmed when the money is on its way back and pending when the provider took the request and has not settled it, along with the amount charged, the refunded and pending totals in minor units, and the quantity of each line refunded so far. A provider that cannot refund answers 422, as does one that cannot refund partially when asked for individual lines, a line that is not on the order, and a quantity beyond what is left of one. The answer also carries settled_minor, what the provider actually sent back, and a message when that is less than what was asked for — a provider that had already refunded part of this payment in its own dashboard will only return what is left. Answers 409 when another refund on the same order is still running. Every amount, typed or sent, is in the order's own currency (a yen order takes whole yen), and the provider is asked for the refund in that currency.

Parameters

Name Type Description
id required integer Order id
items optional array of object The lines that came back. Omit for an unallocated refund. Line ids and their remaining refundable quantities are on `refund_lines` in get_order. Keys: `item_id`, `quantity`
shipping_minor optional integer Postage to refund, in minor units, capped at what the order has left to give back (`refundable_shipping_minor` on get_order)
amount_minor optional integer Override the total in minor units — a goodwill credit, or a price adjustment on a line that is not coming back. The lines still record what the refund was about.
amount optional string The same override as a decimal in the order's own currency, for example "12.50" in euro or "1250" in yen. Omit both, with no `items`, to refund the whole remaining balance.
reason optional string Recorded on the refund transaction for your own records
restock optional boolean Put the returned units back on the shelf, for stock-managed variants. Ignored for an unallocated refund, which names no units.
Default: false
notify optional boolean Whether to email the customer once the refund confirms. False moves the money without sending anything, and the history entry records that the silence was deliberate.
Default: true
against_reference optional string Which payment to refund, by the provider reference on it. Only means anything on an order that was paid more than once; the newest payment is used when this is omitted.
return_id optional integer The received return this refund is for, from `suggested_refund`. With it, a refund naming no lines, no postage and no amount is refused (422) rather than refunding the whole order; a return that is not on the order answers 404, one not yet received 409.

Response

Answers {status, refunded_minor, pending_refund_minor}, where status is confirmed (the money is on its way back) or pending (the provider took the request and has not settled it). A refused refund is a 422.

Record a refund issued elsewhere

POST /api/v1/kahunacart/orders/{id}/refunds/record Permission kahunacart.orders.refund

Write down a refund that was issued somewhere KahunaCart cannot see — in the provider's own dashboard, by bank transfer, or in cash. No provider is called and no money moves; everything else happens exactly as it does for a refund this store issued, so the order's payment status moves, a full refund runs the reversal, and the credit note is earned. Use this rather than refund_order to record a dashboard refund: refund_order would issue a second real one. reference is required and is whatever you can quote later to find the refund again. The customer is not emailed unless you ask, because they have usually already had the provider's own message about it. Refused with 422 when the amount is more than the order has left to give back, and 409 when another refund on the order is running.

Parameters

Name Type Description
id required integer Order id
reference required string The provider's refund id, a bank reference, or whatever else names this refund outside KahunaCart
amount_minor optional integer How much went back, in minor units
amount optional string The same amount as a decimal in the order's own currency, for example "12.50" in euro or "1250" in yen
reason optional string Recorded on the refund transaction for your own records
notify optional boolean Email the customer about it. Off by default: they have already had the refund, and a second message a week later reads like a second refund.
Default: false
return_id optional integer The received return this refund was for, such as a counter return whose card leg failed and was paid back in cash. A return not on the order answers 404, one not received 409; the refund is linked to it, and the return closes once its refunds cover what it owes.

Read the refund request on an order

GET /api/v1/kahunacart/orders/{id}/refund-request Permission kahunacart.orders.view

What a customer asked for on this order and what came of it: the reason they picked off the store's fixed list, anything they wrote, when they asked, and the decision with the sentence it was declined with. Answers the newest request plus every earlier one, because "they asked twice and we said no the first time" is the context worth having before answering the second. request is null on an order nobody has asked about — a 200 with nothing in it, not a 404. can_request says why the customer cannot ask right now, or null when they can. A return is a request with kind return: it carries return_number, method, the label fields and lines (each item_id, title, sku, qty, and once received received_qty and damaged_qty). active is the request still waiting on somebody, can_return why the customer cannot open a return now, returnable and not_returnable the order's lines either way, suggested_refund the refund to make for a received return (when its actionable is true, send its items as item_id and quantity, restock, shipping_minor, amount_minor and return_id to refund_order; when false nothing is left to refund, so close the return with mark_return_refunded), and label_providers the add-ons that can buy a label.

Parameters

Name Type Description
id required integer Order id

Approve a refund request

POST /api/v1/kahunacart/orders/{id}/refund-request/approve Permission kahunacart.orders.refund

Agree to a customer's refund request: the order is refunded through the provider exactly as refund_order does it, and the request is then marked approved — only if the refund answered, so a request never claims a refund the provider refused. Takes the same body refund_order takes, so omitting everything sends back the whole remaining balance and naming lines or an amount approves less than was asked for, which is a real answer. Answers 409 when there is no open request on the order, and whatever refund_order would have answered when the refund itself fails. A return (kind return) is authorised instead of refunded: nothing moves, the customer is emailed how to send it back, and it takes method, note and a label (label_url, label_pdf, tracking_number, carrier) rather than the refund fields.

Parameters

Name Type Description
id required integer Order id
items optional array of object The lines that came back. Omit to approve the whole remaining balance. Keys: `item_id`, `quantity`
shipping_minor optional integer Postage to refund, in minor units
amount_minor optional integer Approve a different amount, in minor units
amount optional string The same amount as a decimal in the order's own currency, for example "12.50" in euro or "1250" in yen
reason optional string Recorded on the refund transaction for your own records
restock optional boolean Put the returned units back on the shelf, for stock-managed variants
Default: false
notify optional boolean Whether to email the customer once the refund confirms
Default: true
method optional string A return only: how it comes back, honoured where the store lets the customer choose
One of: by_mail, in_store
note optional string A return only: a sentence for the customer, printed in the approval email and on their order page
label_url optional string A return only: a label kept elsewhere, a full https address
label_pdf optional string A return only: the label PDF, base64 encoded, at most 5 MB
tracking_number optional string A return only: the return parcel's tracking number
carrier optional string A return only: who carries it

Decline a refund request

POST /api/v1/kahunacart/orders/{id}/refund-request/decline Permission kahunacart.orders.manage

Say no to a customer's refund request, and tell them why. note is required and is what the customer is sent: it goes out as the ordinary note-to-the-customer email and is saved on the order as a note to them, so it lands in the same thread every other message to them does. No money moves. Answers 409 when there is no open request on the order and 422 when the note is empty. A declined return is sent the return-declined email with the note instead.

Parameters

Name Type Description
id required integer Order id
note required string What the customer is told. Sent to them and kept on the order; trimmed at 2000 characters.

List returns and refund requests

GET /api/v1/kahunacart/returns Permission kahunacart.orders.view

Every refund request and return across the store, newest first, each with its lines and the order's order_number. Narrow by status (open, approved, declined, received, refunded, cancelled), kind (refund or return) or order_id. A return goes open, approved (the customer was told to send it), received (it arrived and the sound units are back in stock), then refunded; or declined, or cancelled.

Parameters

Name Type Description
status optional string
One of: open, approved, declined, received, refunded, cancelled
kind optional string
One of: refund, return
order_id optional integer Only the requests on this order
page optional integer At least 1.
Default: 1
per_page optional integer At least 1.
Default: 25

Receive a return

POST /api/v1/kahunacart/orders/{id}/refund-request/receive Permission kahunacart.orders.refund

Check in the parcel of the order's approved return: per line, how many arrived and how many of those are damaged. A line left out arrived exactly as approved with nothing damaged. The sound units go back in stock, once; damaged ones never do. The customer is emailed what arrived. Answers with the return, restocked, and suggested_refund, the refund to make next with refund_order (no restock: receiving already did it). 409 when there is no approved return on the order, 422 for more units than were approved or more damaged than arrived.

Parameters

Name Type Description
id required integer Order id
lines optional array of object Keys: `item_id`, `received_qty`, `damaged_qty`

Cancel a return

POST /api/v1/kahunacart/orders/{id}/refund-request/cancel Permission kahunacart.orders.refund

Call off the order's open or approved return: you stopped waiting for the parcel, or the customer withdrew it. Nothing moves. A return whose goods arrived is closed by refunding it, or with mark_return_refunded.

Parameters

Name Type Description
id required integer Order id
note optional string Why, kept on the return

Close a return as refunded

POST /api/v1/kahunacart/orders/{id}/refund-request/mark-refunded Permission kahunacart.orders.refund

Close the order's received (or approved) return as refunded without moving any money, for a return you settled some other way. A refund made with refund_order that names the return's lines closes it by itself.

Parameters

Name Type Description
id required integer Order id
transaction_id optional integer The refund transaction it was settled by, when there is one on the order

Attach a return label

POST /api/v1/kahunacart/orders/{id}/refund-request/label Permission kahunacart.orders.refund

Give the order's open or approved return a label: a PDF (base64), a URL, a tracking number and a carrier. Absent keys are kept and empty ones cleared. The customer downloads it from their order page; notify sends the approval email again so they get it now.

Parameters

Name Type Description
id required integer Order id
label_url optional string A full https address of the label
label_pdf optional string The label PDF, base64 encoded, at most 5 MB
tracking_number optional string
carrier optional string
notify optional boolean Email the customer the approval again, with the label
Default: false

Buy a return label

POST /api/v1/kahunacart/orders/{id}/refund-request/label/buy Permission kahunacart.orders.refund

Buy a label for the order's open or approved return from an add-on that sells them (get_refund_request lists them as label_providers) and attach it. KahunaCart itself buys no labels; with no such add-on this answers 422.

Parameters

Name Type Description
id required integer Order id
provider required string The id of a label provider
notify optional boolean Email the customer the approval again, with the label
Default: false

Mark an order paid

POST /api/v1/kahunacart/orders/{id}/mark-paid Permission kahunacart.orders.manage

Settle an order whose payment arrived outside KahunaCart, a bank transfer most often. Answers 409 when the payment is not pending and 404 for an order that was never completed. Settling releases whatever completion gated on payment, including download grants.

Parameters

Name Type Description
id required integer Order id
notify optional boolean Whether to send the payment-received email. False settles the order quietly, for money confirmed weeks ago.
Default: true

Ship everything still owed

POST /api/v1/kahunacart/orders/{id}/fulfill Permission kahunacart.orders.manage

Record one shipment covering every unit on the order that has not gone out yet, with no carrier named, and queue the shipped email to the customer. This is the one-click action: use create_shipment when you know which lines went into the box or have a tracking number. Answers 409 for an order that is not completed, one with nothing left to ship, and a digital-only order, which has nothing to ship and must not be told it does.

Parameters

Name Type Description
id required integer Order id
notify optional boolean Whether to email the customer. False marks the order shipped without sending anything, for a correction rather than news, and the history entry records that the silence was deliberate.
Default: true

List an order's shipments

GET /api/v1/kahunacart/orders/{id}/shipments Permission kahunacart.orders.view

Every parcel recorded against an order, oldest first, each with its carrier, tracking number, the tracking link the customer sees and the lines it carried. Also answers lines — every order line with how many units were bought, how many have shipped and how many remain, which is what a shipment must be built from — and carriers, the codes and tracking-URL templates this store offers. Digital lines are never shippable and always report a remainder of zero.

Parameters

Name Type Description
id required integer Order id

Record a shipment

POST /api/v1/kahunacart/orders/{id}/shipments Permission kahunacart.orders.manage

Record that these quantities of these lines went out, optionally with a carrier and a tracking number. Read list_shipments first: a quantity above a line's remainder is refused with 422 and nothing is written, and a digital line can never be shipped. The order's fulfillment status is recomputed from every shipment on it — partial while units are still owed, fulfilled once none are — and the shipped email is queued for this parcel unless notify is false. Leave tracking_url empty and it is derived from the carrier's template where there is one. There is no way to delete or void a shipment.

Parameters

Name Type Description
id required integer Order id
items required array of object What is in the parcel, by order line. Keys: `item_id`, `quantity`
carrier optional string Carrier code from list_shipments, for example ups, royal-mail or dhl-express; "other" for anything not listed
tracking_number optional string The carrier's tracking number
tracking_url optional string Where the tracking number leads; omit to derive it from the carrier's template
note optional string An internal note about this parcel; never shown to the customer
notify optional boolean Queue the shipped email for this parcel; false records it silently
Default: true

Cancel an order

POST /api/v1/kahunacart/orders/{id}/cancel Permission kahunacart.orders.manage

Call off an unpaid sale and give back everything completion took: stock, the coupon redemption and any live download links. Only ever offered for an order whose payment never landed, pending or failed; a paid order answers 409 and is refunded instead, which is what writes the ledger.

Parameters

Name Type Description
id required integer Order id
notify optional boolean Whether to tell anybody. False cancels quietly, sending no merchant notification, and the history entry records that the silence was deliberate.
Default: true

Resend the order confirmation

POST /api/v1/kahunacart/orders/{id}/resend-confirmation Permission kahunacart.orders.manage

Queue the order confirmation email again. It re-renders the same message against the order as it stands now, so an order corrected since it was placed sends the corrected copy. Nothing is sent inline: the queue owns delivery, so a mail server refusing connections becomes a failed job rather than a hung request. An order with no email address answers 422.

Parameters

Name Type Description
id required integer Order id

An order's history

GET /api/v1/kahunacart/orders/{id}/history Permission kahunacart.orders.view

Everything recorded against an order, oldest first: when it was placed, every payment and refund at each stage of its lifecycle, when it shipped, when it was cancelled, every email queued and every email sent, and every note written. Each entry carries a dotted type, who did it (system, admin, customer or plugin) with a frozen display name, an optional message and a data object holding whatever is specific to that entry — amounts, the email kind, a quiet flag on a change that notified nobody on purpose. An order placed before the history table existed answers with an empty history and a computed timeline beside it instead; that fallback is assembled from the order row and the ledger and cannot say when an order shipped or was cancelled, because nothing recorded it at the time.

Parameters

Name Type Description
id required integer Order id

An order's notices

GET /api/v1/kahunacart/orders/{id}/notices Permission kahunacart.orders.view

What still has to be said about one order, in two lists. admin is the merchant's queue for this order — negative stock behind a line, a coupon redeemed past its cap, an outbound webhook that gave up — and every item on it is either unresolved or carries who resolved it and when. customer is what the buyer was told on their receipt and in their confirmation email, read-only here. This is not the order history: a history row is a log entry and is never resolved, and a notice exists so that somebody looks at the order.

Parameters

Name Type Description
id required integer Order id

Resolve an order notice

POST /api/v1/kahunacart/orders/{id}/notices/{noticeId}/resolve Permission kahunacart.orders.manage

Mark one queue item on an order dealt with, recording who did it and when. Idempotent, and it never overwrites an existing resolution, so a second press does not rewrite who dealt with it. Answers 422 for a customer notice — a customer dismisses their own, and there is nothing here for a merchant to resolve on their behalf.

Parameters

Name Type Description
id required integer Order id
noticeId required integer Notice id, from order_notices_list

Reopen a resolved order notice

POST /api/v1/kahunacart/orders/{id}/notices/{noticeId}/reopen Permission kahunacart.orders.manage

Put a resolved queue item back, for the case where somebody pressed the button on the wrong row. The resolution and who made it are cleared.

Parameters

Name Type Description
id required integer Order id
noticeId required integer Notice id, from order_notices_list

The notice queue

GET /api/v1/kahunacart/notices Permission kahunacart.orders.view

Orders that need a human, across the whole store, unresolved first and newest first within each half. Each row carries its order number and the buyer's address beside the notice, the type it is, how loud it is, and whether somebody has already dealt with it. counts is what is outstanding per type whatever filter is applied, which is the figure to read when deciding where to start. Answer this before working through orders one at a time: it is the list of the ones that are not fine.

Parameters

Name Type Description
resolved optional boolean Include items already dealt with; omit for the queue itself
Default: false
type optional string Narrow to one notice type, e.g. stock_negative or webhook_failed
sort optional string A column to sort the whole list by before it is paged; leave out for the usual order
One of: order, type, severity, what, raised, resolved
dir optional string
One of: asc, desc
Default: asc
q optional string Words to search for in the order number and email, the setting a notice is about, the message key, the severity and who resolved it; every word has to be there, in any order. Applied before the sort and the page, and total counts what it found
page optional integer At least 1.
Default: 1
per_page optional integer At least 1.
Default: 50

An order's notes

GET /api/v1/kahunacart/orders/{id}/notes Permission kahunacart.orders.view

The notes thread on an order — internal notes and notes written to the customer in one list, oldest first, each with its author and time. An order entered by hand before this release carries a single note in its data blob, and it appears here as the thread's first internal note with a null id, because it is part of the order row and cannot be deleted.

Parameters

Name Type Description
id required integer Order id

Write a note on an order

POST /api/v1/kahunacart/orders/{id}/notes Permission kahunacart.orders.manage

File a note against an order. With to_customer false — the default — it is internal and never leaves the admin. With to_customer true it is also emailed to the customer straight away and shown on their receipt and in their account, which is the ordinary way to tell somebody their parcel is delayed. The email is queued rather than sent inline, so this answers that it is on its way rather than that it arrived. An order with no email address refuses to_customer with 422 rather than filing a note claiming to have been sent to nobody. A note to the customer cannot be deleted afterwards.

Parameters

Name Type Description
id required integer Order id
body required string The note, as plain text
to_customer optional boolean Email this note to the customer and show it on their receipt
Default: false

Delete an internal note

DELETE /api/v1/kahunacart/orders/{id}/notes/{entryId} Permission kahunacart.orders.manage

Remove an internal note from an order. Internal notes only: a note to the customer has been in their inbox since it was written, and deleting the store's copy would hide it from the merchant without recalling it from anybody, so asking answers 409. An entry that is not a note answers 409 too, and one that belongs to another order answers 404.

Parameters

Name Type Description
id required integer Order id
entryId required integer History entry id of the note, from list_order_notes

Send an order email again

POST /api/v1/kahunacart/orders/{id}/emails/{kind}/resend Permission kahunacart.orders.manage

Queue one of an order's emails again — confirmation, payment-received, shipped, refund, pay-link, customer-note or download-added. The message is re-rendered against the order as it stands now, and re-queued with the payload the last message of that kind carried, so a refund email resends for the amount it was about and a note resends with its note. Those two cannot be composed from the order row alone, so resending either before one was ever sent answers 422. A download-added resend repeats the files the last one was about, or lists all the order's files when none was sent. An order with no email address answers 422, and an unknown kind answers 404. Nothing is sent inline: the queue owns delivery.

Parameters

Name Type Description
id required integer Order id
kind required string Which message to send again
One of: confirmation, payment-received, shipped, refund, pay-link, customer-note, download-added
POST /api/v1/kahunacart/orders/{id}/pay-link Permission kahunacart.orders.manage

Mint a new signed pay link for an order and move its deadline forward by orders.pay_link_days. Use it when a customer says the link in their email no longer works: order links are signed and expire, and this is the only way to give somebody a working one back. Pass send: true to queue the "Your order is ready to pay" email to the address on the order as well as answering with the URL. Answers 409 for an order that cannot be paid at all — already settled, canceled, or never completed — because a link to one would only ever say so, and 422 when send is asked for on an order with no email address.

Parameters

Name Type Description
id required integer Order id
send optional boolean Also email the link to the address on the order
Default: false

List an order's download grants

GET /api/v1/kahunacart/orders/{id}/downloads Permission kahunacart.orders.view

Every download grant issued against an order, with the file it points at, how many times it has been used, when it expires and whether it has been revoked.

Parameters

Name Type Description
id required integer Order id

Revoke a download grant

POST /api/v1/kahunacart/orders/{id}/downloads/{grantId}/revoke Permission kahunacart.orders.manage

Kill one download grant, which is the answer to a chargeback or a link that turned up where it should not have. The row stays, because who was given what and when it was taken away is the history worth keeping, and there is no un-revoke: restoring access means granting again.

Parameters

Name Type Description
id required integer Order id
grantId required integer Download grant id

Attach a file to an order

POST /api/v1/kahunacart/orders/{id}/downloads Permission kahunacart.orders.manage

Multipart: file (required), title, description, name (the file name the browser saves), and notify=1 to email the customer the new file with message above it. The file belongs to this order only and is granted with the store's default download limit and expiry; Revoke takes it back. Answers 201 with the grant and whether the email was queued. Refused before anything is kept on an order with no email address, and with the same checks a product file gets. No tool: it takes a file upload.

Get an order packing slip

GET /api/v1/kahunacart/orders/{id}/packing-slip.pdf

Get a shipment packing slip

GET /api/v1/kahunacart/orders/{id}/shipments/{sid}/packing-slip.pdf

Download a return label

GET /api/v1/kahunacart/orders/{id}/refund-request/label.pdf Permission kahunacart.orders.view

The PDF label uploaded for, or bought for, the return on this order, as a download. Answers 404 when the return has no uploaded label. No tool: it answers a file, not JSON. The customer gets the same file from their receipt at {base}/order/{hash}/return-label.

mark-paid, fulfill and cancel each answer 409 when the order is in the wrong state, with a sentence saying which: payment not pending, a digital-only order with nothing to ship, an order already fulfilled or already canceled, or a paid order that must be refunded rather than canceled.

Revoking a download grant leaves the row in place. Who was given what, and when it was taken away, is the history worth keeping. There is no un-revoke; reinstating access means granting again. See Digital products and downloads.