# Products

> 💡 **Bulk upsert products with variants for a tenant**

***

## 🚀 TL;DR - Quick Reference

**Endpoint**: `POST /products/{tenantId}`

**Use When**:

* Adding new products to catalog
* Updating prices or inventory
* Syncing product information
* Managing product variants (sizes, colors)

**Required**: `ProductId`, `ProductVariants` (can be empty array)\
**Auth**: `x-replenit-auth-key` header\
**Returns**: Count of upserted products

**Quickest Example**:

```bash
POST /products/{YOUR_TENANT_ID}
[{
  "ProductId": "P-42",
  "ProductName": "Premium Soap",
  "ProductVariants": []
}]
```

[Jump to Full API Reference ↓](#endpoint)

***

## 🔗 Endpoint

```http
POST /products/{tenantId}
```

### 📋 Parameters

| Parameter  | Type   | Required | Description                        |
| ---------- | ------ | -------- | ---------------------------------- |
| `tenantId` | `GUID` | ✅ Yes    | The tenant identifier (must exist) |

### 📨 Request

* **Body**: JSON array of `UpsertProductDto` objects
* **Content-Type**: `application/json`

### ✅ Success Response

* **Status**: `200 OK`
* **Body**: Response envelope with `data.count`

### ❌ Error Responses

| Status                      | Description                          |
| --------------------------- | ------------------------------------ |
| `400 Bad Request`           | Invalid or missing tenant identifier |
| `404 Not Found`             | Tenant does not exist in our system  |
| `500 Internal Server Error` | Unexpected server errors             |

***

## 🎯 Identifier Behavior

The product identifier is determined by your tenant's configuration:

| Priority Setting | Identifier Used |
| ---------------- | --------------- |
| `productid`      | Product ID      |
| `sku`            | SKU             |

> 📝 **Note**: Configure your identifier priority in the tenant settings

***

## 📄 Request Schema

### UpsertProductDto Fields

| Field                | Type                        | Max Length | Required | Description                     |
| -------------------- | --------------------------- | ---------- | -------- | ------------------------------- |
| `ProductId`          | `string`                    | 100        | ✅        | Unique product identifier       |
| `ProductName`        | `string`                    | 200        | ❌        | Product name/title              |
| `Taxonomy`           | `string[]`                  | -          | ❌        | Product categories/tags         |
| `IsOfflineExclusive` | `bool`                      | -          | ❌        | Available offline only          |
| `ProductVariants`    | `UpsertProductVariantDto[]` | -          | ✅        | Product variants (can be empty) |

### UpsertProductVariantDto Fields

|| Field | Type | Max Length | Required | Description | ||| --------------- | -------- | ---------- | -------- | --------------------------------- | ||| `VariantId` | `string` | 100 | ✅ | Unique variant identifier | ||| `Sku` | `string` | 200 | ✅ | Variant SKU | || `Name` | `string` | 600 | ❌ | Variant name | | `Image` | `string` | 2000 | ❌ | Image URL | | `Size` | `string` | 50 | ❌ | Size (e.g., "1l", "500ml") | | `Stock` | `int` | - | ❌ | Available inventory | | `OriginalPrice` | `double` | - | ❌ | Regular price | | `SalePrice` | `double` | - | ❌ | Discounted price | | `Currency` | `string` | 3 | ❌ | Currency code (ISO 4217) | | `IsAvailable` | `bool` | - | ❌ | Availability status | | `Language` | `string` | 10 | ❌ | Language code (IETF BCP 47) | | `BrandName` | `string` | 200 | ❌ | Brand name | | `Url` | `string` | 2000 | ❌ | Product page URL |

> 💡 **Note**: Size is automatically parsed into `SizeNumeric` and `SizeMetric` on the server

***

## 🗑️ DELETE Endpoints

Product deletions cover two scopes:

* **Entire product** — removes the product and cascades to all its variants downstream.
* **Single variant** — removes one variant of an existing product.

Both scopes are queued for downstream processing and applied idempotently. Endpoints return success once the request is durably accepted.

| Scope          | Endpoint                                                       |
| -------------- | -------------------------------------------------------------- |
| Entire product | `DELETE /products/{tenantId}/{productId}`                      |
| Single variant | `DELETE /products/{tenantId}/{productId}/variants/{variantId}` |

### 🔗 Delete Product

```http
DELETE /products/{tenantId}/{productId}
```

#### 📋 Parameters

| Parameter   | Type     | Required     | Description              |
| ----------- | -------- | ------------ | ------------------------ |
| `tenantId`  | `GUID`   | ✅ Yes (path) | The tenant identifier    |
| `productId` | `string` | ✅ Yes (path) | The product ID to delete |

#### ✅ Success Response

* **Status**: `200 OK`

```json
{
  "success": true,
  "message": "Product removed.",
  "data": {
    "productId": "P-42",
    "deletedAt": "2024-12-22T14:02:49Z"
  }
}
```

### 🔗 Delete Product Variant

```http
DELETE /products/{tenantId}/{productId}/variants/{variantId}
```

The variant is identified by the `(productId, variantId)` pair. Both segments are required.

#### 📋 Parameters

| Parameter   | Type     | Required     | Description              |
| ----------- | -------- | ------------ | ------------------------ |
| `tenantId`  | `GUID`   | ✅ Yes (path) | The tenant identifier    |
| `productId` | `string` | ✅ Yes (path) | Parent product ID        |
| `variantId` | `string` | ✅ Yes (path) | The variant ID to delete |

#### ✅ Success Response

* **Status**: `200 OK`

```json
{
  "success": true,
  "message": "Variant removed.",
  "data": {
    "productId": "P-42",
    "variantId": "V-42",
    "deletedAt": "2024-12-22T14:02:49Z"
  }
}
```

#### ❌ Error Responses

**Delete Product:**

| Status | Scenario           | Example Response                                                         |
| ------ | ------------------ | ------------------------------------------------------------------------ |
| `400`  | Missing product ID | `{"success": false, "message": "Product ID is required.", "data": {}}`   |
| `404`  | Tenant not found   | `{"success": false, "message": "Tenant not found.", "data": {}}`         |
| `500`  | Server error       | `{"success": false, "message": "Failed to delete product.", "data": {}}` |

**Delete Variant:**

| Status | Scenario           | Example Response                                                         |
| ------ | ------------------ | ------------------------------------------------------------------------ |
| `400`  | Missing product ID | `{"success": false, "message": "Product ID is required.", "data": {}}`   |
| `400`  | Missing variant ID | `{"success": false, "message": "Variant ID is required.", "data": {}}`   |
| `404`  | Tenant not found   | `{"success": false, "message": "Tenant not found.", "data": {}}`         |
| `500`  | Server error       | `{"success": false, "message": "Failed to delete variant.", "data": {}}` |

**Note**: Tenant validation is performed automatically by middleware before reaching the endpoint.

### 🚀 DELETE Examples

#### Delete Product — cURL

```bash
curl -X DELETE "https://api.replen.it/products/{tenantId}/P-42" \
  -H 'x-replenit-auth-key: YOUR_BASE64_ACCESS_KEY'
```

#### Delete Variant — cURL

```bash
curl -X DELETE "https://api.replen.it/products/{tenantId}/P-42/variants/V-42" \
  -H 'x-replenit-auth-key: YOUR_BASE64_ACCESS_KEY'
```

#### Python

```python
import requests

base = f"https://api.replen.it/products/{tenant_id}"
headers = {"x-replenit-auth-key": "YOUR_BASE64_ACCESS_KEY"}

# Delete entire product
requests.delete(f"{base}/P-42", headers=headers)

# Delete single variant
requests.delete(f"{base}/P-42/variants/V-42", headers=headers)
```

#### Node.js

```javascript
const axios = require('axios');

const base = `https://api.replen.it/products/${tenantId}`;
const headers = { 'x-replenit-auth-key': 'YOUR_BASE64_ACCESS_KEY' };

await axios.delete(`${base}/P-42`, { headers });
await axios.delete(`${base}/P-42/variants/V-42`, { headers });
```

***

## 💡 Real-World Use Cases

### Use Case 1: New Product Launch

**Scenario**: Launching a new product with multiple variants (sizes/colors)

**What to send**:

```json
[{
  "ProductId": "P-2024-NEW",
  "ProductName": "Winter Collection Sweater",
  "Taxonomy": ["Clothing", "Winter", "Sweaters"],
  "ProductVariants": [
    {
      "VariantId": "P-2024-NEW-RED-S",
      "Sku": "SWEATER-RED-S",
      "Name": "Red Small",
      "Size": "S",
      "Stock": 50,
      "OriginalPrice": 79.99,
      "SalePrice": 79.99,
      "Currency": "USD",
      "IsAvailable": true
    },
    {
      "VariantId": "P-2024-NEW-RED-M",
      "Sku": "SWEATER-RED-M",
      "Name": "Red Medium",
      "Size": "M",
      "Stock": 100,
      "OriginalPrice": 79.99,
      "SalePrice": 79.99,
      "Currency": "USD",
      "IsAvailable": true
    }
  ]
}]
```

**What happens**:

1. ✅ Product added to catalog
2. 🎯 Can be used in product recommendation campaigns
3. 📊 Inventory tracked per variant

***

### Use Case 2: Price Update (Sale)

**Scenario**: Running a limited-time sale on specific products

**What to send**:

```json
[{
  "ProductId": "P-42",
  "ProductVariants": [{
    "VariantId": "V-42",
    "Sku": "SKU-42",
    "OriginalPrice": 99.99,
    "SalePrice": 79.99,
    "Currency": "USD"
  }]
}]
```

**What happens**:

1. ✅ Sale price updated
2. 🎯 Can trigger "price drop" alerts to interested customers
3. 📊 Campaign ROI tracked with new pricing

***

### Use Case 3: Out of Stock

**Scenario**: Product sold out, update availability

**What to send**:

```json
[{
  "ProductId": "P-POPULAR",
  "ProductVariants": [{
    "VariantId": "V-POPULAR-L",
    "Sku": "SKU-POPULAR-L",
    "Stock": 0,
    "IsAvailable": false
  }]
}]
```

**What happens**:

1. ✅ Product marked as unavailable
2. 🚫 Removed from recommendation engine
3. 📧 Can trigger "back in stock" notification setup

***

### Use Case 4: Multi-Pack Product

**Scenario**: Product sold in packs (e.g., "4 x 500ml")

**What to send**:

```json
[{
  "ProductId": "P-PACK",
  "ProductName": "Organic Juice Pack",
  "ProductVariants": [{
    "VariantId": "V-PACK-4X500",
    "Sku": "JUICE-PACK-4X500ML",
    "Name": "4-Pack Orange Juice",
    "Size": "4x500ml",
    "Stock": 25,
    "OriginalPrice": 15.99,
    "Currency": "EUR"
  }]
}]
```

**Smart parsing**: System automatically calculates:

* `SizeNumeric`: 2.0 (litres total)
* `SizeMetric`: "l"

***

### 📝 Complete Example Request (All Fields)

```json
[
  {
    "ProductId": "P-42",
    "ProductName": "Premium Acme Soap Collection",
    "Taxonomy": ["Personal Care", "Hygiene", "Bath & Body", "Soap"],
    "IsOfflineExclusive": false,
    "ProductVariants": [
      {
        "VariantId": "V-42-RED-L",
        "Sku": "SKU-42-RED-L",
        "Name": "Premium Red Rose Scent - Large",
        "Image": "https://cdn.example.com/products/soap/red-rose-large.jpg",
        "Size": "1l",
        "Stock": 150,
        "OriginalPrice": 29.99,
        "SalePrice": 24.99,
        "Currency": "USD",
        "IsAvailable": true,
        "Language": "en-US",
        "BrandName": "Acme Luxury Collection"
      },
      {
        "VariantId": "V-42-BLUE-M",
        "Sku": "SKU-42-BLUE-M",
        "Name": "Ocean Blue Fresh - Medium",
        "Image": "https://cdn.example.com/products/soap/ocean-blue-medium.jpg",
        "Size": "500ml",
        "Stock": 75,
        "OriginalPrice": 19.99,
        "SalePrice": 14.99,
        "Currency": "USD",
        "IsAvailable": true,
        "Language": "en-US",
        "BrandName": "Acme Luxury Collection"
      },
      {
        "VariantId": "V-42-GREEN-S",
        "Sku": "SKU-42-GREEN-S",
        "Name": "Green Tea Mint - Small",
        "Image": "https://cdn.example.com/products/soap/green-tea-small.jpg",
        "Size": "250ml",
        "Stock": 0,
        "OriginalPrice": 12.99,
        "SalePrice": 12.99,
        "Currency": "USD",
        "IsAvailable": false,
        "Language": "en-US",
        "BrandName": "Acme Luxury Collection"
      }
    ]
  },
  {
    "ProductId": "P-100",
    "ProductName": "Organic Shampoo Bar",
    "Taxonomy": ["Hair Care", "Organic", "Eco-Friendly"],
    "IsOfflineExclusive": true,
    "ProductVariants": [
      {
        "VariantId": "V-100-LAVENDER",
        "Sku": "SKU-100-LAV",
        "Name": "Lavender Dreams",
        "Image": "https://cdn.example.com/products/shampoo/lavender.jpg",
        "Size": "100g",
        "Stock": 50,
        "OriginalPrice": 15.99,
        "SalePrice": 15.99,
        "Currency": "EUR",
        "IsAvailable": true,
        "Language": "en-GB",
        "BrandName": "EcoBeauty"
      }
    ]
  }
]
```

### 📝 Minimal Example Request (Required Fields Only)

```json
[
  {
    "ProductId": "P-200",
    "ProductVariants": []
  }
]
```

### 📝 Example with Excluded Product

```json
[
  {
    "ProductId": "P-300",
    "Sku": "SKU-300",
    "ProductName": "Discontinued Item",
    "Taxonomy": ["Clearance"],
    "ProductVariants": [
      {
        "VariantId": "V-300",
        "Sku": "SKU-300-LAST",
        "Stock": 5,
        "IsAvailable": false
      }
    ]
  }
]
```

### 🚀 cURL Example

```bash
curl -sS \
  -X POST "https://api.replen.it/products/{tenantId}" \
  -H 'Content-Type: application/json' \
  -H 'x-replenit-auth-key: YOUR_BASE64_ACCESS_KEY' \
  -d @products.json
```

### 🐍 Python Example

```python
import requests
import json

url = "https://api.replen.it/products/{tenantId}"
headers = {
    "Content-Type": "application/json",
    "x-replenit-auth-key": "YOUR_BASE64_ACCESS_KEY"
}

products = [
    {
        "ProductId": "P-42",
        "ProductName": "Acme Soap",
        "Taxonomy": ["Hygiene", "Soap"],
        "ProductVariants": [
            {
                "VariantId": "V-42",
                "Sku": "SKU-42-RED",
                "Name": "Red 1L",
                "Size": "1l",
                "Stock": 10,
                "OriginalPrice": 19.99,
                "SalePrice": 14.99,
                "Currency": "USD",
                "IsAvailable": True
            }
        ]
    }
]

response = requests.post(url, headers=headers, json=products)
print(response.json())
```

### 🟢 Node.js Example

```javascript
const axios = require('axios');

const url = 'https://api.replen.it/products/{tenantId}';
const headers = {
    'Content-Type': 'application/json',
    'x-replenit-auth-key': 'YOUR_BASE64_ACCESS_KEY'
};

const products = [
    {
        ProductId: 'P-42',
        ProductName: 'Acme Soap',
        Taxonomy: ['Hygiene', 'Soap'],
        ProductVariants: [
            {
                VariantId: 'V-42',
                Sku: 'SKU-42-RED',
                Name: 'Red 1L',
                Size: '1l',
                Stock: 10,
                OriginalPrice: 19.99,
                SalePrice: 14.99,
                Currency: 'USD',
                IsAvailable: true
            }
        ]
    }
];

axios.post(url, products, { headers })
    .then(response => console.log(response.data))
    .catch(error => console.error(error));
```

***

## 📊 Response Examples

### ✅ Success Response (200)

```json
{
  "success": true,
  "message": "Products saved.",
  "data": {
    "count": 2,
    "processedAt": "2024-12-22T14:05:51Z"
  }
}
```

### ❌ Validation Error (400) - Missing Required Fields

```json
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "00-pqr789...",
  "errors": {
    "[0].ProductId": [
      "The ProductId field is required."
    ],
    "[0].ProductVariants[0].VariantId": [
      "The VariantId field is required."
    ],
    "[0].ProductVariants[0].Sku": [
      "The Sku field is required."
    ]
  }
}
```

**📖 What this means:**

* Your first product (index `[0]`) is missing required fields
* `ProductId` is required for the product
* The first variant (index `[0]`) is missing `VariantId` and `Sku`

**✅ How to fix:**

```json
[
  {
    "ProductId": "P-123",
    "ProductVariants": [
      {
        "VariantId": "V-123-RED",
        "Sku": "SKU-123-RED"
      }
    ]
  }
]
```

**💡 Common causes:**

* Forgot to include required fields
* Typo in field names (they are case-sensitive)
* Sending empty object `{}`

### ❌ Validation Error (400) - String Length Exceeded

```json
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "00-stu123...",
  "errors": {
    "[0].ProductId": [
      "The field ProductId must be a string with a maximum length of 100."
    ],
    "[0].ProductVariants[0].Image": [
      "The field Image must be a string with a maximum length of 2000."
    ],
    "[0].ProductVariants[0].Currency": [
      "The field Currency must be a string with a maximum length of 3."
    ]
  }
}
```

**📖 What this means:**

* One or more fields exceed their maximum allowed length
* `ProductId`: maximum 100 characters
* `Image` URLs: maximum 2000 characters
* `Currency`: maximum 3 characters

**✅ How to fix:**

* Shorten the field values to meet length limits
* For `ProductId`: use shorter IDs or codes
* For `Image`: use URL shorteners or CDN paths
* For `Currency`: use standard 3-letter codes (USD, EUR, etc.)

**💡 Field length limits:** || Field | Max Length | |||-------|------------| || ProductId | 100 | || VariantId | 100 | || Sku | 200 | || Size, Color | 50 | || ProductName, BrandName | 200 | || Variant Name | 600 | || Image, Url | 2000 | || Currency | 3 | || Language | 10 |

### ❌ Not Found (404)

```json
{
  "success": false,
  "message": "Tenant not found.",
  "data": {}
}
```

### ❌ Internal Server Error (500)

```json
{
  "success": false,
  "message": "An unexpected error occurred. Please try again.",
  "data": {}
}
```

***

## 💡 Best Practices

1. **Variant Management**: Always include at least one variant, even for simple products
2. **Image URLs**: Use HTTPS URLs for product images (max 2000 characters)
3. **Size Format**: Use standard size notations (e.g., "1l", "500ml", "XL")
4. **Pricing**: Ensure `SalePrice` is less than or equal to `OriginalPrice`
5. **Taxonomy**: Use consistent category naming across products (hierarchical structure recommended)
6. **Stock Levels**: Keep stock information updated to prevent overselling
7. **Language Format**: Use IETF language tags (BCP 47):
   * Examples: `en`, `en-US`, `en-GB`, `fr-FR`, `de-DE`, `es-ES`
   * Pattern: `language[-script][-region]`
   * Reference: [IETF BCP 47](https://tools.ietf.org/html/bcp47)
8. **Currency Format**: Use ISO 4217 currency codes:
   * Examples: `USD`, `EUR`, `GBP`, `JPY`, `CAD`, `AUD`
   * Always use 3-letter codes
   * Reference: [ISO 4217](https://www.iso.org/iso-4217-currency-codes.html)
9. **Batch Size**: Recommended batch size is 50-100 products per request
10. **Product Identifiers**: Ensure consistency between `ProductId` and `SKU` across your systems

***

## 🎨 Size Parsing Examples

| Input   | SizeNumeric | SizeMetric |
| ------- | ----------- | ---------- |
| "1l"    | 1           | "l"        |
| "500ml" | 500         | "ml"       |
| "2.5kg" | 2.5         | "kg"       |
| "XL"    | null        | "XL"       |

***

## 🔍 Related Resources

* [Customers API Documentation](/replenit-docs/customers.md)
* [Orders API Documentation](/replenit-docs/orders.md)
* [Error Responses Guide](/replenit-docs/error-responses.md)
* [Best Practices Guide](/replenit-docs/best-practices.md)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://replenit.gitbook.io/replenit-docs/products.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
