Requiply
TypeScript SDK

Sales and Goods for Sale

List goods for sale and their stock, read and record sales, and sell goods together with a booking through the SDK.

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.

The organization must be on the Advanced plan with Enable sales on under Features. Otherwise every method throws a RequiplyError with status403 and reasonfeature_disabled (selling_disabled for a publishable key).
MethodSecret keyPublishable key
saleItems.list(), saleItems.get()YesActive products only, while the organization sells to customers
saleLines on pricing.quote() and bookings.create()YesYes, within the stock on hand, while the organization sells to customers
sales.list(), sales.get(), sales.create()YesNo

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, whatever filter says. barcode and purchaseCost are always null.
  • saleItems.get() answers 404 for an archived product, and soldUnits30d, revenue30d, and soldUnitsTotal are 0.
  • saleLines cannot ask for more than stockOnHand. A line over the stock throws 409 with reason out_of_stock; data.saleItemId names the product and data.availableQuantity how many are left (0 when 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);
}
FieldWhat it holds
id, name, sku, barcodeThe product. sku and barcode can be null; barcode is always null for a publishable key.
categoryId, categoryName, imageUrlCan be null.
salePrice, currencyFlat price per unit in the organization currency.
stockOnHandUnits on the shelf. It can be negative when sales outran the recorded stock.
purchaseCostNet cost per unit, or null unless the key may manage stock. Always null for a publishable key.
isArchivedArchived 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:

ParameterEffect
kindstandalone or booking.
statuscompleted or cancelled.
unpaidtrue for standalone sales that still owe money.
clientIdOne client's sales.
soldFrom, soldToISO 8601 date-times.
searchSale number, client name, email, or phone, product name or SKU.
limit, cursorPage 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);
FieldRules
linesRequired. 1 to 100 products, each at most once, with a whole quantity above zero. Every line sells at the product's salePrice.
clientId or clientOptional. client is matched by email, then phone, and created when neither matches.
paymentMethodpayOnPickup (default), or monobank, liqpay, wayforpay, or stripe with an active integration. liqpay needs a client with an email.
soldAtISO 8601. Defaults to now.
noteUp 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 inside total.
  • lineSaleItems shows each product with stockOnHand and stockAfter, 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's totalCost and its payment.
  • bookings.get() returns the goods as saleItems and their sale as saleId.

To change the goods on an existing booking, use Sell items on a booking in the app.

Copyright © 2026