> ## Documentation Index
> Fetch the complete documentation index at: https://docs.jelou.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Crear y actualizar productos

> Crea y actualiza productos con soporte para sucursales, variaciones y modificadores.

Puedes crear o actualizar productos en lote según su SKU. Si un producto con el SKU especificado ya existe, se actualiza; si no, se crea. Los productos pueden asignarse a una [sucursal](/guides/integraciones/e-commerce/api-sucursales/crear), tener múltiples variaciones y grupos de modificadores.

<ParamField path="app_id" type="string" required>
  ID de tu tienda en Jelou Shop.
</ParamField>

<ParamField body="resources" type="object[]" required>
  Lista de productos a crear o actualizar (máx. 500 por solicitud).
</ParamField>

## Campos del producto

Cada objeto dentro de `resources` acepta los siguientes campos:

<ParamField body="sku" type="string" required>
  Identificador único del producto (máx. 255 caracteres).
</ParamField>

<ParamField body="name" type="string" required>
  Nombre del producto (máx. 255 caracteres).
</ParamField>

<ParamField body="price" type="number" required>
  Precio del producto (mín. 0).
</ParamField>

<ParamField body="description" type="string">
  Descripción del producto.
</ParamField>

<ParamField body="has_tax" type="boolean" default="true">
  Indica si el precio incluye impuestos.
</ParamField>

<ParamField body="tax" type="number">
  Tasa de impuesto individual del producto (entre `0` y `100`). Ejemplo: `15` para 15%, `12` para 12%. Solo aplica cuando `enable_per_product_tax` está activo en la [configuración de la tienda](/guides/integraciones/e-commerce/api-configuracion/tienda). Si no se envía o es `0`, se usa la tasa global de la tienda.
</ParamField>

<ParamField body="status" type="boolean" default="true">
  Estado del producto (activo/inactivo).
</ParamField>

<ParamField body="stock_type" type="string" default="unlimited">
  Tipo de inventario: `limited` o `unlimited`.
</ParamField>

<ParamField body="stock" type="number">
  Cantidad disponible. Aplica solo cuando `stock_type` es `limited`.
</ParamField>

<ParamField body="product_url" type="string">
  URL del producto en tu tienda (máx. 2048 caracteres).
</ParamField>

<ParamField body="discount_type" type="string">
  Tipo de descuento: `value` (monto fijo) o `percentage`.
</ParamField>

<ParamField body="discount" type="number">
  Valor del descuento (mín. 0).
</ParamField>

<ParamField body="categories" type="string[]">
  Lista de nombres de categorías. Se crean automáticamente si no existen.
</ParamField>

<ParamField body="images" type="string[]">
  Lista de URLs de imágenes públicas del producto.
</ParamField>

<ParamField body="note_enabled" type="boolean" default="false">
  Habilita un campo de nota en el detalle del producto, permitiendo al cliente agregar un comentario al momento de añadirlo al carrito (ej: "Sin cebolla", "Envolver para regalo").
</ParamField>

<ParamField body="note_label" type="string">
  Texto placeholder que se muestra en el campo de nota (máx. 255 caracteres). Si no se especifica, se usa un texto genérico por defecto.
</ParamField>

<ParamField body="branch" type="string">
  Código de la sucursal a la que se asigna el producto. La sucursal debe existir previamente.
</ParamField>

<Warning>
  La sucursal debe estar creada antes de asignarla a un producto. Usa el endpoint [Crear sucursales](/guides/integraciones/e-commerce/api-sucursales/crear) para registrarla primero.
</Warning>

<ParamField body="features" type="object[]">
  Lista de características (ficha técnica) del producto.

  <Expandable title="Campos de cada característica">
    <ParamField body="label" type="string" required>
      Nombre de la característica (máx. 255 caracteres). Ej: `Material`, `Peso`.
    </ParamField>

    <ParamField body="value" type="string" required>
      Valor de la característica (máx. 200 caracteres). Ej: `Algodón`, `500g`.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="replace_variations" type="boolean" default="false">
  Controla cómo se sincronizan las variaciones. Con `false` (por defecto), las variaciones se actualizan o crean por SKU y se **conservan** las existentes que no vengan en el payload. Con `true`, se **eliminan** las variaciones que no estén presentes en el payload.
</ParamField>

<ParamField body="variations" type="object[]">
  Lista de variaciones del producto.

  <Expandable title="Campos de variación">
    <ParamField body="sku" type="string" required>
      SKU único de la variación (máx. 255 caracteres). Si ya existe una variación con este SKU, se actualiza.
    </ParamField>

    <ParamField body="price" type="number" required>
      Precio de la variación (mín. 0).
    </ParamField>

    <ParamField body="stock_type" type="string" default="unlimited">
      Tipo de inventario de la variación: `limited` o `unlimited`. Permite manejar el stock de cada variación de forma independiente al producto padre.
    </ParamField>

    <ParamField body="stock" type="number">
      Cantidad disponible de la variación. Aplica solo cuando `stock_type` es `limited`.
    </ParamField>

    <ParamField body="properties" type="object">
      Propiedades adicionales en formato JSON.
    </ParamField>

    <ParamField body="images" type="string[]">
      Lista de URLs de imágenes de la variación.
    </ParamField>

    <ParamField body="attributes" type="object[]">
      Lista de atributos de la variación.

      <Expandable title="Campos de atributo">
        <ParamField body="name" type="string" required>
          Nombre del atributo — por ejemplo: `Talla`, `Color` (máx. 255 caracteres).
        </ParamField>

        <ParamField body="value" type="string" required>
          Valor del atributo — por ejemplo: `M`, `Azul` (máx. 255 caracteres).
        </ParamField>
      </Expandable>
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="modifier_groups" type="object[]">
  Grupos de modificadores (add-ons) del producto. Comunes en delivery de comida (ej: "Escoja las Bebidas", "Elige las salsas"). Si el campo está ausente, se preservan los modificadores existentes. Si es un array vacío `[]`, se eliminan todos. Si tiene datos, se reemplazan completamente.

  <Expandable title="Campos del grupo de modificadores">
    <ParamField body="code" type="string" required>
      Código único del grupo (máx. 255 caracteres).
    </ParamField>

    <ParamField body="name" type="string" required>
      Nombre visible del grupo (máx. 255 caracteres).
    </ParamField>

    <ParamField body="min_quantity" type="integer" default="0">
      Mínimo de opciones que el cliente debe seleccionar.
    </ParamField>

    <ParamField body="max_quantity" type="integer" default="1">
      Máximo de opciones que el cliente puede seleccionar (mín. 1).
    </ParamField>

    <ParamField body="max_per_option" type="integer">
      Máximo de veces que se puede elegir una misma opción (mín. 1).
    </ParamField>

    <ParamField body="is_required" type="boolean" default="false">
      Si el grupo es obligatorio para agregar al carrito.
    </ParamField>

    <ParamField body="position" type="integer">
      Posición de ordenamiento del grupo (se asigna automáticamente si se omite).
    </ParamField>

    <ParamField body="options" type="object[]" required>
      Lista de opciones del grupo (mínimo 1 opción).

      <Expandable title="Campos de opción">
        <ParamField body="code" type="string" required>
          Código único de la opción (máx. 255 caracteres).
        </ParamField>

        <ParamField body="name" type="string" required>
          Nombre visible de la opción (máx. 255 caracteres).
        </ParamField>

        <ParamField body="price" type="number" default="0">
          Precio adicional de la opción (mín. 0).
        </ParamField>

        <ParamField body="image_url" type="string">
          URL de la imagen de la opción (debe ser URL pública).
        </ParamField>

        <ParamField body="position" type="integer">
          Posición de ordenamiento (se asigna automáticamente si se omite).
        </ParamField>

        <ParamField body="status" type="boolean" default="true">
          Si la opción está disponible.
        </ParamField>

        <ParamField body="linked_product_sku" type="string">
          SKU de un producto vinculado. Se resuelve a `linked_product_id` (UUID). Si el SKU no se encuentra, se almacena como `null`.
        </ParamField>
      </Expandable>
    </ParamField>
  </Expandable>
</ParamField>

<Info>
  Los modificadores se almacenan en el producto y se devuelven automáticamente en todos los endpoints que retornan productos.
</Info>

## Ejemplo completo

Este ejemplo crea tres productos: una camiseta con variaciones de talla, un pantalón simple y una pizza con grupos de modificadores.

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST "https://gateway.jelou.ai/ecommerce/v2/apps/{app_id}/batch/products/upsert_by_sku" \
    -H "x-api-key: TU_CLAVE_API" \
    -H "Content-Type: application/json" \
    -d '{
      "resources": [
        {
          "sku": "CAM-BLU",
          "name": "Camiseta Básica Azul",
          "description": "Camiseta de algodón 100% premium, corte regular",
          "price": 29.99,
          "has_tax": true,
          "tax": 15,
          "status": true,
          "stock_type": "limited",
          "stock": 150,
          "product_url": "https://mi-tienda.com/camiseta-basica-azul",
          "discount_type": "percentage",
          "discount": 10,
          "branch": "SUC-CENTRO",
          "categories": ["Ropa", "Camisetas"],
          "images": [
            "https://mi-tienda.com/images/camiseta-azul-front.jpg",
            "https://mi-tienda.com/images/camiseta-azul-back.jpg"
          ],
          "variations": [
            {
              "sku": "CAM-BLU-S",
              "price": 29.99,
              "stock_type": "limited",
              "stock": 50,
              "attributes": [
                { "name": "Talla", "value": "S" },
                { "name": "Color", "value": "Azul" }
              ]
            },
            {
              "sku": "CAM-BLU-M",
              "price": 29.99,
              "stock_type": "limited",
              "stock": 60,
              "attributes": [
                { "name": "Talla", "value": "M" },
                { "name": "Color", "value": "Azul" }
              ]
            },
            {
              "sku": "CAM-BLU-L",
              "price": 31.99,
              "stock_type": "limited",
              "stock": 40,
              "attributes": [
                { "name": "Talla", "value": "L" },
                { "name": "Color", "value": "Azul" }
              ],
              "images": [
                "https://mi-tienda.com/images/camiseta-azul-L.jpg"
              ]
            }
          ]
        },
        {
          "sku": "PAN-NEG-M",
          "name": "Pantalón Negro",
          "price": 49.99,
          "branch": "SUC-NORTE",
          "categories": ["Ropa", "Pantalones"],
          "note_enabled": true,
          "note_label": "¿Alguna instrucción especial?"
        },
        {
          "sku": "PIZZA-FAMILIAR",
          "name": "Pizza Familiar Pepperoni",
          "description": "Pizza familiar de pepperoni con masa artesanal",
          "price": 18.99,
          "stock_type": "unlimited",
          "branch": "SUC-NORTE",
          "categories": ["Pizzas", "Promociones"],
          "images": ["https://ejemplo.com/pizza-pepperoni.jpg"],
          "modifier_groups": [
            {
              "code": "bebidas",
              "name": "Escoja su Bebida",
              "min_quantity": 1,
              "max_quantity": 1,
              "is_required": true,
              "options": [
                { "code": "coca-1l", "name": "Coca Cola 1L", "price": 0 },
                { "code": "sprite-1l", "name": "Sprite 1L", "price": 0 },
                { "code": "fanta-1l", "name": "Fanta 1L", "price": 0.50 }
              ]
            },
            {
              "code": "extras",
              "name": "Ingredientes Extra",
              "min_quantity": 0,
              "max_quantity": 5,
              "max_per_option": 2,
              "is_required": false,
              "options": [
                { "code": "queso-extra", "name": "Queso Extra", "price": 1.50 },
                { "code": "jamon", "name": "Jamón", "price": 2.00 },
                { "code": "aceitunas", "name": "Aceitunas", "price": 1.00 }
              ]
            }
          ]
        }
      ]
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json 202 Accepted theme={null}
  {
    "message": "Batch upsert process initiated successfully",
    "count": 3,
    "jobs": 3
  }
  ```

  ```json 422 Validation Error theme={null}
  {
    "message": "The resources field is required.",
    "errors": {
      "resources": ["The resources field is required."]
    }
  }
  ```
</ResponseExample>

<Note>
  Reemplaza `{app_id}` con el ID de tu tienda y `TU_CLAVE_API` con tu [clave API](/guides/configuracion/claves-api).
</Note>

## Comportamiento

<AccordionGroup>
  <Accordion title="Procesamiento asíncrono (por defecto)">
    El endpoint retorna `202 Accepted` inmediatamente. Los productos se procesan en segundo plano.
  </Accordion>

  <Accordion title="Procesamiento síncrono (X-Sync)">
    Envía el header `X-Sync: true` para procesar de forma síncrona y recibir el resultado por producto en la misma respuesta (`200 OK`). En este modo el máximo es de **50 productos** por solicitud.

    ```json 200 OK theme={null}
    {
      "message": "Batch upsert processed synchronously",
      "count": 2,
      "succeeded": 1,
      "failed_count": 1,
      "failed": [
        { "sku": "CAM-BLU", "status": "failed", "error": "..." }
      ],
      "results": [
        { "sku": "PAN-NEG-M", "status": "success" },
        { "sku": "CAM-BLU", "status": "failed", "error": "..." }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Sucursal inexistente">
    Si el código de sucursal no coincide con ninguna sucursal de la tienda, el producto se crea sin sucursal asignada. No produce error.
  </Accordion>

  <Accordion title="Categorías automáticas">
    Si una categoría no existe, se crea automáticamente dentro de la tienda y la sucursal correspondiente.
  </Accordion>

  <Accordion title="Imágenes asíncronas">
    Las imágenes se descargan y procesan en segundo plano después de la creación del producto.
  </Accordion>

  <Accordion title="Variaciones por SKU">
    Las variaciones se identifican por su SKU. Si ya existe una variación con ese SKU, se actualiza en lugar de crear una nueva. Por defecto, las variaciones existentes que no vengan en el payload se conservan; envía `replace_variations: true` para eliminarlas.
  </Accordion>

  <Accordion title="Modificadores">
    Si `modifier_groups` está ausente en el payload, se preservan los modificadores existentes. Si es un array vacío `[]`, se eliminan todos. Si tiene datos, se reemplazan completamente.
  </Accordion>
</AccordionGroup>

## Errores de validación

Si los datos no cumplen las reglas de validación, la API responde con `422` y detalla los campos con error.

```json theme={null}
{
  "message": "The resources field is required.",
  "errors": {
    "resources": ["The resources field is required."]
  }
}
```

<Accordion title="Mensajes de error comunes">
  | Campo                                          | Mensaje                                                               |
  | ---------------------------------------------- | --------------------------------------------------------------------- |
  | `resources`                                    | At least one product is required.                                     |
  | `resources`                                    | Cannot process more than 500 products at once.                        |
  | `resources.*.sku`                              | Each product must have a SKU.                                         |
  | `resources.*.name`                             | Each product must have a name.                                        |
  | `resources.*.price`                            | Each product must have a price. / Price cannot be negative.           |
  | `resources.*.stock_type`                       | Stock type must be either limited or unlimited.                       |
  | `resources.*.images.*`                         | Each image must be a valid URL.                                       |
  | `resources.*.variations.*.stock_type`          | Variation stock type must be either limited or unlimited.             |
  | `resources.*.variations.*.stock`               | Variation stock must be an integer. / Cannot be negative.             |
  | `resources.*.modifier_groups.*.code`           | Each modifier group must have a code.                                 |
  | `resources.*.modifier_groups.*.name`           | Each modifier group must have a name.                                 |
  | `resources.*.modifier_groups.*.options`        | Each modifier group must have at least one option.                    |
  | `resources.*.modifier_groups.*.options.*.code` | Each modifier option must have a code.                                |
  | `resources.*.modifier_groups.*.options.*.name` | Each modifier option must have a name.                                |
  | `resources.*.tax`                              | Tax rate must be a number. / cannot be negative. / cannot exceed 100. |
  | `resources.*.replace_variations`               | Replace variations flag must be true or false.                        |
  | `resources.*.features.*.label`                 | Each feature must have a label. / cannot exceed 255 characters.       |
  | `resources.*.features.*.value`                 | Each feature must have a value. / cannot exceed 200 characters.       |
</Accordion>

<Note>
  Los mensajes de la columna **Mensaje** los devuelve la API en inglés (no están localizados), por lo que se muestran aquí tal cual responde la API.
</Note>

## Límites

* Máximo **500 productos** por solicitud en modo asíncrono (por defecto), o **50** en modo síncrono (`X-Sync: true`).
* Máximo **10,000 productos** por ventana de **60 segundos** por tienda (rate limit). Si se excede, la API responde con `429 Too Many Requests`.
* Todos los productos se validan antes de procesarse.
