> ## 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.

# Products

> Create and update products with support for branches, variations, and modifiers.

You can create or update products in bulk by their SKU. If a product with the specified SKU already exists, it is updated; otherwise, it is created. Products can be assigned to a [branch](/guides/integraciones/e-commerce/api-sucursales/crear), have multiple variations, and modifier groups.

<ParamField path="app_id" type="string" required>
  Your store ID in Jelou Shop.
</ParamField>

<ParamField body="resources" type="object[]" required>
  List of products to create or update (max. 500 per request).
</ParamField>

## Product fields

Each object within `resources` accepts the following fields:

<ParamField body="sku" type="string" required>
  Unique product identifier (max. 255 characters).
</ParamField>

<ParamField body="name" type="string" required>
  Product name (max. 255 characters).
</ParamField>

<ParamField body="price" type="number" required>
  Product price (min. 0).
</ParamField>

<ParamField body="description" type="string">
  Product description.
</ParamField>

<ParamField body="has_tax" type="boolean" default="true">
  Indicates whether the price includes taxes.
</ParamField>

<ParamField body="tax" type="number">
  Individual product tax rate (between `0` and `100`). Example: `15` for 15%, `12` for 12%. Only applies when `enable_per_product_tax` is active in the [store settings](/guides/integraciones/e-commerce/api-configuracion/tienda). If not sent or is `0`, the store's global rate is used.
</ParamField>

<ParamField body="status" type="boolean" default="true">
  Product status (active/inactive).
</ParamField>

<ParamField body="stock_type" type="string" default="unlimited">
  Inventory type: `limited` or `unlimited`.
</ParamField>

<ParamField body="stock" type="number">
  Available quantity. Applies only when `stock_type` is `limited`.
</ParamField>

<ParamField body="product_url" type="string">
  URL of the product in your store (max. 2048 characters).
</ParamField>

<ParamField body="discount_type" type="string">
  Discount type: `value` (fixed amount) or `percentage`.
</ParamField>

<ParamField body="discount" type="number">
  Discount value (min. 0).
</ParamField>

<ParamField body="categories" type="string[]">
  List of category names. They are created automatically if they do not exist.
</ParamField>

<ParamField body="images" type="string[]">
  List of public image URLs for the product.
</ParamField>

<ParamField body="note_enabled" type="boolean" default="false">
  Enables a note field on the product detail page, allowing the customer to add a comment when adding the product to the cart (e.g., "No onions", "Gift wrap").
</ParamField>

<ParamField body="note_label" type="string">
  Placeholder text displayed in the note field (max. 255 characters). If not specified, a generic default text is used.
</ParamField>

<ParamField body="branch" type="string">
  Code of the branch to which the product is assigned. The branch must already exist.
</ParamField>

<Warning>
  The branch must be created before assigning it to a product. Use the [Create branch](/guides/integraciones/e-commerce/api-sucursales/crear) endpoint to register it first.
</Warning>

<ParamField body="features" type="object[]">
  List of product features (spec sheet).

  <Expandable title="Fields of each feature">
    <ParamField body="label" type="string" required>
      Feature name (max. 255 characters). E.g., `Material`, `Weight`.
    </ParamField>

    <ParamField body="value" type="string" required>
      Feature value (max. 200 characters). E.g., `Cotton`, `500g`.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="replace_variations" type="boolean" default="false">
  Controls how variations are synced. With `false` (default), variations are updated or created by SKU and existing ones not present in the payload are **kept**. With `true`, variations not present in the payload are **removed**.
</ParamField>

<ParamField body="variations" type="object[]">
  List of product variations.

  <Expandable title="Variation fields">
    <ParamField body="sku" type="string" required>
      Unique SKU of the variation (max. 255 characters). If a variation with this SKU already exists, it is updated.
    </ParamField>

    <ParamField body="price" type="number" required>
      Variation price (min. 0).
    </ParamField>

    <ParamField body="stock_type" type="string" default="unlimited">
      Variation inventory type: `limited` or `unlimited`. Allows managing stock for each variation independently from the parent product.
    </ParamField>

    <ParamField body="stock" type="number">
      Available quantity for the variation. Applies only when `stock_type` is `limited`.
    </ParamField>

    <ParamField body="properties" type="object">
      Additional properties in JSON format.
    </ParamField>

    <ParamField body="images" type="string[]">
      List of image URLs for the variation.
    </ParamField>

    <ParamField body="attributes" type="object[]">
      List of variation attributes.

      <Expandable title="Attribute fields">
        <ParamField body="name" type="string" required>
          Attribute name — for example: `Size`, `Color` (max. 255 characters).
        </ParamField>

        <ParamField body="value" type="string" required>
          Attribute value — for example: `M`, `Blue` (max. 255 characters).
        </ParamField>
      </Expandable>
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="modifier_groups" type="object[]">
  Product modifier groups (add-ons). Common in food delivery (e.g., "Choose your Drinks", "Pick your Sauces"). If the field is absent, existing modifiers are preserved. If it is an empty array `[]`, all modifiers are deleted. If it has data, modifiers are completely replaced.

  <Expandable title="Modifier group fields">
    <ParamField body="code" type="string" required>
      Unique group code (max. 255 characters).
    </ParamField>

    <ParamField body="name" type="string" required>
      Visible group name (max. 255 characters).
    </ParamField>

    <ParamField body="min_quantity" type="integer" default="0">
      Minimum number of options the customer must select.
    </ParamField>

    <ParamField body="max_quantity" type="integer" default="1">
      Maximum number of options the customer can select (min. 1).
    </ParamField>

    <ParamField body="max_per_option" type="integer">
      Maximum number of times the same option can be chosen (min. 1).
    </ParamField>

    <ParamField body="is_required" type="boolean" default="false">
      Whether the group is required to add to the cart.
    </ParamField>

    <ParamField body="position" type="integer">
      Group sort position (assigned automatically if omitted).
    </ParamField>

    <ParamField body="options" type="object[]" required>
      List of group options (minimum 1 option).

      <Expandable title="Option fields">
        <ParamField body="code" type="string" required>
          Unique option code (max. 255 characters).
        </ParamField>

        <ParamField body="name" type="string" required>
          Visible option name (max. 255 characters).
        </ParamField>

        <ParamField body="price" type="number" default="0">
          Additional price for the option (min. 0).
        </ParamField>

        <ParamField body="image_url" type="string">
          Option image URL (must be a public URL).
        </ParamField>

        <ParamField body="position" type="integer">
          Sort position (assigned automatically if omitted).
        </ParamField>

        <ParamField body="status" type="boolean" default="true">
          Whether the option is available.
        </ParamField>

        <ParamField body="linked_product_sku" type="string">
          SKU of a linked product. Resolved to `linked_product_id` (UUID). If the SKU is not found, stored as `null`.
        </ParamField>
      </Expandable>
    </ParamField>
  </Expandable>
</ParamField>

<Info>
  Modifiers are stored on the product and returned automatically in all endpoints that return products.
</Info>

## Full example

This example creates three products: a t-shirt with size variations, a simple pair of pants, and a pizza with modifier groups.

<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: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "resources": [
        {
          "sku": "TSHIRT-BLU",
          "name": "Basic Blue T-Shirt",
          "description": "100% premium cotton t-shirt, regular fit",
          "price": 29.99,
          "has_tax": true,
          "tax": 15,
          "status": true,
          "stock_type": "limited",
          "stock": 150,
          "product_url": "https://my-store.com/basic-blue-tshirt",
          "discount_type": "percentage",
          "discount": 10,
          "branch": "BRANCH-DOWNTOWN",
          "categories": ["Clothing", "T-Shirts"],
          "images": [
            "https://my-store.com/images/blue-tshirt-front.jpg",
            "https://my-store.com/images/blue-tshirt-back.jpg"
          ],
          "variations": [
            {
              "sku": "TSHIRT-BLU-S",
              "price": 29.99,
              "stock_type": "limited",
              "stock": 50,
              "attributes": [
                { "name": "Size", "value": "S" },
                { "name": "Color", "value": "Blue" }
              ]
            },
            {
              "sku": "TSHIRT-BLU-M",
              "price": 29.99,
              "stock_type": "limited",
              "stock": 60,
              "attributes": [
                { "name": "Size", "value": "M" },
                { "name": "Color", "value": "Blue" }
              ]
            },
            {
              "sku": "TSHIRT-BLU-L",
              "price": 31.99,
              "stock_type": "limited",
              "stock": 40,
              "attributes": [
                { "name": "Size", "value": "L" },
                { "name": "Color", "value": "Blue" }
              ],
              "images": [
                "https://my-store.com/images/blue-tshirt-L.jpg"
              ]
            }
          ]
        },
        {
          "sku": "PANTS-BLK-M",
          "name": "Black Pants",
          "price": 49.99,
          "branch": "BRANCH-NORTH",
          "categories": ["Clothing", "Pants"],
          "note_enabled": true,
          "note_label": "Any special instructions?"
        },
        {
          "sku": "PIZZA-FAMILY",
          "name": "Family Pepperoni Pizza",
          "description": "Family-size pepperoni pizza with artisan crust",
          "price": 18.99,
          "stock_type": "unlimited",
          "branch": "BRANCH-NORTH",
          "categories": ["Pizzas", "Promotions"],
          "images": ["https://example.com/pepperoni-pizza.jpg"],
          "modifier_groups": [
            {
              "code": "drinks",
              "name": "Choose your Drink",
              "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": "Extra Toppings",
              "min_quantity": 0,
              "max_quantity": 5,
              "max_per_option": 2,
              "is_required": false,
              "options": [
                { "code": "extra-cheese", "name": "Extra Cheese", "price": 1.50 },
                { "code": "ham", "name": "Ham", "price": 2.00 },
                { "code": "olives", "name": "Olives", "price": 1.00 }
              ]
            }
          ]
        }
      ]
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json 200 Success 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>
  Replace `{app_id}` with your store ID and `YOUR_API_KEY` with your [API key](/en/guides/configuracion/claves-api).
</Note>

## Behavior

<AccordionGroup>
  <Accordion title="Asynchronous processing (default)">
    The endpoint returns `202 Accepted` immediately. Products are processed in the background.
  </Accordion>

  <Accordion title="Synchronous processing (X-Sync)">
    Send the `X-Sync: true` header to process synchronously and receive the per-product result in the same response (`200 OK`). In this mode the maximum is **50 products** per request.

    ```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="Non-existent branch">
    If the branch code does not match any branch in the store, the product is created without a branch assignment. No error is produced.
  </Accordion>

  <Accordion title="Automatic categories">
    If a category does not exist, it is created automatically within the store and the corresponding branch.
  </Accordion>

  <Accordion title="Asynchronous images">
    Images are downloaded and processed in the background after the product is created.
  </Accordion>

  <Accordion title="Variations by SKU">
    Variations are identified by their SKU. If a variation with that SKU already exists, it is updated instead of creating a new one. By default, existing variations not present in the payload are kept; send `replace_variations: true` to remove them.
  </Accordion>

  <Accordion title="Modifiers">
    If `modifier_groups` is absent in the payload, existing modifiers are preserved. If it is an empty array `[]`, all modifiers are deleted. If it has data, modifiers are completely replaced.
  </Accordion>
</AccordionGroup>

## Validation errors

If the data does not meet the validation rules, the API responds with `422` and details the fields with errors.

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

<Accordion title="Common error messages">
  | Field                                          | Message                                                               |
  | ---------------------------------------------- | --------------------------------------------------------------------- |
  | `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>

## Limits

* Maximum **500 products** per request in asynchronous mode (default), or **50** in synchronous mode (`X-Sync: true`).
* Maximum **10,000 products** per **60-second** window per store (rate limit). If exceeded, the API responds with `429 Too Many Requests`.
* All products are validated before being processed.
