Skip to content

API reference

Cart and checkout

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

items array · body · required
The cart's lines, up to 100. May be empty.
items.*.product_id string · body · required
The product's id.
items.*.variant_id string · body
The variant's id. Without it, the product's first variant at the website branch is priced.
items.*.quantity integer · body · required
1 to 99.
shipping_method string · 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.

Example answer
{
  "sub_total": 248000,
  "vat": 0,
  "delivery_fee": 500,
  "total": 248500,
  "lines": [
    {
      "product_id": "01a16356-86d9-70ef-a8b8-caf1c046af33",
      "variant_id": "01a16356-86df-7218-9383-60fd40425609",
      "quantity": 2,
      "unit_price": 112000,
      "total": 224000,
      "available": 10
    }
  ]
}
POST

/api/v1/cart/stock-check

Scope
checkout
Customer token
none

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

items array · body · required
The cart's lines, 1 to 100.
items.*.product_id string · body · required
The product's id.
items.*.variant_id string · body
The variant's id. Without it, the product's first variant at the website branch is checked.
items.*.quantity integer · 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.

Example answer
{
  "data": {
    "valid": true,
    "items": [
      {
        "product_id": "01a16356-8716-7254-a23b-4fa5bdd2e54c",
        "variant_id": "01a16356-871a-70a2-a045-f9bde015f03e",
        "product_name": "Fox Speedframe helmet",
        "variant_label": "",
        "requested": 1,
        "available": 22
      }
    ]
  }
}
GET

/api/v1/cart

Scope
checkout
Customer token
required

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.

Example answer
{
  "data": {
    "items": [
      {
        "id": "01a16741-0c5e-7a12-9b3f-2d6e8a4c1f70",
        "product": {
          "id": "01a16356-8716-7254-a23b-4fa5bdd2e54c",
          "name": "Fox Speedframe helmet",
          "slug": "fox-speedframe-helmet",
          "image": null
        },
        "variant": {
          "id": "01a16356-871a-70a2-a045-f9bde015f03e",
          "size": null,
          "color": null,
          "sku": "FOX-SPF"
        },
        "quantity": 2,
        "unit_price": 24000,
        "total": 48000
      }
    ],
    "sub_total": 48000,
    "discount": 0,
    "vat": 0,
    "total": 48000,
    "item_count": 2
  }
}
POST

/api/v1/cart/sync

Scope
checkout
Customer token
required

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

items array · body · required
The website's cart, up to 100 lines. An empty list empties the saved cart.
items.*.product_id string · body · required
The product's id.
items.*.variant_id string · body
The variant's id. Without it, the product's first variant at the website branch is saved.
items.*.quantity integer · body · required
1 to 99.

If the saved cart should survive sign-in, read it with GET cart first and send the combined lines.

Example answer
{
  "data": {
    "items": [
      {
        "id": "01a16741-0c5e-7a12-9b3f-2d6e8a4c1f70",
        "product": {
          "id": "01a16356-86d9-70ef-a8b8-caf1c046af33",
          "name": "Trek Marlin 5",
          "slug": "trek-marlin-5",
          "image": null
        },
        "variant": {
          "id": "01a16356-86df-7218-9383-60fd40425609",
          "size": null,
          "color": null,
          "sku": "TRK-MAR5-M"
        },
        "quantity": 1,
        "unit_price": 112000,
        "total": 112000
      }
    ],
    "sub_total": 112000,
    "discount": 0,
    "vat": 0,
    "total": 112000,
    "item_count": 1
  }
}
POST

/api/v1/cart/items

Scope
checkout
Customer token
required

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_id string · body · required
The product's id.
variant_id string · body
The variant's id. Needed when the product has more than one variant at the website branch.
quantity integer · 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.

Example answer
{
  "data": {
    "items": [
      {
        "id": "01a16741-0c5e-7a12-9b3f-2d6e8a4c1f70",
        "product": {
          "id": "01a16356-8716-7254-a23b-4fa5bdd2e54c",
          "name": "Fox Speedframe helmet",
          "slug": "fox-speedframe-helmet",
          "image": null
        },
        "variant": {
          "id": "01a16356-871a-70a2-a045-f9bde015f03e",
          "size": null,
          "color": null,
          "sku": "FOX-SPF"
        },
        "quantity": 1,
        "unit_price": 24000,
        "total": 24000
      }
    ],
    "sub_total": 24000,
    "discount": 0,
    "vat": 0,
    "total": 24000,
    "item_count": 1
  }
}
PATCH

/api/v1/cart/items/{id}

Scope
checkout
Customer token
required

Sets the quantity of one line in the saved cart. Answers the whole cart as GET cart does.

Parameters

id string · path · required
The cart line's id, from items.*.id.
quantity integer · body · required
The new quantity, 1 to 99.

404 when the line is not in this customer's cart. To take a line off, use DELETE; a quantity of 0 is refused.

Example answer
{
  "data": {
    "items": [
      {
        "id": "01a16741-0c5e-7a12-9b3f-2d6e8a4c1f70",
        "product": {
          "id": "01a16356-8716-7254-a23b-4fa5bdd2e54c",
          "name": "Fox Speedframe helmet",
          "slug": "fox-speedframe-helmet",
          "image": null
        },
        "variant": {
          "id": "01a16356-871a-70a2-a045-f9bde015f03e",
          "size": null,
          "color": null,
          "sku": "FOX-SPF"
        },
        "quantity": 3,
        "unit_price": 24000,
        "total": 72000
      }
    ],
    "sub_total": 72000,
    "discount": 0,
    "vat": 0,
    "total": 72000,
    "item_count": 3
  }
}
DELETE

/api/v1/cart/items/{id}

Scope
checkout
Customer token
required

Takes one line off the saved cart. Answers what is left, as GET cart does.

Parameters

id string · path · required
The cart line's id, from items.*.id.

404 when the line is not in this customer's cart.

Example answer
{
  "data": {
    "items": [],
    "sub_total": 0,
    "discount": 0,
    "vat": 0,
    "total": 0,
    "item_count": 0
  }
}

Coupons and referrals

POST

/api/v1/coupons/validate

Scope
checkout
Customer token
optional
Limit
20 a minute

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

code string · body · required
The coupon code, in any case, up to 50 characters.
cart_total number · body · required
The items' total, which the minimum and the discount are worked on.
email string · 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.

Example answer
{
  "success": true,
  "message": "Coupon applied successfully",
  "coupon": {
    "id": "01a16356-8e0e-70d4-b697-32955b587d6f",
    "code": "SAVE-500",
    "name": "KES 500 off a big basket",
    "type": "fixed",
    "value": 500,
    "discount_amount": 500,
    "min_cart_value": 5000,
    "expires_at": "2026-12-21 20:59:59"
  }
}
POST

/api/v1/coupons/remove

Scope
checkout
Customer token
none
Limit
20 a minute

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.

Example answer
{
  "success": true,
  "message": "Coupon removed successfully"
}
POST

/api/v1/referrals/validate

Scope
checkout
Customer token
none
Limit
20 a minute

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

code string · 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.

Example answer
{
  "data": {
    "valid": true,
    "referrer_name": "Grace A."
  }
}

Orders

POST

/api/v1/orders/guest

Scope
checkout
Customer token
none
Limit
10 a minute

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

items array · body · required
The cart's lines, 1 to 50.
items.*.product_id string · body · required
The product's id.
items.*.variant_id string · body
The variant's id. Without it, the product's first variant at the website branch is ordered.
items.*.quantity integer · body · required
1 to 100.
name string · body · required
Up to 255 characters.
email string · body · required
Where the order email goes.
phone string · body · required
9 to 20 characters, such as 0712 345 678. It finds the customer.
address_line_1 string · body
Up to 500 characters. Needed when the order is delivered; not for shop_pickup.
address_line_2 string · body
Up to 500 characters.
city string · body
Up to 100 characters. Needed when the order is delivered; not for shop_pickup.
county string · body
Up to 100 characters.
notes string · body
The customer's note, up to 1,000 characters.
shipping_method string · 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_method string · 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_code string · body
A coupon, in any case. It takes one of the coupon's uses with the order.
referral_code string · body
A friend's referral code, up to 12 characters. Recorded on the customer when it works; ignored when it does not.
points integer · body
Refused for a guest: points can only be spent through POST orders with a login.
idempotency_key string · 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.".

Example answer
{
  "data": {
    "order": {
      "id": "01a16732-e01a-721d-80eb-906fd14c3e29",
      "order_number": "BY-000017",
      "status": "pending",
      "items": [
        {
          "product_name": "Fox Speedframe helmet",
          "product_slug": "fox-speedframe-helmet",
          "product_image": null,
          "variant_image": null,
          "variant_name": "",
          "quantity": 1,
          "unit_price": 24000,
          "total": 24000
        }
      ],
      "sub_total": 24000,
      "discount": 0,
      "vat": 0,
      "total": 24000,
      "amount_paid": 0,
      "balance": 24000,
      "payment_status": "pending",
      "notes": "Shipping: Wanjiru Kamau | 0712 345 678 | wanjiru@example.com | Riverside Drive 14 | Nairobi, Nairobi\nDelivery method: Nairobi\nPayment method: Pay later\nCustomer notes: Call at the gate",
      "instalment_plan": null,
      "created_at": "2026-10-23T05:19:09.000000Z",
      "charges": []
    }
  }
}
POST

/api/v1/orders

Scope
checkout
Customer token
required

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

items array · 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.
name string · body · required
Up to 255 characters.
email string · body · required
Up to 255 characters.
phone string · body · required
9 to 20 characters.
address_line_1, address_line_2, city, county string · body
Where to deliver, as for orders/guest.
notes, shipping_method, payment_method, coupon_code, referral_code string · body
As for orders/guest.
points integer · 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_key string · 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.".

Example answer
{
  "data": {
    "order": {
      "id": "01a16748-3b2d-7e51-8c0a-1f4d6b9e2a35",
      "order_number": "BY-000018",
      "status": "pending",
      "items": [
        {
          "product_name": "Fox Speedframe helmet",
          "product_slug": "fox-speedframe-helmet",
          "product_image": null,
          "variant_image": null,
          "variant_name": "",
          "quantity": 1,
          "unit_price": 24000,
          "total": 24000
        }
      ],
      "sub_total": 24000,
      "discount": 0,
      "vat": 0,
      "total": 24000,
      "amount_paid": 0,
      "balance": 24000,
      "payment_status": "pending",
      "notes": "Shipping: Wanjiru Kamau | 0712 345 678 | wanjiru@example.com | Riverside Drive 14 | Nairobi, Nairobi\nDelivery method: Nairobi\nPayment method: Pay online (Pesapal)",
      "instalment_plan": null,
      "created_at": "2026-10-23T05:42:31.000000Z",
      "charges": []
    },
    "pesapal_redirect_url": "https://pay.pesapal.com/iframe/PesapalIframe3/Index?OrderTrackingId=b945e4af-80a5-4ec1-8706-e03f8332fb04"
  }
}
GET

/api/v1/orders

Scope
checkout
Customer token
required

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}.

Parameters

per_page integer · query
1 to 50, 15 by default.
page integer · query
The page, from 1.
Example answer
{
  "current_page": 1,
  "data": [
    {
      "id": "01a16356-8b9e-70ef-80ea-bfc44a0c1f9e",
      "order_number": "BY-000001",
      "status": "active",
      "items": [
        {
          "product_name": "Shimano Deore 12-speed chain",
          "product_slug": "shimano-deore-12-speed-chain",
          "product_image": null,
          "variant_image": null,
          "variant_name": "",
          "quantity": 1,
          "unit_price": 4800,
          "total": 4800
        }
      ],
      "sub_total": 4800,
      "discount": 0,
      "vat": 0,
      "total": 4800,
      "amount_paid": 4800,
      "balance": 0,
      "payment_status": "fully_paid",
      "notes": null,
      "instalment_plan": null,
      "created_at": "2026-10-09T08:15:00.000000Z",
      "charges": []
    }
  ],
  "first_page_url": "https://app.debianpos.com/api/v1/orders?page=1",
  "from": 1,
  "last_page": 1,
  "last_page_url": "https://app.debianpos.com/api/v1/orders?page=1",
  "links": [
    {
      "url": null,
      "label": "« Previous",
      "page": null,
      "active": false
    },
    {
      "url": "https://app.debianpos.com/api/v1/orders?page=1",
      "label": "1",
      "page": 1,
      "active": true
    },
    {
      "url": null,
      "label": "Next »",
      "page": null,
      "active": false
    }
  ],
  "next_page_url": null,
  "path": "https://app.debianpos.com/api/v1/orders",
  "per_page": 15,
  "prev_page_url": null,
  "to": 1,
  "total": 1
}
GET

/api/v1/orders/{uuid}

Scope
checkout
Customer token
required

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

uuid string · 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.

Example answer
{
  "data": {
    "id": "01a16752-4f0b-7c3d-a1e2-6b9f0d3c8e17",
    "order_number": "BY-000019",
    "status": "pending",
    "items": [
      {
        "product_name": "Trek Marlin 5",
        "product_slug": "trek-marlin-5",
        "product_image": null,
        "variant_image": null,
        "variant_name": "",
        "quantity": 1,
        "unit_price": 112000,
        "total": 112000
      }
    ],
    "sub_total": 112000,
    "discount": 0,
    "vat": 0,
    "total": 112000,
    "amount_paid": 40000,
    "balance": 72000,
    "payment_status": "partially_paid",
    "notes": "Shipping: Wanjiru Kamau | 0712 345 678 | wanjiru@example.com | Riverside Drive 14 | Nairobi, Nairobi\nDelivery method: Shop pickup\nPayment method: Lipa Pole Pole (instalments)",
    "instalment_plan": {
      "status": "active",
      "overdue": false,
      "deposit_minimum": 33600,
      "deposit_outstanding": 0,
      "deposit_due_by": "2026-10-26T20:59:59.000000Z",
      "deposit_paid_at": "2026-10-23T06:02:41.000000Z",
      "due_at": "2026-12-22T06:02:41.000000Z",
      "refunded_amount": 0,
      "can_pay_online": true,
      "min_payment": 1,
      "max_payment": 72000,
      "payments": [
        {
          "amount": 40000,
          "method": "Online",
          "paid_at": "2026-10-23T06:02:41.000000Z"
        }
      ]
    },
    "created_at": "2026-10-23T05:58:12.000000Z",
    "charges": []
  }
}
GET

/api/v1/orders/{uuid}/status

Scope
checkout
Customer token
none

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}.

Parameters

uuid string · path · required
The order's id, data.order.id from checkout.

404 when the id is not an order's.

Example answer
{
  "data": {
    "id": "01a16732-e01a-721d-80eb-906fd14c3e29",
    "order_number": "BY-000017",
    "status": "pending",
    "items": [
      {
        "product_name": "Fox Speedframe helmet",
        "product_slug": "fox-speedframe-helmet",
        "product_image": null,
        "variant_image": null,
        "variant_name": "",
        "quantity": 1,
        "unit_price": 24000,
        "total": 24000
      }
    ],
    "sub_total": 24000,
    "discount": 0,
    "vat": 0,
    "total": 24000,
    "amount_paid": 0,
    "balance": 24000,
    "payment_status": "pending",
    "notes": "Shipping: Wanjiru Kamau | 0712 345 678 | wanjiru@example.com | Riverside Drive 14 | Nairobi, Nairobi\nDelivery method: Nairobi\nPayment method: Pay later\nCustomer notes: Call at the gate",
    "instalment_plan": null,
    "created_at": "2026-10-23T05:19:09.000000Z",
    "charges": []
  }
}
GET

/api/v1/orders/{uuid}/receipt

Scope
checkout
Customer token
none
Limit
10 a minute

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

uuid string · 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

uuid string · path · required
The order's id.
amount number · 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.

Example answer
{
  "data": {
    "pesapal_redirect_url": "https://pay.pesapal.com/iframe/PesapalIframe3/Index?OrderTrackingId=b945e4af-80a5-4ec1-8706-e03f8332fb04"
  }
}
POST

/api/v1/orders/{uuid}/guest-pay

Scope
checkout
Customer token
none
Limit
10 a minute

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

uuid string · path · required
The order's id, data.order.id from checkout.
amount number · 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.

Example answer
{
  "data": {
    "pesapal_redirect_url": "https://pay.pesapal.com/iframe/PesapalIframe3/Index?OrderTrackingId=b945e4af-80a5-4ec1-8706-e03f8332fb04"
  }
}
GET

/api/v1/payments/status

Scope
checkout
Customer token
none
Limit
30 a minute

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

OrderTrackingId string · query
Pesapal's tracking id, from the return address. This or OrderMerchantReference is required.
OrderMerchantReference string · 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.

Example answer
{
  "data": {
    "payment_status": "Completed",
    "confirmation_code": "SJK4M2X8QP",
    "payment_method": "MpesaKE",
    "amount": 24000,
    "order_id": "01a16732-e01a-721d-80eb-906fd14c3e29",
    "order_number": "BY-000017"
  }
}

Pre-orders

POST

/api/v1/pre-orders

Scope
checkout
Customer token
optional
Limit
10 a minute

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_name string · body · required
Up to 255 characters.
customer_email string · body · required
Where the pre-order email goes.
customer_phone string · body · required
Up to 50 characters.
needed_by_date date · body
When the customer needs it, such as 2026-12-01.
shipping_method string · body · required
air or sea.
items array · body · required
1 to 20 items.
items.*.product_name string · body · required
Up to 255 characters.
items.*.description string · body · required
Size, colour, model and the like, up to 2,000 characters.
items.*.quantity integer · body · required
At least 1.
items.*.reference_link string · body
A link to the item, up to 500 characters.
files array · 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.

Example answer
{
  "data": [
    {
      "id": "01a16356-8ddf-73c8-abee-6fc3cc06bc66",
      "status": "quoted",
      "shipping_method": "air",
      "needed_by_date": null,
      "landing_price": 52000,
      "customer_confirmed": true,
      "created_at": "2026-10-15T07:15:00+00:00",
      "items": [
        {
          "product_name": "Shimano Deore XT M8100 groupset",
          "description": "12-speed, 10-51T cassette, 175 mm cranks, 32T ring.",
          "quantity": 1,
          "reference_link": null,
          "item_price": 52000
        }
      ]
    }
  ]
}
GET

/api/v1/pre-orders/{id}

Scope
checkout
Customer token
required

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.

Parameters

id string · path · required
The pre-order's id.

404 for a pre-order that is not this customer's.

Example answer
{
  "data": {
    "id": "01a16356-8ddf-73c8-abee-6fc3cc06bc66",
    "status": "quoted",
    "shipping_method": "air",
    "needed_by_date": null,
    "landing_price": 52000,
    "customer_confirmed": true,
    "created_at": "2026-10-15T07:15:00+00:00",
    "items": [
      {
        "product_name": "Shimano Deore XT M8100 groupset",
        "description": "12-speed, 10-51T cassette, 175 mm cranks, 32T ring.",
        "quantity": 1,
        "reference_link": null,
        "item_price": 52000
      }
    ],
    "tracking_number": null,
    "price_quoted_at": "2026-10-17T09:15:00+00:00",
    "shipped_at": null,
    "completed_at": null,
    "customer_response_note": null,
    "status_history": [
      {
        "from_status": "sourcing",
        "to_status": "quoted",
        "notes": null,
        "created_at": "2026-10-17T09:15:00+00:00"
      }
    ]
  }
}
POST

/api/v1/pre-orders/{id}/respond

Scope
checkout
Customer token
required

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

id string · path · required
The pre-order's id.
response string · body · required
accepted or declined.
notes string · 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."
  }
}