The cart, coupons and referral codes, placing an order, paying it online through Pesapal, and pre-orders for what the shop does not stock. A guest's cart lives on the website, which sends its lines here to be priced and checked against stock; a signed-in customer's cart is kept on the server, and cart/sync replaces it with the website's cart when they sign in. Everything is priced at the shop's website branch, a product's sale taken off, with no VAT or discount at the cart: checkout works those out. Orders are placed at the website branch and wait for staff to confirm them in the back office before their stock is taken.
Every call sends the shop's key in the X-Storefront-Key header and asks for JSON. Calls marked with a customer token also send the signed-in customer's token. The integration guide explains keys, answers, errors and limits.
Cart
POST
/api/v1/cart/totals
Scope
checkout
Customer token
none
Prices a guest's cart at the website branch: each variant's branch price, or its own price, less its product's sale. No VAT and no discount are added at the cart. A line whose product or variant the website does not show is left out without an error. lines gives each line's price now, its total and what the branch holds, for a cart that shows current prices and warns of short stock. The answer has no data wrapper.
Parameters
itemsarray · body · required
The cart's lines, up to 100. May be empty.
items.*.product_idstring · body · required
The product's id.
items.*.variant_idstring · body
The variant's id. Without it, the product's first variant at the website branch is priced.
items.*.quantityinteger · body · required
1 to 99.
shipping_methodstring · body
A delivery choice. With it, the answer carries delivery_fee, the fee checkout will add, and the total includes it.
Stock is not checked here; call cart/stock-check before checkout. Without shipping_method there is no delivery_fee.
Checks whether the website branch holds what the cart asks for. When it does, every line comes back with what was asked and what the branch has. A line the website does not show is left out.
Parameters
itemsarray · body · required
The cart's lines, 1 to 100.
items.*.product_idstring · body · required
The product's id.
items.*.variant_idstring · body
The variant's id. Without it, the product's first variant at the website branch is checked.
items.*.quantityinteger · body · required
1 to 100.
When any line asks for more than the branch holds, it answers 422. Each short line is listed under errors.items with its own message, such as "Only 22 in stock for Fox Speedframe helmet.". The top message is "Only 22 in stock for Fox Speedframe helmet. Please reduce quantity." for one short line, and "Some items in your cart exceed available stock. Please adjust quantities to continue." for more than one. variant_label is the variant's colour and size, joined by " / "; it is empty when the variant has neither. Checkout runs the same check and gives the same 422.
The signed-in customer's saved cart, priced at the website branch now. A saved line whose product or variant the website no longer shows is left out of the answer. discount and vat are always 0; item_count is the sum of the quantities.
Stock is not checked on the saved cart; call cart/stock-check before checkout.
Replaces the signed-in customer's saved cart with the website's own, as when a shopper signs in with a cart built as a guest. It does not merge: every saved line is removed first. Lines the website does not show are skipped, and two lines for the same variant are added together, up to 99. Answers the cart as GET cart does.
Parameters
itemsarray · body · required
The website's cart, up to 100 lines. An empty list empties the saved cart.
items.*.product_idstring · body · required
The product's id.
items.*.variant_idstring · body
The variant's id. Without it, the product's first variant at the website branch is saved.
items.*.quantityinteger · body · required
1 to 99.
If the saved cart should survive sign-in, read it with GET cart first and send the combined lines.
Adds a product to the saved cart. Adding a variant already in the cart adds to its line, up to 99. Answers the whole cart as GET cart does.
Parameters
product_idstring · body · required
The product's id.
variant_idstring · body
The variant's id. Needed when the product has more than one variant at the website branch.
quantityinteger · body · required
1 to 99.
404 "Product not found" when the website does not show the product or the variant given. 422 "This product requires a variant selection." when the product has more than one variant and no variant_id is sent. Stock is not checked here.
Checks a coupon by the till's rules without taking a use: it must be on, not expired and not used up, cart_total must reach its minimum, and the customer must be under its limit per customer. The customer is the signed-in one, or for a guest the shop's customer with the email given; with neither, the limit per customer is not checked. A use is counted only when an order is placed with the code. The answer has no data wrapper.
Parameters
codestring · body · required
The coupon code, in any case, up to 50 characters.
cart_totalnumber · body · required
The items' total, which the minimum and the discount are worked on.
emailstring · body
A guest's email, so their earlier uses count.
404 with success false and "Invalid coupon code" when no coupon has the code. 400 with success false and the reason when it cannot be used, such as "Coupon EASTER-15 expired on Tue, Sep 22, 2026." type is percent or fixed; discount_amount is worked on cart_total. expires_at is in UTC, or null. Send the code as coupon_code when placing the order; an order takes a coupon or points, not both.
Kept for websites built on Bike Yard's, which call it when the customer takes a coupon off. Nothing is stored or changed and it always answers success; drop the code from the website's own checkout state. The answer has no data wrapper.
Whether a friend's referral code works. Always answers 200: valid is false when no customer has the code or when the shop has loyalty or referrals turned off. A valid code gives only the referrer's first name and last initial, enough to recognise them.
Parameters
codestring · body · required
The code, in any case, up to 12 characters.
Send the code as referral_code when placing the order or registering. A code that does not work never stops the order.
Places an order without a login. It goes on the shop's customer with that phone number, their saved details left as they are, or on a new website customer. The order is placed at the website branch, priced as the till would, and waits for staff to confirm it in the back office: its stock is not taken until then. The name, address, delivery method, payment method and the customer's note are written into the order's notes. The customer gets the shop's Order received message and email, and staff who confirm orders at the website branch are told. Answers 201.
Parameters
itemsarray · body · required
The cart's lines, 1 to 50.
items.*.product_idstring · body · required
The product's id.
items.*.variant_idstring · body
The variant's id. Without it, the product's first variant at the website branch is ordered.
items.*.quantityinteger · body · required
1 to 100.
namestring · body · required
Up to 255 characters.
emailstring · body · required
Where the order email goes.
phonestring · body · required
9 to 20 characters, such as 0712 345 678. It finds the customer.
address_line_1string · body
Up to 500 characters. Needed when the order is delivered; not for shop_pickup.
address_line_2string · body
Up to 500 characters.
citystring · body
Up to 100 characters. Needed when the order is delivered; not for shop_pickup.
countystring · body
Up to 100 characters.
notesstring · body
The customer's note, up to 1,000 characters.
shipping_methodstring · body
shop_pickup (the default), nairobi, country_wide or east_africa, one the shop offers (storefront/config). Its fee goes on the order as a delivery charge, unless the goods reach the shop's free-delivery amount or the choice has no fee, when staff set any charge as they confirm.
payment_methodstring · body
PAY_LATER (the default), PESAPAL or LIPA_POLE_POLE. MPESA and BANK are still accepted for older websites and are only written into the notes.
coupon_codestring · body
A coupon, in any case. It takes one of the coupon's uses with the order.
referral_codestring · body
A friend's referral code, up to 12 characters. Recorded on the customer when it works; ignored when it does not.
pointsinteger · body
Refused for a guest: points can only be spent through POST orders with a login.
idempotency_keystring · body
Any string up to 255 characters, new for each checkout. A repeat with the same key and email within a day answers with the first order.
Keep data.order.id: it is the only way a guest reaches the order again, through orders/{uuid}/status, receipt and guest-pay. With PESAPAL or LIPA_POLE_POLE the answer also carries data.pesapal_redirect_url, as POST orders shows; send the customer there. If Pesapal is off for the shop or could not make the page, the order still stands without it, to be paid later with guest-pay. Refusals answer 422 and place nothing: a short line as cart/stock-check gives; "An item in your cart is no longer available. Please remove it and try again." under errors.items when the website no longer shows a line; the coupon's reason under errors.coupon_code, such as "There is no coupon NOPE."; "Log in to spend your points." under errors.points; and for LIPA_POLE_POLE the shop's rule under errors.order, such as "Lipa Pole Pole is for orders of KES 20,000 or more.".
Places an order as the signed-in customer, in the same way as orders/guest and with the same fields, except that the address is optional and points may be spent. When address_line_1 and city are both given, the address is saved to the customer's addresses: the same line and city updates the saved one, and the first one saved becomes the default. Answers 201.
Parameters
itemsarray · body · required
The cart's lines, 1 to 50, each with product_id, variant_id and quantity as for orders/guest. The saved cart is not read or emptied; send its lines.
namestring · body · required
Up to 255 characters.
emailstring · body · required
Up to 255 characters.
phonestring · body · required
9 to 20 characters.
address_line_1, address_line_2, city, countystring · body
Where to deliver, as for orders/guest.
notes, shipping_method, payment_method, coupon_code, referral_codestring · body
As for orders/guest.
pointsinteger · body
Loyalty points to spend, at least 1, by the till's rules: the shop's value for each point comes off the total, up to its share of the items' total. Not with a coupon.
idempotency_keystring · body
New for each checkout. A repeat with the same key from the same customer within a day answers with the first order.
With PESAPAL the Pesapal page is for the whole balance; with LIPA_POLE_POLE a plan is started and the page is for its deposit. Pesapal sends the customer back to {website address}/checkout/confirmation?order={order id}, the website address the shop sets on its Storefront API page, or to Debian POS's own page while none is set; the website then calls payments/status. A website order paid in full online takes its stock at once, so it cannot be sold at the counter, and still waits for staff to confirm it; so does a Lipa Pole Pole order once its deposit is in. A part payment takes no stock. Besides the refusals of orders/guest, points can be refused under errors.points with "An order can carry either a coupon or a points redemption, not both. Remove one and try again." or the till's rule, such as "Loyalty is switched off, so points cannot be spent.".
The signed-in customer's orders, newest first, in Laravel's paginator shape. Their sales at the till are included, as they are the same customer. Each order reads as in GET orders/{uuid}.
One of the signed-in customer's own orders. status is pending while the order waits for staff to confirm it, then active, or cancelled; payment_status is pending, partially_paid, fully_paid, over_paid or cancelled. instalment_plan is null unless the order is on Lipa Pole Pole.
Parameters
uuidstring · path · required
The order's id.
404 for an order that is not this customer's. The plan's status is awaiting_deposit, expired (the deposit's time has passed), active, completed or cancelled. deposit_outstanding is what is still owed towards deposit_minimum before deposit_due_by; due_at is when the whole balance is due. can_pay_online is true while the plan is awaiting its deposit or active, the order owes money and the shop takes Pesapal; min_payment and max_payment bound the amount to send to pay or guest-pay, and are null when it cannot be paid. payments lists counter payments and completed online ones, online ones with the method Online.
Any order by its id, with no login, for the website's confirmation page and for guests. The id is given only to the order's customer. The body is the same as GET orders/{uuid}.
The customer's copy of the receipt as a PDF download, named for the order number (receipt-by-000017.pdf), with every payment so far and the balance. By the order's id, with no login, as orders/{uuid}/status is. There is no JSON example: the body is the PDF itself, with Content-Type application/pdf.
Parameters
uuidstring · path · required
The order's id.
404 when the id is not an order's. Link to it directly from the confirmation page or the customer's account.
Paying online
POST
/api/v1/orders/{uuid}/pay
Scope
checkout
Customer token
required
Makes a Pesapal page for one of the signed-in customer's own orders, as when they closed Pesapal's page before paying, chose to pay later, or are paying a Lipa Pole Pole instalment. An order pays its whole balance; a Lipa Pole Pole order pays the amount sent. A page still waiting for the same amount is given again, so a second tap never makes a second charge; one waiting for another amount is cancelled first. An order waiting for staff to confirm it can be paid.
Parameters
uuidstring · path · required
The order's id.
amountnumber · body
Only for a Lipa Pole Pole order, and needed for one: at least 1 and no more than the balance. Ignored for any other order.
Pesapal sends the customer back to {website address}/checkout/confirmation?order={order id}, as at checkout. Refusals: 503 "Online payments are currently not available." when the shop does not take Pesapal; 422 "This order was cancelled." or "This order is already fully paid."; for a Lipa Pole Pole order, 422 "Enter an amount of at least KES 1.", "The amount is more than the balance of KES 72,000.", "This Lipa Pole Pole plan is paid off and cannot take payments." (or is cancelled), or "The time to pay this Lipa Pole Pole deposit has passed. Please call us on {the shop's phone}."; 502 "Could not initiate payment. Please try again later." when Pesapal could not make the page. 404 for an order that is not this customer's.
The same as orders/{uuid}/pay, by the order's id with no login, for a guest. A Lipa Pole Pole order pays the amount sent; any other order pays its whole balance.
Parameters
uuidstring · path · required
The order's id, data.order.id from checkout.
amountnumber · body
Only for a Lipa Pole Pole order, and needed for one: at least 1 and no more than the balance. Ignored for any other order.
Refusals as for orders/{uuid}/pay. 404 when the id is not an order's.
Where a Pesapal payment stands, for the website's confirmation page. Pesapal sends the customer back to {website address}/checkout/confirmation?order={order id} with OrderTrackingId and OrderMerchantReference added; pass either one on. A payment not yet settled is asked of Pesapal again before answering, and credited to the order only once, so the page may call this more than once. When Pesapal cannot be reached, the last state known is given.
Parameters
OrderTrackingIdstring · query
Pesapal's tracking id, from the return address. This or OrderMerchantReference is required.
OrderMerchantReferencestring · query
The payment's reference, from the return address.
400 "Missing OrderTrackingId" with neither; 404 "Payment not found" when no payment has them. payment_status is Pesapal's: INITIATED, PENDING, Completed, Failed, Reversed, INVALID, Cancelled or Refund Requested. payment_method is the method Pesapal reports, null until paid. Once it is Completed, read orders/{uuid}/status for the order's new balance and payment_status.
Asks the shop to source something it does not stock, with photos or documents. Send it as multipart/form-data when files are attached. With a login it is kept on the customer and appears in their pre-orders; without one, only on the pre-order. It starts as pending. The customer gets the pre-order email and staff who see pre-orders are told. Answers 201.
Parameters
customer_namestring · body · required
Up to 255 characters.
customer_emailstring · body · required
Where the pre-order email goes.
customer_phonestring · body · required
Up to 50 characters.
needed_by_datedate · body
When the customer needs it, such as 2026-12-01.
shipping_methodstring · body · required
air or sea.
itemsarray · body · required
1 to 20 items.
items.*.product_namestring · body · required
Up to 255 characters.
items.*.descriptionstring · body · required
Size, colour, model and the like, up to 2,000 characters.
items.*.quantityinteger · body · required
At least 1.
items.*.reference_linkstring · body
A link to the item, up to 500 characters.
filesarray · body
Up to 10 files, each JPG, JPEG, PNG, GIF or PDF and under 10 MB.
Refusals answer 422 with Bike Yard's messages: "Please add at least one item to your pre-order.", "Product name is required for all items.", "Description is required for all items.", "Only JPG, PNG, GIF, and PDF files are allowed." and "Each file must be less than 10MB.". A guest cannot see the pre-order afterwards; the shop contacts them with the quote.
Example answer
{
"data": {
"id": "01a16760-7d4e-7b28-9a61-3e5c0f8b2d94",
"message": "Thank you! Your pre-order request has been submitted successfully. A confirmation email has been sent to wanjiru@example.com. We will contact you within 24-48 hours with a detailed quote."
}
}
GET
/api/v1/pre-orders
Scope
checkout
Customer token
required
The signed-in customer's pre-orders, newest first, all of them in one list. status is pending, sourcing, quoted, accepted, rejected, shipping, completed or cancelled. landing_price is the quoted price and each item's item_price its share; both are 0 until the shop quotes.
customer_confirmed is the staff's own record that they checked who the customer is before sourcing; it is not the customer's answer to the quote.
One of the signed-in customer's own pre-orders, with its tracking number, when it was quoted, shipped and completed, the customer's note on the quote, and its status history, newest first.
Accepts or declines the quote on one of the signed-in customer's own pre-orders. Only a quoted pre-order takes an answer: accepted moves it to accepted and declined to rejected, the note kept as customer_response_note and on its history. Staff who see pre-orders are told.
Parameters
idstring · path · required
The pre-order's id.
responsestring · body · required
accepted or declined.
notesstring · body
The customer's note to the shop, up to 1,000 characters.
Declining answers "Pre-order declined.". 422 "This pre-order is not awaiting a response." under errors.response when it is not quoted. 404 for a pre-order that is not this customer's.
Example answer
{
"data": {
"message": "Pre-order confirmed. We will begin processing your order."
}
}