Sales and Goods for Sale
These resources cover the Sales module: goods such as memory cards, batteries, or gaffer tape that a rental business sells with a stock count, next to its rentals.
RequiplyError with status403 and reasonfeature_disabled (selling_disabled for a publishable key).| Method | Secret key | Publishable key |
|---|---|---|
saleItems.list(), saleItems.get() | Yes | Active products only, while the organization sells to customers |
saleLines on pricing.quote() and bookings.create() | Yes | Yes, within the stock on hand, while the organization sells to customers |
sales.list(), sales.get(), sales.create() | Yes | No |
Goods for sale are kept apart from rental products: inventory.list(), availability, and categories.list() never return them.
With a publishable key
A storefront can show goods for sale and let customers add them to a booking request, the way the client portal does in its Items to buy step. A publishable key reaches them only while the organization sells to customers, which takes all three of:
- the Sales feature on the Advanced plan, as above;
- Selling items switched on in the client portal settings;
- at least one product for sale that is not archived.
Otherwise the call throws 403 with reason selling_disabled, so a storefront can hide its goods section.
With a publishable key:
saleItems.list()returns active products only, whateverfiltersays.barcodeandpurchaseCostare alwaysnull.saleItems.get()answers404for an archived product, andsoldUnits30d,revenue30d, andsoldUnitsTotalare0.saleLinescannot ask for more thanstockOnHand. A line over the stock throws409withreasonout_of_stock;data.saleItemIdnames the product anddata.availableQuantityhow many are left (0when sold out).
try {
await requiply.pricing.quote({ startDate, endDate, items, saleLines });
} catch (error) {
if (error instanceof RequiplyError && error.reason === 'out_of_stock') {
showNotice(`Only ${error.data?.availableQuantity} left`);
} else if (error instanceof RequiplyError && error.reason === 'selling_disabled') {
hideGoodsSection();
} else {
throw error;
}
}
saleItems.list()
saleItems.list(
params?: SaleItemsListParams,
options?: RequiplyRequestOptions,
): Promise<SaleItemsListResponse>
Optional parameters: search (name, SKU, or barcode), category (category ID), filter (active by default, archived, or all; publishable keys always get active), limit (1 to 100, default 25), and cursor.
const { items, nextCursor } = await requiply.saleItems.list({ search: 'battery' });
for (const item of items) {
console.log(item.name, item.salePrice, item.currency, item.stockOnHand);
}
| Field | What it holds |
|---|---|
id, name, sku, barcode | The product. sku and barcode can be null; barcode is always null for a publishable key. |
categoryId, categoryName, imageUrl | Can be null. |
salePrice, currency | Flat price per unit in the organization currency. |
stockOnHand | Units on the shelf. It can be negative when sales outran the recorded stock. |
purchaseCost | Net cost per unit, or null unless the key may manage stock. Always null for a publishable key. |
isArchived | Archived products cannot be sold. |
saleItems.get()
saleItems.get(
saleItemId: string,
options?: RequiplyRequestOptions,
): Promise<SaleItemResponse>
Returns saleItem with the fields above plus soldUnits30d, revenue30d, and soldUnitsTotal.
const { saleItem } = await requiply.saleItems.get(itemId);
console.log(saleItem.soldUnits30d, saleItem.revenue30d);
sales.list()
sales.list(
params?: SalesListParams,
options?: RequiplyRequestOptions,
): Promise<SalesListResponse>
Lists sales, newest first, both standalone and sold with bookings. Optional filters:
| Parameter | Effect |
|---|---|
kind | standalone or booking. |
status | completed or cancelled. |
unpaid | true for standalone sales that still owe money. |
clientId | One client's sales. |
soldFrom, soldTo | ISO 8601 date-times. |
search | Sale number, client name, email, or phone, product name or SKU. |
limit, cursor | Page size (1 to 100, default 25) and the nextCursor of the previous page. |
const { sales, currency } = await requiply.sales.list({ kind: 'standalone', unpaid: true });
Each sale has id, saleOrgId (the sale number staff see in the app), status, soldAt, total, returnedTotal, net (total minus returns), paidAmount, balance, paymentMethod, paidAt, note, the client, the booking it belongs to (reservationId, reservationOrgId), itemCount, and itemsLabel. balance is what a standalone sale still owes; it is always 0 for a sale on a booking, which is paid through the booking.
sales.get()
sales.get(
id: string | number,
params?: { idKind?: 'internal' | 'org' },
options?: RequiplyRequestOptions,
): Promise<SaleResponse>
id is the sale UUID, or the sale number from the app with idKind: 'org'.
const { sale } = await requiply.sales.get(12, { idKind: 'org' });
The sale also carries lines (product, quantity, returnedQuantity, unitPrice, lineTotal), returns, and payments. For a sale on a booking, payments are the booking's, because the booking collects the rental and the goods in one payment.
sales.create()
sales.create(
input: SaleCreateInput,
options?: RequiplyRequestOptions,
): Promise<SaleCreateResponse>
Records a standalone sale, like New sale in the app.
const { sale, stockWarnings, payment } = await requiply.sales.create({
lines: [{ saleItemId: itemId, quantity: 2 }],
client: { name: 'Alex', email: 'customer@example.com' },
paymentMethod: 'monobank',
note: 'Counter sale',
});
if (payment?.paymentUrl) console.log('Pay here:', payment.paymentUrl);
| Field | Rules |
|---|---|
lines | Required. 1 to 100 products, each at most once, with a whole quantity above zero. Every line sells at the product's salePrice. |
clientId or client | Optional. client is matched by email, then phone, and created when neither matches. |
paymentMethod | payOnPickup (default), or monobank, liqpay, wayforpay, or stripe with an active integration. liqpay needs a client with an email. |
soldAt | ISO 8601. Defaults to now. |
note | Up to 2000 characters. |
The stock drops right away. A sale may take stock below zero: it is still recorded, and stockWarnings lists each such product with its stockOnHand and the stock after the sale. payment is the pending charge the sale starts with, with a paymentUrl for an online method.
A refused sale throws a RequiplyError with a reason such as sale_item_archived, payment_method_unavailable, or client_email_required. See Error Handling for the full list.
Goods on a booking
pricing.quote() and bookings.create() accept saleLines to sell goods together with a rental:
const quote = await requiply.pricing.quote({
startDate: '2026-07-01T10:00:00',
endDate: '2026-07-03T18:00:00',
items: [{ groupId: cameraKitId, quantity: 1 }],
saleLines: [{ saleItemId: memoryCardId, quantity: 2 }],
});
console.log(quote.saleSubtotal, quote.lineSaleItems[0]?.stockAfter);
- A secret key may sell into negative stock, as staff can in the app. A publishable key cannot: see With a publishable key.
- Goods sell at their catalog price and are never discounted or adjusted. Their total is
saleSubtotal, already insidetotal. lineSaleItemsshows each product withstockOnHandandstockAfter, so you can warn before the stock goes negative.- On
bookings.create(), the stock drops when the booking is created and comes back if the booking is cancelled. The goods total joins the booking'stotalCostand its payment. bookings.get()returns the goods assaleItemsand their sale assaleId.
To change the goods on an existing booking, use Sell items on a booking in the app.