---
title: "Ventas y productos en venta"
description: "Lista los productos en venta y su stock, consulta y registra ventas, y vende productos junto con una reserva a través del SDK."
canonical_url: "https://requiply.com/docs/es/developers/typescript-sdk/sales"
---
# Ventas y productos en venta

> Lista los productos en venta y su stock, consulta y registra ventas, y vende productos junto con una reserva a través del SDK.

Estos recursos cubren el módulo de [Ventas](https://requiply.com/docs/es/guides/sales): productos como tarjetas de memoria, baterías o cinta americana que una empresa de alquiler vende con control de stock, junto a sus alquileres.

<warning>

La organización debe tener el plan Advanced con **Enable sales** (activar ventas) activado en [Funciones](https://requiply.com/docs/es/guides/settings/organization/features#activar-las-ventas). Si no, todos los métodos lanzan un `RequiplyError` con `status` `403` y `reason` `feature_disabled` (`selling_disabled` con una clave publicable).

</warning>

<table>
<thead>
  <tr>
    <th>
      Método
    </th>
    
    <th>
      Clave secreta
    </th>
    
    <th>
      Clave publicable
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        saleItems.list()
      </code>
      
      , <code>
        saleItems.get()
      </code>
    </td>
    
    <td>
      Sí
    </td>
    
    <td>
      Solo los productos activos, mientras la organización vende a los clientes
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        saleLines
      </code>
      
       en <code>
        pricing.quote()
      </code>
      
       y <code>
        bookings.create()
      </code>
    </td>
    
    <td>
      Sí
    </td>
    
    <td>
      Sí, dentro del stock disponible, mientras la organización vende a los clientes
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        sales.list()
      </code>
      
      , <code>
        sales.get()
      </code>
      
      , <code>
        sales.create()
      </code>
    </td>
    
    <td>
      Sí
    </td>
    
    <td>
      No
    </td>
  </tr>
</tbody>
</table>

Los productos en venta se mantienen aparte de los productos de alquiler: `inventory.list()`, la disponibilidad y `categories.list()` nunca los devuelven.

## Con una clave publicable

Una tienda online puede mostrar productos en venta y dejar que los clientes los añadan a una solicitud de reserva, como hace el [portal de clientes](https://requiply.com/docs/es/guides/online-booking/booking-flow) en su paso **Items to buy** (artículos para comprar). Una clave publicable solo llega a ellos mientras la organización **vende a los clientes**, para lo que hacen falta las tres cosas siguientes:

- la función de ventas en el plan Advanced, como se indica arriba;
- **Selling items** (venta de artículos) activado en la [configuración del portal de clientes](https://requiply.com/docs/es/guides/online-booking/portal-settings#acciones);
- al menos un producto en venta que no esté archivado.

Si no, la llamada lanza un `403` con `reason` `selling_disabled`, así que una tienda online puede ocultar su sección de productos.

Con una clave publicable:

- `saleItems.list()` solo devuelve los productos activos, diga lo que diga `filter`. `barcode` y `purchaseCost` son siempre `null`.
- `saleItems.get()` responde `404` para un producto archivado, y `soldUnits30d`, `revenue30d` y `soldUnitsTotal` son `0`.
- `saleLines` no puede pedir más de `stockOnHand`. Una línea que supera el stock lanza un `409` con `reason` `out_of_stock`; `data.saleItemId` indica el producto y `data.availableQuantity`, cuántos quedan (`0` cuando está agotado).

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

Parámetros opcionales: `search` (nombre, SKU o código de barras), `category` (ID de categoría), `filter` (`active` por defecto, `archived` o `all`; las claves publicables siempre reciben `active`), `limit` (de 1 a 100, por defecto 25) y `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>
      Campo
    </th>
    
    <th>
      Qué contiene
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        id
      </code>
      
      , <code>
        name
      </code>
      
      , <code>
        sku
      </code>
      
      , <code>
        barcode
      </code>
    </td>
    
    <td>
      El producto. <code>
        sku
      </code>
      
       y <code>
        barcode
      </code>
      
       pueden ser <code>
        null
      </code>
      
      ; <code>
        barcode
      </code>
      
       siempre es <code>
        null
      </code>
      
       con una clave publicable.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        categoryId
      </code>
      
      , <code>
        categoryName
      </code>
      
      , <code>
        imageUrl
      </code>
    </td>
    
    <td>
      Pueden ser <code>
        null
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        salePrice
      </code>
      
      , <code>
        currency
      </code>
    </td>
    
    <td>
      Precio fijo por unidad en la moneda de la organización.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        stockOnHand
      </code>
    </td>
    
    <td>
      Las unidades en el almacén. Puede ser negativo cuando las ventas han superado el stock registrado.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        purchaseCost
      </code>
    </td>
    
    <td>
      El coste neto por unidad, o <code>
        null
      </code>
      
       salvo que la clave pueda gestionar el stock. Siempre <code>
        null
      </code>
      
       con una clave publicable.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        isArchived
      </code>
    </td>
    
    <td>
      Los productos archivados no se pueden vender.
    </td>
  </tr>
</tbody>
</table>

## `saleItems.get()`

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

Devuelve `saleItem` con los campos de arriba más `soldUnits30d`, `revenue30d` y `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>
```

Lista las ventas, de la más reciente a la más antigua, tanto las independientes como las vendidas con reservas. Filtros opcionales:

<table>
<thead>
  <tr>
    <th>
      Parámetro
    </th>
    
    <th>
      Efecto
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        kind
      </code>
    </td>
    
    <td>
      <code>
        standalone
      </code>
      
       o <code>
        booking
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        status
      </code>
    </td>
    
    <td>
      <code>
        completed
      </code>
      
       o <code>
        cancelled
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        unpaid
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
      
       para las ventas independientes que todavía tienen importe pendiente.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        clientId
      </code>
    </td>
    
    <td>
      Las ventas de un cliente.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        soldFrom
      </code>
      
      , <code>
        soldTo
      </code>
    </td>
    
    <td>
      Fechas y horas ISO 8601.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        search
      </code>
    </td>
    
    <td>
      Número de venta, nombre, email o teléfono del cliente, nombre o SKU del producto.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        limit
      </code>
      
      , <code>
        cursor
      </code>
    </td>
    
    <td>
      El tamaño de página (de 1 a 100, por defecto 25) y el <code>
        nextCursor
      </code>
      
       de la página anterior.
    </td>
  </tr>
</tbody>
</table>

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

Cada venta tiene `id`, `saleOrgId` (el número de venta que ve el personal en la app), `status`, `soldAt`, `total`, `returnedTotal`, `net` (el total menos las devoluciones), `paidAmount`, `balance`, `paymentMethod`, `paidAt`, `note`, el cliente, la reserva a la que pertenece (`reservationId`, `reservationOrgId`), `itemCount` e `itemsLabel`. `balance` es lo que todavía debe una venta independiente; siempre es `0` en una venta de una reserva, que se paga a través de la reserva.

## `sales.get()`

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

`id` es el UUID de la venta, o el número de venta de la app con `idKind: 'org'`.

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

La venta también incluye `lines` (producto, cantidad, `returnedQuantity`, `unitPrice`, `lineTotal`), `returns` y `payments`. En una venta de una reserva, `payments` son los de la reserva, porque la reserva cobra el alquiler y los productos en un único pago.

## `sales.create()`

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

Registra una venta independiente, como [Nueva venta](https://requiply.com/docs/es/guides/sales/new-sale) en la 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>
      Campo
    </th>
    
    <th>
      Reglas
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        lines
      </code>
    </td>
    
    <td>
      Obligatorio. De 1 a 100 productos, cada uno una sola vez, con una cantidad entera mayor que cero. Cada línea se vende al <code>
        salePrice
      </code>
      
       del producto.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        clientId
      </code>
      
       o <code>
        client
      </code>
    </td>
    
    <td>
      Opcional. <code>
        client
      </code>
      
       se busca por email y, después, por teléfono, y se crea cuando no coincide ninguno.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        paymentMethod
      </code>
    </td>
    
    <td>
      <code>
        payOnPickup
      </code>
      
       (por defecto), o <code>
        monobank
      </code>
      
      , <code>
        liqpay
      </code>
      
      , <code>
        wayforpay
      </code>
      
       o <code>
        stripe
      </code>
      
       con una integración activa. <code>
        liqpay
      </code>
      
       necesita un cliente con email.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        soldAt
      </code>
    </td>
    
    <td>
      ISO 8601. Por defecto, el momento actual.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        note
      </code>
    </td>
    
    <td>
      Hasta 2000 caracteres.
    </td>
  </tr>
</tbody>
</table>

El stock baja al momento. Una venta puede dejar el stock por debajo de cero: se registra igualmente, y `stockWarnings` lista cada producto afectado con su `stockOnHand` y el stock `after` (después de) la venta. `payment` es el cobro pendiente con el que empieza la venta, con un `paymentUrl` para un método online.

Una venta rechazada lanza un `RequiplyError` con un `reason` como `sale_item_archived`, `payment_method_unavailable` o `client_email_required`. Consulta [Gestión de errores](https://requiply.com/docs/es/developers/typescript-sdk/error-handling#motivos-de-error) para ver la lista completa.

## Productos en una reserva

`pricing.quote()` y `bookings.create()` aceptan `saleLines` para vender productos junto con un alquiler:

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

- Una clave secreta puede vender dejando el stock en negativo, igual que puede hacerlo el personal en la app. Una clave publicable no: consulta [Con una clave publicable](#con-una-clave-publicable).
- Los productos se venden a su precio de catálogo y nunca se les aplica descuento ni ajuste. Su total es `saleSubtotal`, ya incluido en `total`.
- `lineSaleItems` muestra cada producto con `stockOnHand` y `stockAfter`, para que puedas avisar antes de que el stock quede en negativo.
- En `bookings.create()`, el stock baja al crear la reserva y se recupera si la reserva se cancela. El total de los productos se suma al `totalCost` de la reserva y a su pago.
- `bookings.get()` devuelve los productos como `saleItems` y su venta como `saleId`.

Para cambiar los productos de una reserva existente, usa [Vender artículos en una reserva](https://requiply.com/docs/es/guides/bookings/sell-items) en la app.


## Sitemap

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