---
title: "Sales and Goods for Sale"
description: "List goods for sale and their stock, read and record sales, and sell goods together with a booking through the SDK."
canonical_url: "https://requiply.com/docs/en/developers/typescript-sdk/sales"
---
# 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](https://requiply.com/docs/en/guides/sales) module: goods such as memory cards, batteries, or gaffer tape that a rental business sells with a stock count, next to its rentals.

<warning>

The organization must be on the Advanced plan with **Enable sales** on under [Features](https://requiply.com/docs/en/guides/settings/organization/features#enable-sales). Otherwise every method throws a `RequiplyError` with `status` `403` and `reason` `feature_disabled` (`selling_disabled` for a publishable key).

</warning>

<table>
<thead>
  <tr>
    <th>
      Method
    </th>
    
    <th>
      Secret key
    </th>
    
    <th>
      Publishable key
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        saleItems.list()
      </code>
      
      , <code>
        saleItems.get()
      </code>
    </td>
    
    <td>
      Yes
    </td>
    
    <td>
      Active products only, while the organization sells to customers
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        saleLines
      </code>
      
       on <code>
        pricing.quote()
      </code>
      
       and <code>
        bookings.create()
      </code>
    </td>
    
    <td>
      Yes
    </td>
    
    <td>
      Yes, within the stock on hand, while the organization sells to customers
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        sales.list()
      </code>
      
      , <code>
        sales.get()
      </code>
      
      , <code>
        sales.create()
      </code>
    </td>
    
    <td>
      Yes
    </td>
    
    <td>
      No
    </td>
  </tr>
</tbody>
</table>

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](https://requiply.com/docs/en/guides/online-booking/booking-flow) 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](https://requiply.com/docs/en/guides/online-booking/portal-settings#actions);
- 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).

```ts
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()`

```ts
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`.

```ts
const { items, nextCursor } = await requiply.saleItems.list({ search: 'battery' });

for (const item of items) {
  console.log(item.name, item.salePrice, item.currency, item.stockOnHand);
}
```

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      What it holds
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        id
      </code>
      
      , <code>
        name
      </code>
      
      , <code>
        sku
      </code>
      
      , <code>
        barcode
      </code>
    </td>
    
    <td>
      The product. <code>
        sku
      </code>
      
       and <code>
        barcode
      </code>
      
       can be <code>
        null
      </code>
      
      ; <code>
        barcode
      </code>
      
       is always <code>
        null
      </code>
      
       for a publishable key.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        categoryId
      </code>
      
      , <code>
        categoryName
      </code>
      
      , <code>
        imageUrl
      </code>
    </td>
    
    <td>
      Can be <code>
        null
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        salePrice
      </code>
      
      , <code>
        currency
      </code>
    </td>
    
    <td>
      Flat price per unit in the organization currency.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        stockOnHand
      </code>
    </td>
    
    <td>
      Units on the shelf. It can be negative when sales outran the recorded stock.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        purchaseCost
      </code>
    </td>
    
    <td>
      Net cost per unit, or <code>
        null
      </code>
      
       unless the key may manage stock. Always <code>
        null
      </code>
      
       for a publishable key.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        isArchived
      </code>
    </td>
    
    <td>
      Archived products cannot be sold.
    </td>
  </tr>
</tbody>
</table>

## `saleItems.get()`

```ts
saleItems.get(
  saleItemId: string,
  options?: RequiplyRequestOptions,
): Promise<SaleItemResponse>
```

Returns `saleItem` with the fields above plus `soldUnits30d`, `revenue30d`, and `soldUnitsTotal`.

```ts
const { saleItem } = await requiply.saleItems.get(itemId);
console.log(saleItem.soldUnits30d, saleItem.revenue30d);
```

## `sales.list()`

```ts
sales.list(
  params?: SalesListParams,
  options?: RequiplyRequestOptions,
): Promise<SalesListResponse>
```

Lists sales, newest first, both standalone and sold with bookings. Optional filters:

<table>
<thead>
  <tr>
    <th>
      Parameter
    </th>
    
    <th>
      Effect
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        kind
      </code>
    </td>
    
    <td>
      <code>
        standalone
      </code>
      
       or <code>
        booking
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        status
      </code>
    </td>
    
    <td>
      <code>
        completed
      </code>
      
       or <code>
        cancelled
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        unpaid
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
      
       for standalone sales that still owe money.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        clientId
      </code>
    </td>
    
    <td>
      One client's sales.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        soldFrom
      </code>
      
      , <code>
        soldTo
      </code>
    </td>
    
    <td>
      ISO 8601 date-times.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        search
      </code>
    </td>
    
    <td>
      Sale number, client name, email, or phone, product name or SKU.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        limit
      </code>
      
      , <code>
        cursor
      </code>
    </td>
    
    <td>
      Page size (1 to 100, default 25) and the <code>
        nextCursor
      </code>
      
       of the previous page.
    </td>
  </tr>
</tbody>
</table>

```ts
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()`

```ts
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'`.

```ts
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()`

```ts
sales.create(
  input: SaleCreateInput,
  options?: RequiplyRequestOptions,
): Promise<SaleCreateResponse>
```

Records a standalone sale, like [New sale](https://requiply.com/docs/en/guides/sales/new-sale) in the app.

```ts
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);
```

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Rules
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        lines
      </code>
    </td>
    
    <td>
      Required. 1 to 100 products, each at most once, with a whole quantity above zero. Every line sells at the product's <code>
        salePrice
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        clientId
      </code>
      
       or <code>
        client
      </code>
    </td>
    
    <td>
      Optional. <code>
        client
      </code>
      
       is matched by email, then phone, and created when neither matches.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        paymentMethod
      </code>
    </td>
    
    <td>
      <code>
        payOnPickup
      </code>
      
       (default), or <code>
        monobank
      </code>
      
      , <code>
        liqpay
      </code>
      
      , <code>
        wayforpay
      </code>
      
      , or <code>
        stripe
      </code>
      
       with an active integration. <code>
        liqpay
      </code>
      
       needs a client with an email.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        soldAt
      </code>
    </td>
    
    <td>
      ISO 8601. Defaults to now.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        note
      </code>
    </td>
    
    <td>
      Up to 2000 characters.
    </td>
  </tr>
</tbody>
</table>

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](https://requiply.com/docs/en/developers/typescript-sdk/error-handling#error-reasons) for the full list.

## Goods on a booking

`pricing.quote()` and `bookings.create()` accept `saleLines` to sell goods together with a rental:

```ts
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](#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](https://requiply.com/docs/en/guides/bookings/sell-items) in the app.


## Sitemap

See the full [sitemap](https://requiply.com/docs/sitemap.md) for all pages.
