---
title: "Продажі та товари на продаж"
description: "Отримуйте список товарів на продаж і їхніх залишків, читайте й записуйте продажі та продавайте товари разом із бронюванням через SDK."
canonical_url: "https://requiply.com/docs/uk/developers/typescript-sdk/sales"
---
# Продажі та товари на продаж

> Отримуйте список товарів на продаж і їхніх залишків, читайте й записуйте продажі та продавайте товари разом із бронюванням через SDK.

Ці ресурси охоплюють модуль [Продажі](https://requiply.com/docs/uk/guides/sales): товари на кшталт карт памʼяті, батарей чи гаферного скотчу, які орендний бізнес продає з обліком залишку поруч з орендою.

<warning>

Організація має бути на тарифі Advanced з увімкненим **Увімкнути продажі** у [Функції](https://requiply.com/docs/uk/guides/settings/organization/features#%D1%83%D0%B2%D1%96%D0%BC%D0%BA%D0%BD%D1%83%D1%82%D0%B8-%D0%BF%D1%80%D0%BE%D0%B4%D0%B0%D0%B6%D1%96). Інакше кожен метод викидає `RequiplyError` зі `status` `403` і `reason` `feature_disabled` (`selling_disabled` для публічного ключа).

</warning>

<table>
<thead>
  <tr>
    <th>
      Метод
    </th>
    
    <th>
      Секретний ключ
    </th>
    
    <th>
      Публічний ключ
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        saleItems.list()
      </code>
      
      , <code>
        saleItems.get()
      </code>
    </td>
    
    <td>
      Так
    </td>
    
    <td>
      Лише активні товари, поки організація продає клієнтам
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        saleLines
      </code>
      
       у <code>
        pricing.quote()
      </code>
      
       і <code>
        bookings.create()
      </code>
    </td>
    
    <td>
      Так
    </td>
    
    <td>
      Так, у межах наявного залишку, поки організація продає клієнтам
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        sales.list()
      </code>
      
      , <code>
        sales.get()
      </code>
      
      , <code>
        sales.create()
      </code>
    </td>
    
    <td>
      Так
    </td>
    
    <td>
      Ні
    </td>
  </tr>
</tbody>
</table>

Товари на продаж зберігаються окремо від товарів для оренди: `inventory.list()`, доступність і `categories.list()` ніколи їх не повертають.

## З публічним ключем

Вітрина може показувати товари на продаж і давати клієнтам додавати їх до запиту на бронювання так само, як це робить [клієнтський портал](https://requiply.com/docs/uk/guides/online-booking/booking-flow) на кроці **Додати товари для покупки**. Публічний ключ отримує до них доступ, лише поки організація **продає клієнтам**, для чого потрібні всі три умови:

- функція «Продажі» на тарифі Advanced, як описано вище;
- увімкнений **Продаж товарів** у [налаштуваннях клієнтського порталу](https://requiply.com/docs/uk/guides/online-booking/portal-settings#%D0%B4%D1%96%D1%97);
- щонайменше один товар на продаж, який не архівовано.

Інакше виклик викидає `403` з `reason` `selling_disabled`, тож вітрина може приховати розділ товарів.

З публічним ключем:

- `saleItems.list()` повертає лише активні товари, незалежно від `filter`. `barcode` і `purchaseCost` завжди `null`.
- `saleItems.get()` повертає `404` для архівованого товару, а `soldUnits30d`, `revenue30d`, `soldUnitsTotal` дорівнюють `0`.
- `saleLines` не може запитувати більше за `stockOnHand`. Рядок понад залишок викидає `409` з `reason` `out_of_stock`; `data.saleItemId` називає товар, а `data.availableQuantity` показує, скільки залишилося (`0`, коли все продано).

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

Необовʼязкові параметри: `search` (назва, артикул чи штрихкод), `category` (ідентифікатор категорії), `filter` (за замовчуванням `active`, також `archived` або `all`; публічні ключі завжди отримують `active`), `limit` (від 1 до 100, за замовчуванням 25), `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>
      Поле
    </th>
    
    <th>
      Що містить
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        id
      </code>
      
      , <code>
        name
      </code>
      
      , <code>
        sku
      </code>
      
      , <code>
        barcode
      </code>
    </td>
    
    <td>
      Товар. <code>
        sku
      </code>
      
       і <code>
        barcode
      </code>
      
       можуть бути <code>
        null
      </code>
      
      ; для публічного ключа <code>
        barcode
      </code>
      
       завжди <code>
        null
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        categoryId
      </code>
      
      , <code>
        categoryName
      </code>
      
      , <code>
        imageUrl
      </code>
    </td>
    
    <td>
      Можуть бути <code>
        null
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        salePrice
      </code>
      
      , <code>
        currency
      </code>
    </td>
    
    <td>
      Фіксована ціна за одиницю у валюті організації.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        stockOnHand
      </code>
    </td>
    
    <td>
      Одиниці на полиці. Значення може бути відʼємним, коли продажі перевищили облікований залишок.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        purchaseCost
      </code>
    </td>
    
    <td>
      Чиста собівартість одиниці або <code>
        null
      </code>
      
      , якщо ключ не може керувати залишками. Для публічного ключа завжди <code>
        null
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        isArchived
      </code>
    </td>
    
    <td>
      Архівовані товари продати не можна.
    </td>
  </tr>
</tbody>
</table>

## `saleItems.get()`

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

Повертає `saleItem` з наведеними вище полями, а також полями `soldUnits30d`, `revenue30d`, `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>
```

Перелічує продажі від найновіших, як окремі, так і повʼязані з бронюваннями. Необовʼязкові фільтри:

<table>
<thead>
  <tr>
    <th>
      Параметр
    </th>
    
    <th>
      Дія
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        kind
      </code>
    </td>
    
    <td>
      <code>
        standalone
      </code>
      
       або <code>
        booking
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        status
      </code>
    </td>
    
    <td>
      <code>
        completed
      </code>
      
       або <code>
        cancelled
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        unpaid
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
      
       для окремих продажів, за якими ще є заборгованість.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        clientId
      </code>
    </td>
    
    <td>
      Продажі одного клієнта.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        soldFrom
      </code>
      
      , <code>
        soldTo
      </code>
    </td>
    
    <td>
      Дата й час у форматі ISO 8601.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        search
      </code>
    </td>
    
    <td>
      Номер продажу, імʼя клієнта, email чи телефон, назва товару чи артикул.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        limit
      </code>
      
      , <code>
        cursor
      </code>
    </td>
    
    <td>
      Розмір сторінки (від 1 до 100, за замовчуванням 25) і <code>
        nextCursor
      </code>
      
       попередньої сторінки.
    </td>
  </tr>
</tbody>
</table>

```ts
const { sales, currency } = await requiply.sales.list({ kind: 'standalone', unpaid: true });
```

Кожен продаж має такі поля: `id`; `saleOrgId` (номер продажу, який співробітники бачать у застосунку); `status`; `soldAt`; `total`; `returnedTotal`; `net` (сума мінус повернення); `paidAmount`; `balance`; `paymentMethod`; `paidAt`; `note`; клієнта; бронювання, до якого він належить (`reservationId`, `reservationOrgId`); `itemCount`; `itemsLabel`. `balance` показує, скільки ще винен окремий продаж. Для продажу в бронюванні це значення завжди `0`, бо його оплачують через бронювання.

## `sales.get()`

```ts
sales.get(
  id: string | number,
  params?: { idKind?: 'internal' | 'org' },
  options?: RequiplyRequestOptions,
): Promise<SaleResponse>
```

`id` є UUID продажу або номером продажу із застосунку з `idKind: 'org'`.

```ts
const { sale } = await requiply.sales.get(12, { idKind: 'org' });
```

Продаж також містить `lines` (товар, кількість, `returnedQuantity`, `unitPrice`, `lineTotal`), а також `returns` і `payments`. Для продажу в бронюванні `payments` належать бронюванню, бо бронювання стягує оренду й товари одним платежем.

## `sales.create()`

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

Записує окремий продаж, як [Новий продаж](https://requiply.com/docs/uk/guides/sales/new-sale) у застосунку.

```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>
      Поле
    </th>
    
    <th>
      Правила
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        lines
      </code>
    </td>
    
    <td>
      Обовʼязкове. Від 1 до 100 товарів, кожен щонайбільше один раз, із цілою кількістю, більшою за нуль. Кожен рядок продається за <code>
        salePrice
      </code>
      
       товару.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        clientId
      </code>
      
       або <code>
        client
      </code>
    </td>
    
    <td>
      Необовʼязкове. <code>
        client
      </code>
      
       зіставляється за email, потім за телефоном, а якщо збігу немає, створюється новий.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        paymentMethod
      </code>
    </td>
    
    <td>
      <code>
        payOnPickup
      </code>
      
       (за замовчуванням) або один з онлайн-способів (<code>
        monobank
      </code>
      
      , <code>
        liqpay
      </code>
      
      , <code>
        wayforpay
      </code>
      
      , <code>
        stripe
      </code>
      
      ) з активною інтеграцією. <code>
        liqpay
      </code>
      
       потребує клієнта з email.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        soldAt
      </code>
    </td>
    
    <td>
      ISO 8601. За замовчуванням поточний момент.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        note
      </code>
    </td>
    
    <td>
      До 2000 символів.
    </td>
  </tr>
</tbody>
</table>

Залишок зменшується одразу. Продаж може опустити залишок нижче нуля: його все одно буде записано, а `stockWarnings` перелічує кожен такий товар із його `stockOnHand` і залишком `after` після продажу. `payment` є очікуваним платежем, з якого починається продаж, із `paymentUrl` для онлайн-способу оплати.

Відхилений продаж викидає `RequiplyError` з `reason` на кшталт `sale_item_archived`, `payment_method_unavailable` чи `client_email_required`. Повний перелік див. в [Обробка помилок](https://requiply.com/docs/uk/developers/typescript-sdk/error-handling#%D0%BF%D1%80%D0%B8%D1%87%D0%B8%D0%BD%D0%B8-%D0%BF%D0%BE%D0%BC%D0%B8%D0%BB%D0%BE%D0%BA).

## Товари в бронюванні

`pricing.quote()` і `bookings.create()` приймають `saleLines`, щоб продати товари разом з орендою:

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

- Секретний ключ може продавати у відʼємний залишок, як це можуть робити співробітники в застосунку. Публічний ключ не може: див. [З публічним ключем](#%D0%B7-%D0%BF%D1%83%D0%B1%D0%BB%D1%96%D1%87%D0%BD%D0%B8%D0%BC-%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%BC).
- Товари продаються за каталожною ціною, знижки та коригування до них ніколи не застосовуються. Їхня сума є `saleSubtotal` і вже входить у `total`.
- `lineSaleItems` показує кожен товар зі `stockOnHand` і `stockAfter`, тож можна попередити, перш ніж залишок стане відʼємним.
- У `bookings.create()` залишок зменшується під час створення бронювання і повертається, якщо бронювання скасують. Сума товарів додається до `totalCost` бронювання та його оплати.
- `bookings.get()` повертає товари як `saleItems`, а їхній продаж як `saleId`.

Щоб змінити товари в наявному бронюванні, скористайтеся [Продажем товарів у бронюванні](https://requiply.com/docs/uk/guides/bookings/sell-items) в застосунку.


## Sitemap

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