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

# Upsert Products

> Create or update products from any e-commerce platform

Create or update up to 100 products in your Sequenzy catalog, keyed by your `productId`. Products pushed here behave exactly like products synced from Shopify or WooCommerce - they power product blocks in emails, replenishment reminders, back-in-stock notifications, and segmentation.

Use this endpoint to connect any e-commerce platform (custom checkout, CheckoutChamp, Sticky.io, your own backend) to Sequenzy. See the [custom commerce integration guide](/concepts/custom-commerce) for the full walkthrough.

Updates are partial: optional fields you omit keep their stored values, so a stock-only update like `{"productId": "SKU-1", "title": "...", "inStock": false}` does not erase the product's image, URL, or pricing. Pass an explicit `null` to clear a stored value.

## Request Body

<ParamField body="products" type="array" required>
  Products to create or update (1-100 per request). Each product is upserted by
  its `productId`.
</ParamField>

<ParamField body="products[].productId" type="string" required>
  Your product identifier. Used as the upsert key - pushing the same
  `productId` again updates the product.
</ParamField>

<ParamField body="products[].title" type="string" required>
  Product title.
</ParamField>

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

<ParamField body="products[].imageUrl" type="string">
  Product image URL.
</ParamField>

<ParamField body="products[].url" type="string">
  Public product page URL. Used for links in emails.
</ParamField>

<ParamField body="products[].priceCents" type="integer">
  Price in cents. Defaults to the lowest variant price when variants are
  provided.
</ParamField>

<ParamField body="products[].compareAtPriceCents" type="integer">
  Compare-at (strikethrough) price in cents.
</ParamField>

<ParamField body="products[].currency" type="string">
  ISO 4217 currency code (e.g. `USD`).
</ParamField>

<ParamField body="products[].inStock" type="boolean">
  Whether the product is in stock. Defaults to `true`, or to whether any
  variant is available when variants are provided.
</ParamField>

<ParamField body="products[].variants" type="array">
  Product variants. Each variant has `variantId` (required), `title`
  (required), `sku`, `priceCents`, `compareAtPriceCents`, `imageUrl`,
  `inStock`, `inventoryQuantity`, and `options` (array of `{ name, value }`).
  When provided, the variant list is replaced entirely - pass an empty array
  to remove all variants. Omit the field to leave existing variants unchanged.
</ParamField>

```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/products" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "products": [
      {
        "productId": "SKU-PROTEIN-1KG",
        "title": "Protein Powder",
        "description": "Whey protein, 1kg bag",
        "imageUrl": "https://cdn.example.com/protein.jpg",
        "url": "https://store.example.com/products/protein",
        "currency": "USD",
        "variants": [
          {
            "variantId": "SKU-PROTEIN-1KG-VANILLA",
            "title": "Vanilla",
            "priceCents": 8850,
            "inventoryQuantity": 12,
            "options": [{ "name": "Flavor", "value": "Vanilla" }]
          },
          {
            "variantId": "SKU-PROTEIN-1KG-CHOC",
            "title": "Chocolate",
            "priceCents": 8850,
            "inStock": false,
            "options": [{ "name": "Flavor", "value": "Chocolate" }]
          }
        ]
      }
    ]
  }'
```

## Stock Transitions

When an upsert changes a product or variant from out of stock to in stock, Sequenzy automatically fires the `ecommerce.back_in_stock` event for subscribers who [requested a back-in-stock notification](/api-reference/products/back-in-stock). Going out of stock cancels pending notifications.

Replenishment settings configured in the dashboard are preserved across upserts.

## Responses

<ResponseExample>
  ```json 200 theme={null}
  {
    "success": true,
    "upserted": 1,
    "backInStockEventsTriggered": 0,
    "products": [
      {
        "id": "prod_abc123",
        "productId": "SKU-PROTEIN-1KG",
        "provider": "api",
        "title": "Protein Powder",
        "priceCents": 8850,
        "currency": "USD",
        "inStock": true,
        "variants": [
          {
            "variantId": "SKU-PROTEIN-1KG-VANILLA",
            "title": "Vanilla",
            "priceCents": 8850,
            "inStock": true,
            "inventoryQuantity": 12
          }
        ]
      }
    ]
  }
  ```

  ```json 401 theme={null}
  {
    "success": false,
    "error": "Unauthorized"
  }
  ```

  ```json 422 theme={null}
  {
    "success": false,
    "error": "Validation error: products is required"
  }
  ```

  ```json 500 theme={null}
  {
    "success": false,
    "error": "Internal server error"
  }
  ```
</ResponseExample>
