---
updatedAt: 2026-02-25T12:49:03.000Z
agentTools:
  projectIndex: https://developer.xentral.com/llms.txt
---

# Create a product (V2 API)

<br />

> **No V3 Product API**
>
> Unlike customers and sales orders, there is **no V3 API** for products. The V3 API only covers business documents, sales prices, and customers. Use the **V2 API** for all product operations.

***

## Contents

* [Overview](#overview)
* [ERP Context](#erp-context)
* [At a Glance](#at-a-glance)
* [Pricing](#pricing)
* [Product Number Assignment](#product-number-assignment)
* [Prerequisites](#prerequisites)
* [Before You Start](#before-you-start)
* [Workflow](#workflow)
* [Step-by-Step Guide](#step-by-step-guide)
* [Product Variants (Matrix Products)](#product-variants-matrix-products)
* [Bill of Materials (BOM)](#bill-of-materials-bom)
* [External References (Foreign Numbers)](#external-references-foreign-numbers)
* [Product Properties](#product-properties)
* [Cross-Selling](#cross-selling)
* [Categories and Merchandise Groups](#categories-and-merchandise-groups)
* [Sales Channel Settings and Product Texts](#sales-channel-settings-and-product-texts)
* [What the API Cannot Do](#what-the-api-cannot-do)
* [Error Handling](#error-handling)
* [Related Resources](#related-resources)

***

## Overview

This guide shows you how to create a product via the Xentral API. You'll learn the minimum required fields, how to set prices, configure variants, manage bill of materials, and handle shop synchronization via external references.

**Use Cases:**

* Import product catalog from an online shop (Shopify, WooCommerce, etc.) into Xentral
* Synchronize products from a PIM system (Akeneo, Pimcore, etc.)
* Create products from a B2B partner portal
* Migrate product master data from another ERP system

> **New to the Xentral API?** Read first:
>
> * [Authentication](https://developer.xentral.com/reference/authentication) - Create API Token
> * [Rate Limiting](https://developer.xentral.com/reference/rate-limiting) - Request limits

***

## ERP Context

> **Important for developers without ERP experience:** In the Xentral UI, products are internally referred to as **"Artikel"** (article). A product is a master data record that contains all product-related data, such as dimensions, weights, properties, custom fields, prices, tax settings, components (in case of JIT or production products), images, attachments, texts and translations. Without a product you cannot do any business process in Xentral.

### What is linked to a product?

In Xentral, a product record is connected to:

* **Sales Orders** - Line items referencing this product
* **Invoices & Delivery Notes** - Billing and shipping documents
* **Purchase Orders** - Ordering from suppliers
* **Sales Prices** - Customer-specific, group-specific, and quantity-based pricing (see [Pricing](#pricing))
* **Purchase Prices** - Supplier pricing with quantity breaks (see [Pricing](#pricing))
* **Bill of Materials (BOM)** - Component list for JIT or production products (see [BOM](#bill-of-materials-bom))
* **Production Orders** - Manufacturing instructions (V3 API)
* **External References** - Shop IDs, barcodes, foreign numbers (see [External References](#external-references-foreign-numbers))
* **Properties** - Custom attributes like color, material, weight class (see [Properties](#product-properties))
* **Cross-Selling** - Related product recommendations (see [Cross-Selling](#cross-selling))
* **Sales Channels** - Per-shop settings for visibility, SEO, delivery time (see [Sales Channel Settings](#sales-channel-settings-and-product-texts))

### What is configured ON a product?

These are **settings** stored directly on the product record:

* Tax category (`standard`, `reduced`, `free`)
* Measurements (width, height, length, weight — dimensions in cm, weight in kg)
* Unit of measure (Stück, Kilogram, Liter, etc.)
* Manufacturer information (name, number, link)
* Stock management settings (`isStockItem` — enables warehouse tracking)
* Batch tracking (`hasBatches`), best-before date tracking (`hasBestBeforeDate`), and serial numbers (`serialNumbersMode`)
* Customs information (country of origin and customs tariff number)
* Age rating (for DHL age verification: `"16"` or `"18"`)

### Product types at a glance

| Type                      | `isStockItem` | `hasBillOfMaterials` | `isMatrixProduct` | Use Case                                                                                     |
| ------------------------- | ------------- | -------------------- | ----------------- | -------------------------------------------------------------------------------------------- |
| **Simple product**        | `true`        | `false`              | `false`           | Standard physical goods                                                                      |
| **Service**               | `false`       | `false`              | `false`           | Consulting, installation                                                                     |
| **Matrix parent**         | `false`       | -                    | `true`            | Parent product defining size/color options. Not sold directly, no stock.                     |
| **Matrix child**          | `true`        | `false`              | `false`           | Own product with own SKU and stock. References parent via `matrixProduct` field.             |
| **JIT (Just-in-Time)**    | `false`       | `true`               | `false`           | `isAssembledJustInTime: true` — No stock. Components are picked individually from warehouse. |
| **Production product**    | `true`        | `true`               | `false`           | Manufactured in-house (`isProductionProduct: true`). Has stock after production completion.  |
| **Normal BOM**            | `true`        | `true`               | `false`           | Assembled/bundled products. Has stock, picked as single item.                                |
| **Shipping cost product** | `false`       | `false`              | `false`           | `isShippingCostsProduct: true`                                                               |
| **Discount product**      | `false`       | `false`              | `false`           | `isDiscountProduct: true` — Used for discounts on orders                                     |

### Why is this important?

Understanding this helps you:

* Know that **prices and stock are NOT set during product creation** — they are managed through separate endpoints after the product exists (see [Pricing](#pricing))
* Know that products **always belong to a project** (Mandant) — this is a required field
* Understand that a product number (SKU) serves as the primary business identifier, while the `id` is the internal database reference and is used throughout the whole API to identify a product
* Plan your API calls: depending on the level of detail that you want to save for your product, creating a complete product typically requires **multiple requests** through a set of product-related endpoints (product → prices → images → external references)

***

## At a Glance

|                     |                                                                                                                                 |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| **Endpoint**        | `POST /api/v2/products`                                                                                                         |
| **Method**          | POST                                                                                                                            |
| **Auth**            | Bearer Token ([Documentation](https://developer.xentral.com/reference/authentication))                                          |
| **Required Scope**  | `product:create`                                                                                                                |
| **Required Fields** | `name`, `project.id`                                                                                                            |
| **Product Number**  | Auto-assigned from number range, or provide custom via `number` field                                                           |
| **Sales Prices**    | Separate endpoint: `POST /api/v3/salesPrices` (see [Pricing](#pricing))                                                         |
| **Purchase Prices** | Separate endpoint: `POST /api/v2/purchasePrices` ([API Reference](https://developer.xentral.com/reference/purchasepricecreate)) |
| **Stock**           | Managed through warehouse endpoints (see [warehousestockitem](https://developer.xentral.com/reference/warehousestockitem))      |

**Minimal example:**

```bash
curl -X POST "https://{instance}.xentral.biz/api/v2/products" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My First Product",
    "project": {"id": "1"}
  }'
```

**Response:** `201 Created` - The response body is empty. The new product ID is in the `Location` header:

```
Location: https://{instance}.xentral.biz/api/v2/products/42
```

> [Full API Reference: Create Product](https://developer.xentral.com/reference/productcreatev2)

***

## Pricing

Prices are **not** set on the product itself. Instead, sales prices and purchase prices are managed through **separate endpoints**. This separation allows for customer-specific pricing, quantity breaks, date ranges, and multi-currency support.

> **Important:** All prices in Xentral are **net** (excluding VAT). If your shop sends gross prices, you must convert them before sending to the API.

### Sales Prices

Sales prices define what customers pay. Each price entry can be:

* A **base price** (no customer/group — applies to everyone)
* A **customer-specific price** (linked to a specific customer)
* A **group price** (linked to a customer group)

**Quantity breaks** can be applied to **all** of the above price types. A quantity break applies from a minimum quantity (e.g., from 10 units, use this price).

> **Important:** You can set either `customer` or `customerGroup` on a sales price, but **not both at the same time**.

**Create a base sales price:**

> **Required Scope:** `salesPrice:create`

```bash
curl -X POST "https://{instance}.xentral.biz/api/v3/salesPrices" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "product": {"id": "{productId}"},
    "price": {"amount": "19.99", "currency": "EUR"},
    "amount": 1
  }'
```

> [API Reference: Create Sales Price (V3)](https://developer.xentral.com/reference/postapi-v3-salesprices)

**Response:** `201 Created` — The response body contains the created resource with its ID.

**Create a customer-specific quantity break:**

> **Required Scope:** `salesPrice:create`

```bash
curl -X POST "https://{instance}.xentral.biz/api/v3/salesPrices" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "product": {"id": "{productId}"},
    "price": {"amount": "17.50", "currency": "EUR"},
    "amount": 10,
    "customer": {"id": "{customerId}"},
    "validFrom": "2026-01-01",
    "expiresAt": "2026-12-31"
  }'
```

**Read sales prices for a product:**

> **Required Scope:** `salesPrice:read`

```bash
curl -s "https://{instance}.xentral.biz/api/v3/salesPrices?filter[0][key]=product.id&filter[0][op]=equals&filter[0][value]={productId}" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
```

> [API Reference: List Sales Prices (V3)](https://developer.xentral.com/reference/getapi-v3-salesprices)

**Response:**

```json
{
  "data": [
    {
      "id": "1",
      "customer": null,
      "customerGroup": null,
      "validFrom": null,
      "expiresAt": null,
      "amount": 1,
      "price": {"amount": "19.99000000", "currency": "EUR"}
    },
    {
      "id": "2",
      "customer": {"id": "4", "name": "Max Mustermann"},
      "customerGroup": null,
      "validFrom": "2026-01-01",
      "expiresAt": "2026-12-31",
      "amount": 10,
      "price": {"amount": "17.50000000", "currency": "EUR"}
    }
  ]
}
```

**Update sales prices:**

> **Required Scope:** `salesPrice:update`

```bash
curl -X PATCH "https://{instance}.xentral.biz/api/v3/salesPrices" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '[{"id": "{priceId}", "price": {"amount": "21.99", "currency": "EUR"}}]'
```

**Response:** `204 No Content`

> [API Reference: Update Sales Prices (V3)](https://developer.xentral.com/reference/patchapi-v3-salesprices)

### Purchase Prices

Purchase prices are used in purchase orders. They require a supplier reference.

> **Required Scope:** `purchasePrice:create`

```bash
curl -X POST "https://{instance}.xentral.biz/api/v2/purchasePrices" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "product": {"id": "{productId}"},
    "supplier": {"id": "{supplierId}"},
    "fromQuantity": 1,
    "price": {"amount": "8.50", "currency": "EUR"},
    "validFrom": "2026-01-01",
    "expiresAt": "2026-12-31"
  }'
```

> [API Reference: Create Purchase Price](https://developer.xentral.com/reference/purchasepricecreate)
>
> **Note:** If no supplier is provided and no standard supplier is defined on the product, you'll get: `"Supplier missing and no standard supplier defined."`

### Pricing Summary

| What                                 | Endpoint                                   | Scope                  |
| ------------------------------------ | ------------------------------------------ | ---------------------- |
| **Create** sales price               | `POST /api/v3/salesPrices`                 | `salesPrice:create`    |
| **Read** sales prices                | `GET /api/v3/salesPrices`                  | `salesPrice:read`      |
| **Update** sales prices              | `PATCH /api/v3/salesPrices`                | `salesPrice:update`    |
| **Delete** sales price               | `DELETE /api/v3/salesPrices/{id}`          | `salesPrice:delete`    |
| **Create** purchase price            | `POST /api/v2/purchasePrices`              | `purchasePrice:create` |
| **Read** purchase prices per product | `GET /api/v2/products/{id}/purchasePrices` | `purchasePrice:read`   |
| **Update** purchase price            | `PATCH /api/v2/purchasePrices/{id}`        | `purchasePrice:update` |
| **Delete** purchase price            | `DELETE /api/v2/purchasePrices/{id}`       | `purchasePrice:delete` |

***

## Product Number Assignment

### Automatic Assignment (Default)

If you don't provide a `number` field, Xentral automatically assigns the next available product number. The number range depends on your configuration:

| Scenario                                                    | Number Range Used                |
| ----------------------------------------------------------- | -------------------------------- |
| No specific number range configured                         | System-wide product number range |
| Project has own number range                                | Project's product number range   |
| Merchandise group (Artikelkategorien) with own number range | Merchandise group's number range |
| Merchandise group with `useMainProductNumberRange: true`    | System-wide product number range |

> **Tip:** Check your number range configuration in Xentral: Settings > Basic Settings > Number Ranges. Project number ranges are configured per project. Merchandise group number ranges are configured under: Master Data > Merchandise Groups.

### Providing Custom Numbers (Data Migration)

For data migration or legacy imports, you **can** provide your own product number:

```bash
curl -X POST "https://{instance}.xentral.biz/api/v2/products" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Migrated Product",
    "project": {"id": "1"},
    "number": "LEGACY-10042"
  }'
```

> **Warning:** Custom numbers must be unique. If the number already exists, you'll get a `409 Conflict` error:
>
> ```json
> {"title":"A product with this number exists already."}
> ```

### Best Practices for Number Ranges

| Scenario            | Approach                                                                                              |
| ------------------- | ----------------------------------------------------------------------------------------------------- |
| **New integration** | Let Xentral assign numbers automatically                                                              |
| **Data migration**  | Provide existing numbers, ensure no duplicates                                                        |
| **Multi-shop**      | Use merchandise groups with separate number ranges per channel, or separate number ranges per project |

### DATEV Compatibility

> **Needs verification:** It is unclear whether DATEV requirements for numeric-only numbers apply to product numbers (they definitely apply to customer/debtor numbers). If you export to DATEV, consider using numeric-only numbers with a consistent digit count to be safe — but verify this with your accounting team.
>
> **Reference:** [Nummernkreise verwenden](https://help.xentral.com/hc/de/articles/5697692402204-Nummernkreise-verwenden-Belegnummer-Kundennummer-Lieferantennummer-Artikelnummer)

***

## Prerequisites

* [ ] Xentral account with API access
* [ ] Personal Access Token (PAT) with scope `product:create` ([Guide](https://developer.xentral.com/reference/authentication))
* [ ] At least one project exists in Xentral (projects can only be created via UI, not API)

**Optional but useful:**

* [ ] Merchandise groups configured (for product number ranges and DATEV revenue accounts)
* [ ] Product categories created (for organizing products in a tree structure)
* [ ] Suppliers created (required for purchase prices)

***

## Before You Start

### Decision 1: Is this a stock item?

The `isStockItem` field controls whether Xentral tracks inventory for this product:

| Setting           | Use Case                                   | Effect                                        |
| ----------------- | ------------------------------------------ | --------------------------------------------- |
| `true`            | Physical goods (shipped from warehouse)    | Stock tracked, reservations created on orders |
| `false` (default) | Services, digital products, shipping costs | No inventory tracking                         |

> **Note:** Setting `isStockItem: true` does NOT set initial stock quantities. Stock is managed through warehouse operations (goods receipt, inventory counts). See [What the API Cannot Do](#what-the-api-cannot-do).

### Decision 2: Simple product or variants?

| Type                          | Use Case                          | API Approach                                                     |
| ----------------------------- | --------------------------------- | ---------------------------------------------------------------- |
| **Simple product**            | Single SKU, no size/color options | Standard `POST /api/v2/products`                                 |
| **Matrix product (variants)** | Multiple sizes, colors, etc.      | Create parent with `isMatrixProduct: true`, then create variants |

> **Important:** A matrix parent cannot be sold directly. It has no stock. Only its variant children (matrix children) are own products with their own SKU, stock, and prices. They appear on sales orders. See [Product Variants](#product-variants-matrix-products) for the full workflow.

### Decision 3: Does this product have components?

| Type                   | `hasBillOfMaterials` | `isAssembledJustInTime` | Has Stock?                 | Behavior                                                                                                                                                          |
| ---------------------- | -------------------- | ----------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Simple product**     | `false`              | `false`                 | Yes (if `isStockItem`)     | No components                                                                                                                                                     |
| **Normal BOM**         | `true`               | `false`                 | **Yes**                    | Stock is managed on the BOM product. Components are picked from warehouse internally. Same sales process as simple product.                                       |
| **JIT (Just-in-Time)** | `true`               | `true`                  | **No**                     | No stock on the JIT product itself. Components are picked individually from the warehouse and listed separately on documents.                                     |
| **Production product** | `true`               | `false`                 | **Yes** (after production) | Manufactured in-house (`isProductionProduct: true`). Same sales process as Normal BOM — has stock, picked normally. Difference: system knows it can produce more. |

> **Normal BOM vs. Production BOM for sales:** Both behave the same in the sales process — they have stock and are picked normally from the warehouse. The difference is only in procurement: a production product signals that Xentral can trigger a production order to create more stock.
>
> **When to use JIT:** Use JIT for bundles where customers should see each included item (e.g., a gift set showing "1x Coffee + 1x Mug + 1x Chocolate"). Also use JIT if you sell the components individually as well, or if you include them in different JIT products. The `hideJustInTimeItemsOnDocuments` flag can hide JIT items from customer-facing documents if needed.

### Decision 4: How to handle shop references?

If you're syncing products from an external system, you need a way to link them:

| Option                  | Approach                                        | Best for                                        |
| ----------------------- | ----------------------------------------------- | ----------------------------------------------- |
| **External references** | `POST /api/v1/products/{id}/externalReferences` | Multi-shop setups with separate IDs per channel |
| **EAN field**           | Set `ean` on the product                        | Products with standard barcodes                 |
| **Product number**      | Use your shop SKU as the `number` field         | Single-shop with unique SKUs                    |
| **Free fields**         | Use `freeFields` for custom mappings            | Legacy integrations                             |

> **Tip:** Use `GET /api/v2/products` with filters to search for existing products by number, EAN, or name before creating duplicates. See [Step 1: Check if Product Exists](#step-1-check-if-product-exists).

### Decision 5: Batch tracking or serial numbers?

| Feature               | Field               | Values                                                       | Use Case                                                    |
| --------------------- | ------------------- | ------------------------------------------------------------ | ----------------------------------------------------------- |
| **Batch tracking**    | `hasBatches`        | `true` / `false`                                             | Food (best-before dates), pharmaceuticals, chemicals        |
| **Best-before dates** | `hasBestBeforeDate` | `true` / `false`                                             | Perishable goods. Can be used with or without `hasBatches`. |
| **Serial numbers**    | `serialNumbersMode` | `"disabled"`, `"user"`, `"product"`, `"productAndWarehouse"` | Electronics, high-value items                               |

Serial number modes:

* `"disabled"` — No serial tracking
* `"user"` — Manual serial entry by warehouse staff
* `"product"` — Serial numbers are assigned per product (e.g., on the delivery note), but stock is NOT managed on serial number level. You know which serial numbers exist, but not which warehouse holds which serial number.
* `"productAndWarehouse"` — Serial numbers are tracked per product AND warehouse. Stock is managed on serial number level — Xentral knows exactly which serial number is in which warehouse. Use this mode when you need full serial number traceability across warehouses.

***

## Workflow

```
┌──────────────┐     ┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  1. Check    │ ──▶ │  2. Create   │ ──▶ │  3. Add      │ ──▶ │  4. Add      │
│  Existing?   │     │  Product     │     │  Prices      │     │  References  │
└──────────────┘     └──────────────┘     └──────────────┘     └──────────────┘
       │                    │                    │                     │
       ▼                    ▼                    ▼                     ▼
┌──────────────┐     ┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│   Found?     │     │  Store ID    │     │  Optional:   │     │  5. Verify   │
│   Use ID!    │     │   Mapping    │     │  Images,     │     │  Product     │
└──────────────┘     └──────────────┘     │  Variants,   │     └──────────────┘
                                          │  BOM, Props  │
                                          └──────────────┘
```

***

## Step-by-Step Guide

### Step 1: Check if Product Exists

Before creating a new product, check if it already exists to avoid duplicates.

**Search by product number (V2 API):**

> **Required Scope:** `product:read`

```bash
curl -s "https://{instance}.xentral.biz/api/v2/products?filter[0][key]=number&filter[0][op]=equals&filter[0][value]=MY-SKU-001" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
```

> [API Reference: List Products](https://developer.xentral.com/reference/productlistv2)

**Response:**

```json
{
  "data": [],
  "extra": {"totalCount": 0, "page": {"number": 1, "size": 10}}
}
```

If `totalCount > 0`, the product already exists. Use the existing ID instead of creating a new one.

**Other search options (V2 filter):**

* By name: `filter[0][key]=name&filter[0][op]=contains&filter[0][value]=Coffee`
* By EAN: `filter[0][key]=ean&filter[0][op]=equals&filter[0][value]=4006381333931`

> **Note:** The `name` field supports `contains` and `startsWith` operators on the products endpoint (unlike customers V2, which only supports `equals`).

***

### Step 2: Create Product

**Minimal product (just name and project):**

> **Required Scope:** `product:create`

```bash
curl -X POST "https://{instance}.xentral.biz/api/v2/products" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Organic Coffee Beans 500g",
    "project": {"id": "1"}
  }'
```

> [API Reference: Create Product](https://developer.xentral.com/reference/productcreatev2)

**Response:** `201 Created`

The product ID is in the `Location` header:

```
Location: https://{instance}.xentral.biz/api/v2/products/42
```

**More complex product example:**

```bash
curl -X POST "https://{instance}.xentral.biz/api/v2/products" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Organic Coffee Beans 500g",
    "number": "COFFEE-500",
    "project": {"id": "1"},
    "description": "Premium organic coffee beans, medium roast, 500g package",
    "shortDescription": "Organic coffee 500g",
    "ean": "4006381333931",
    "isStockItem": true,
    "salesTax": "standard",
    "unit": "Stk",
    "measurements": {
      "width":  {"value": 10.0, "unit": "cm"},
      "height": {"value": 20.0, "unit": "cm"},
      "length": {"value": 5.0,  "unit": "cm"},
      "weight": {"value": 0.55, "unit": "kg"},
      "netWeight": {"value": 0.50, "unit": "kg"}
    },
    "manufacturer": {
      "name": "Fair Trade Roasters",
      "number": "FTR-2024",
      "link": "https://fairtraderoasters.example.com"
    },
    "merchandiseGroup": {"id": "1"},
    "categories": [{"id": "1"}],
    "countryOfOrigin": "DE",
    "customsTariffNumber": "09011100",
    "minimumOrderQuantity": 1,
    "minimumStorageQuantity": 10
  }'
```

For a complete list of available fields, see the [API Reference: Create Product](https://developer.xentral.com/reference/productcreatev2).

***

### Step 3: Add Prices

After creating the product, add sales prices and optionally purchase prices via separate endpoints. See [Pricing](#pricing) for the full documentation.

**Quick example — create a base sales price:**

> **Required Scope:** `salesPrice:create`

```bash
curl -X POST "https://{instance}.xentral.biz/api/v3/salesPrices" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "product": {"id": "{productId}"},
    "price": {"amount": "19.99", "currency": "EUR"},
    "amount": 1
  }'
```

> [API Reference: Create Sales Price (V3)](https://developer.xentral.com/reference/postapi-v3-salesprices)

**Response:** `201 Created` — The response body contains the created resource with its ID.

***

### Step 4: Upload Product Image (optional)

Product images are managed through a separate endpoint.

> **Required Scope:** `productMedia:create`

```bash
curl -X POST "https://{instance}.xentral.biz/api/v1/productMedia" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "product": {"id": "{productId}"},
    "title": "Product front view",
    "keyword": "defaultImage",
    "fileName": "coffee-500g-front.jpg",
    "fileContent": "{base64-encoded-image-data}"
  }'
```

> [API Reference: Create Product Media](https://developer.xentral.com/reference/productmediacreate)

**Image keywords:**

| Keyword        | Purpose                                            | In Xentral UI    |
| -------------- | -------------------------------------------------- | ---------------- |
| `defaultImage` | Main product image (shown in lists, documents)     | "Standardbild"   |
| `printImage`   | Image used on printed documents (offers, invoices) | "Druckbild"      |
| `labelImage`   | Image used on labels                               | "Labelbild"      |
| `otherImage`   | Additional product images                          | "Weitere Bilder" |

> **Note:** Images must be **base64-encoded** and only **JPEG** and **PNG** formats are supported. Each image keyword can have multiple versions — use `POST /api/v1/productMedia/{id}/versions` to add versions to an existing image.

***

### Step 5: Add External References (optional)

If you sync products from external systems, store the external IDs as references. See [External References](#external-references-foreign-numbers) for the full documentation.

> **Required Scope:** `productExternalReferences:create`

```bash
curl -X POST "https://{instance}.xentral.biz/api/v1/products/{productId}/externalReferences" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Shopify",
    "number": "gid://shopify/Product/123456789",
    "target": {"id": "1"},
    "isActive": true,
    "isScannable": false
  }'
```

> [API Reference: Create External Reference](https://developer.xentral.com/reference/externalreferencecreate)

***

### Step 6: Verify Product

> **Required Scope:** `product:read`

```bash
curl -s "https://{instance}.xentral.biz/api/v2/products/{productId}" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
```

> [API Reference: View Product](https://developer.xentral.com/reference/productviewv2)

**Response:**

```json
{
  "data": {
    "id": "42",
    "number": "COFFEE-500",
    "name": "Organic Coffee Beans 500g",
    "description": "Premium organic coffee beans, medium roast, 500g package",
    "ean": "4006381333931",
    "isStockItem": true,
    "salesTax": "standard",
    "stockCount": 0,
    "measurements": {
      "width":  {"value": 10.0, "unit": "cm"},
      "height": {"value": 20.0, "unit": "cm"},
      "length": {"value": 5.0,  "unit": "cm"},
      "weight": {"value": 0.55, "unit": "kg"}
    },
    "project": {"id": "1"},
    "manufacturer": {
      "name": "Fair Trade Roasters",
      "number": "FTR-2024"
    },
    "...": "..."
  }
}
```

> **Note:** The response contains many more fields than shown here. `stockCount` will be `0` after creation. Stock is managed through warehouse operations, not the product API.

**Check sales prices:**

> **Required Scope:** `salesPrice:read`

```bash
curl -s "https://{instance}.xentral.biz/api/v3/salesPrices?filter[0][key]=product.id&filter[0][op]=equals&filter[0][value]={productId}" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
```

> [API Reference: List Sales Prices (V3)](https://developer.xentral.com/reference/getapi-v3-salesprices)

**Check stock levels:**

> **No scope required** for this endpoint.

```bash
curl -s "https://{instance}.xentral.biz/api/v1/products/{productId}/stocks" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
```

### Update Product (PATCH)

To update product data after creation:

> **Required Scope:** `product:update`

```bash
curl -X PATCH "https://{instance}.xentral.biz/api/v2/products/{productId}" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"description": "Updated product description"}'
```

> [API Reference: Update Product](https://developer.xentral.com/reference/productupdatev2)

**Response:** `204 No Content` - The update was successful. No body is returned.

**Bulk update (multiple products):**

```bash
curl -X PATCH "https://{instance}.xentral.biz/api/v2/products" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '[
    {"id": "42", "salesTax": "reduced"},
    {"id": "43", "isDisabled": true, "disabledReason": "Discontinued"}
  ]'
```

> [API Reference: Bulk Update Products](https://developer.xentral.com/reference/productupdatemultiplev2)

***

## Product Variants (Matrix Products)

Matrix products allow you to manage products with multiple options like size, color, or material. The parent product defines the options, and variant children represent specific combinations.

```
Matrix Product (Parent)          Not sold directly — serves as template. No stock.
├── Option: Size [S, M, L]       Defines dimensions of variation
├── Option: Color [Red, Blue]    Defines dimensions of variation
│
├── Variant: S / Red             Each variant = own product with own SKU and stock
├── Variant: S / Blue            Can have own price, stock, images
├── Variant: M / Red             Appears independently on sales orders
├── Variant: M / Blue
├── Variant: L / Red
└── Variant: L / Blue            6 variants from 2 options (3 × 2)
```

> **Important:** A matrix parent cannot be sold directly. It does not appear on sales orders, invoices, or delivery notes. It has no stock. Only the variant children (matrix children) are sellable — each is its own product with its own SKU, stock, and prices. The parent serves purely as a template and organizational container.

### Creating a Matrix Product

**Step 1: Create the parent product with options inline:**

> **Required Scope:** `product:create`

```bash
curl -X POST "https://{instance}.xentral.biz/api/v2/products" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Basic T-Shirt",
    "project": {"id": "1"},
    "isMatrixProduct": true,
    "options": [
      {
        "name": "Size",
        "values": [
          {"name": "S", "sort": 1},
          {"name": "M", "sort": 2},
          {"name": "L", "sort": 3}
        ]
      },
      {
        "name": "Color",
        "values": [
          {"name": "Red", "sort": 1},
          {"name": "Blue", "sort": 2}
        ]
      }
    ]
  }'
```

**Step 2: Create variant children from the defined options:**

> **Required Scope:** `productVariants:create`

```bash
curl -X POST "https://{instance}.xentral.biz/api/v1/products/{parentId}/actions/createVariants" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "2",
    "variants": [
      {
        "options": [
          {"name": "S", "option": {"name": "Size"}},
          {"name": "Red", "option": {"name": "Color"}}
        ]
      },
      {
        "options": [
          {"name": "M", "option": {"name": "Size"}},
          {"name": "Blue", "option": {"name": "Color"}}
        ]
      }
    ]
  }'
```

> [API Reference: Create Variants](https://developer.xentral.com/reference/productcreatevariants)

### Variant Numbering Modes

The `mode` field controls how variant product numbers are generated:

| Mode  | Name                                      | Example (parent: `TSHIRT`)      |
| ----- | ----------------------------------------- | ------------------------------- |
| `"1"` | Merchandise group number range            | `100042`, `100043`, ...         |
| `"2"` | Options appended to main number           | `TSHIRT-S-Red`, `TSHIRT-M-Blue` |
| `"3"` | Main number + appendix (no separator)     | `TSHIRT001`, `TSHIRT002`        |
| `"4"` | Suffix with configurable separator/digits | `TSHIRT-001`, `TSHIRT-002`      |

Mode `"4"` supports additional configuration:

* `prefixSeparator` — Character between main number and suffix (e.g., `"-"`)
* `prefixDigits` — Number of digits in the suffix (e.g., `3` → `001`)
* `prefixNextNumber` — Starting number for the suffix

> **Note:** Mode `"2"` produces the most readable variant numbers. Mode `"1"` uses numeric-only numbers from the merchandise group range.

### Managing Options After Creation

You can also add options and values step by step:

> **Required Scopes:** `productOptions:create`, `productOptionValues:create`

```bash
# Add an option to an existing matrix product
curl -X POST "https://{instance}.xentral.biz/api/v1/products/{productId}/options" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"name": "Material", "sort": 3}'

# Add values to an option
curl -X POST "https://{instance}.xentral.biz/api/v1/products/{productId}/options/{optionId}/values" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"name": "Cotton", "sort": 1}'
```

> **Reference:** [Matrix product: Create product variants](https://help.xentral.com/hc/en-us/articles/360016725120)

***

## Bill of Materials (BOM)

A bill of materials defines the components (parts) that make up a product. This is used for bundled products, assembled goods, or manufactured items.

### BOM Types

| Type                   | Fields                                                    | Has Stock? | Behavior on Documents                                                                                                                                                   |
| ---------------------- | --------------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Normal BOM**         | `hasBillOfMaterials: true`                                | **Yes**    | Product appears as single line item. Picked normally from warehouse.                                                                                                    |
| **JIT (Just-in-Time)** | `hasBillOfMaterials: true`, `isAssembledJustInTime: true` | **No**     | Components are **expanded** on delivery notes and invoices — each part appears as its own line. Components are picked individually from the warehouse.                  |
| **Production BOM**     | `hasBillOfMaterials: true`, `isProductionProduct: true`   | **Yes**    | Same sales behavior as Normal BOM (has stock, picked normally). Difference: components define the manufacturing recipe, and the system knows it can produce more stock. |

> **Normal BOM and Production BOM behave the same in the sales process.** You have stock on these products and pick them normally from the warehouse. JIT instead does not have stock — the components have to be picked individually from the warehouse.
>
> **When to use JIT:** Use JIT for bundles where customers should see each included item (e.g., a gift set showing "1x Coffee + 1x Mug + 1x Chocolate"). Also use JIT if you sell the components individually as well, or if you include them in different JIT products. The `hideJustInTimeItemsOnDocuments` flag can hide JIT items from customer-facing documents if needed.

### Adding Parts to a BOM

First, create the BOM product with `hasBillOfMaterials: true`, then add component parts:

> **Required Scope:** `productParts:create`

```bash
curl -X POST "https://{instance}.xentral.biz/api/v2/products/{productId}/parts" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '[
    {"part": {"id": "10"}, "amount": 1, "type": "shopping part"},
    {"part": {"id": "11"}, "amount": 2, "type": "shopping part"},
    {"part": {"id": "12"}, "amount": 1, "type": "information part / service"}
  ]'
```

**Part types:**

| Type                           | Description                                 |
| ------------------------------ | ------------------------------------------- |
| `"shopping part"`              | Physical component (affects stock, default) |
| `"information part / service"` | Service or info item (no stock impact)      |
| `"provision"`                  | Provision for third-party services          |

> [API Reference: Create Product Parts](https://developer.xentral.com/reference/productcreatepartsv2)
>
> **Reference:** [Verhalten von Stücklisten auf Belegen bestimmen](https://help.xentral.com/hc/de/articles/360016774219)

***

## External References (Foreign Numbers)

External references ("Fremdnummern") store identifiers from external systems on a product. Each reference can optionally be tied to a specific sales channel (target).

### When to Use

* **Multi-shop:** Each shop has its own product ID → store as external reference per channel
* **Barcode scanning:** Additional barcodes beyond the main EAN (set `isScannable: true`)
* **Marketplace IDs:** Amazon ASIN, eBay item number, etc.
* **Legacy systems:** Old product numbers from a previous ERP

### Creating an External Reference

> **Required Scope:** `productExternalReferences:create`

```bash
curl -X POST "https://{instance}.xentral.biz/api/v1/products/{productId}/externalReferences" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Amazon",
    "number": "B08N5WRWNW",
    "target": {"id": "1"},
    "isActive": true,
    "isScannable": true
  }'
```

> [API Reference: Create External Reference](https://developer.xentral.com/reference/externalreferencecreate)

| Field         | Required | Description                                                                                                             |
| ------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
| `name`        | No       | Label for this reference (e.g., shop name)                                                                              |
| `number`      | No       | The external ID / foreign number                                                                                        |
| `target`      | No       | Sales channel reference (`{"id": "..."}`). Optional — not every external reference needs to be tied to a sales channel. |
| `isActive`    | No       | Whether this reference is active                                                                                        |
| `isScannable` | No       | Whether this number can be used for barcode scanning in warehouse                                                       |

### Finding Products by External Reference

There is no global search endpoint for external references. External references are a **sub-resource of a product**, so you can only list them per product:

```bash
curl -s "https://{instance}.xentral.biz/api/v1/products/{productId}/externalReferences" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
```

> [API Reference: List External References](https://developer.xentral.com/reference/externalreferencelist)
>
> **Important:** To find a product by its external reference number, you need to **maintain a mapping** in your middleware (external number -> Xentral product ID). Store the product ID when you create the external reference, and use that mapping for lookups. This is the recommended pattern for multi-shop integrations.
>
> **Reference:** [Fremdnummern in Xentral](https://help.xentral.com/hc/en-us/articles/360016758199)

***

## Product Properties

Properties ("Eigenschaften") are custom attributes that can be assigned to products. Unlike free fields, properties are structured, reusable, and can be used for filtering in the UI.

**Examples:** Color, Material, Weight class, Voltage, Thread size

### How Properties Work

1. **Create a property definition** (global, reusable across products):

> **Required Scope:** `productProperties:create`

```bash
curl -X POST "https://{instance}.xentral.biz/api/v1/productsProperties" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '[{"name": "Material", "project": {"id": "1"}}]'
```

> [API Reference: Create Product Properties](https://developer.xentral.com/reference/productpropertycreate)

2. **Assign property values** to a specific product (update, not create):

> **No scope required** for this endpoint.

```bash
curl -X PATCH "https://{instance}.xentral.biz/api/v1/products/{productId}/properties" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '[{"name": "Material", "value": "Organic Cotton"}]'
```

> [API Reference: Update Product Properties](https://developer.xentral.com/reference/productpropertyupdatemultiple)

3. **Read properties** for a product:

> **No scope required** for this endpoint.

```bash
curl -s "https://{instance}.xentral.biz/api/v1/products/{productId}/properties" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
```

> **Note:** Properties are updated via PATCH on the product sub-endpoint, not created via POST. The global property definition must exist first.
>
> **Reference:** [Create and maintain product properties](https://help.xentral.com/hc/en-us/articles/5355878386972)

***

## Cross-Selling

Cross-selling links related products together for recommendations (e.g., "Customers also bought...").

> **Required Scope:** `productCrossSelling:create`

```bash
curl -X POST "https://{instance}.xentral.biz/api/v1/products/{productId}/crossSelling" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "product": {"id": "15"},
      "active": true,
      "assignToEachOther": true
    },
    {
      "product": {"id": "23"},
      "active": true,
      "assignToEachOther": false
    }
  ]'
```

> [API Reference: Create Cross-Selling](https://developer.xentral.com/reference/productcreatecrosssellingproducts)

| Field               | Required | Description                                                                                         |
| ------------------- | -------- | --------------------------------------------------------------------------------------------------- |
| `product`           | **Yes**  | Reference to the related product (`{"id": "..."}`)                                                  |
| `active`            | No       | Whether this cross-sell is active (default: `true`)                                                 |
| `assignToEachOther` | No       | If `true`, the cross-selling relationship is **bidirectional** — both products reference each other |

> **Tip:** Use `assignToEachOther: true` to create a mutual recommendation. Otherwise, only the source product will show the cross-sell reference.
>
> **Reference:** [Cross-selling](https://help.xentral.com/hc/en-us/articles/360016719640)

***

## Categories and Merchandise Groups

### Product Categories (Artikelkategorien)

Categories organize products in a **hierarchical tree** structure. A product can belong to multiple categories.

**List existing categories:**

> **Required Scope:** `productCategories:read`

```bash
curl -s "https://{instance}.xentral.biz/api/v2/productsCategories" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
```

> [API Reference: List Product Categories](https://developer.xentral.com/reference/productcategorylist)

**Create a category:**

> **Required Scope:** `productCategories:create`

```bash
curl -X POST "https://{instance}.xentral.biz/api/v2/productsCategories" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"name": "Hot Beverages", "parent": {"id": "1"}}'
```

**Assign categories during product creation (V2):**

```json
{
  "categories": [{"id": "1"}, {"id": "5"}]
}
```

> **Note:** The V2 API supports an array of categories, allowing a product to belong to multiple categories simultaneously.

### Merchandise Groups (Warengruppen)

Merchandise groups serve a different purpose than categories. They control:

* **Product number ranges** (each group can have its own number range)
* **DATEV revenue accounts** (different accounting for different product types)
* **Tax settings per product group**

**List existing groups:**

> **Required Scope:** `merchandiseGroup:read`

```bash
curl -s "https://{instance}.xentral.biz/api/v1/productsMerchandiseGroups" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
```

> [API Reference: List Merchandise Groups](https://developer.xentral.com/reference/productmerchandisegrouplist)

**Assign during product creation:**

```json
{
  "merchandiseGroup": {"id": "1"}
}
```

> **Key difference:** Categories are for **organization and shop navigation**. Merchandise groups are for **accounting and number ranges**. A product has one merchandise group but can have multiple categories.
>
> **Reference:** [Artikelnummernkreis und SKU (Modul Artikel Kategorien)](https://help.xentral.com/hc/de/articles/5355892884636)

***

## Sales Channel Settings and Product Texts

Sales channel settings control how a product appears in connected shops. General settings are on the product object; per-channel overrides use a separate endpoint.

> **Note:** This section covers both **sales channel settings** (visibility, stock sync, delivery time) and **product texts** (descriptions, SEO metadata), which also have per-sales-channel overrides.

### General Settings (on product creation)

> **No scope required** for sales channel settings on the product object.

```json
{
  "salesChannel": {
    "description": "Rich HTML shop description",
    "meta": {
      "title": "SEO Page Title",
      "description": "Meta description for search engines",
      "keywords": "coffee, organic, beans"
    },
    "isStockNumberSyncActive": true,
    "isSoldOut": false,
    "isVisible": true,
    "suggestedRetailPrice": "24.99",
    "deliveryTime": "2-3 business days"
  }
}
```

### Per-Channel Settings (separate endpoint)

For individual shop configurations that differ from the general settings:

```bash
curl -X POST "https://{instance}.xentral.biz/api/v1/products/{productId}/salesChannels" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "salesChannel": {"id": "1"},
    "isActive": true,
    "useIndividualSalesChannelSettings": true,
    "isStockNumberSyncActive": true,
    "suggestedRetailPrice": "22.99",
    "deliveryTime": "1-2 business days"
  }'
```

> [API Reference: Create Sales Channel Settings](https://developer.xentral.com/reference/productcreatesaleschannels)

| Field                               | Description                                         |
| ----------------------------------- | --------------------------------------------------- |
| `salesChannel`                      | Reference to the shop connection (`{"id": "..."}`)  |
| `isActive`                          | Whether the product is active in this channel       |
| `useIndividualSalesChannelSettings` | Override general settings for this specific channel |
| `isStockNumberSyncActive`           | Sync stock quantities to this shop                  |
| `suggestedRetailPrice`              | Recommended retail price (string)                   |
| `deliveryTime`                      | Delivery time text shown in shop                    |

***

## What the API Cannot Do

Understanding what is **not** available through a set of product-related endpoints prevents confusion:

### Stock Management

Stock quantities are managed through **warehouse endpoints**, not the product API:

```bash
# READ stock (works — no scope required)
GET /api/v1/products/{id}/stocks

# ADD stock via warehouse stock item endpoint
# See: https://developer.xentral.com/reference/warehousestockitem
```

> **Important:** While you can read stock via the product stocks endpoint, adding or modifying stock is done through warehouse operations (goods receipt, inventory counts, production completion, returns processing) or the `warehousestockitem` endpoint.

**Stock response structure:**

```json
{
  "data": {
    "totals": {
      "physical": 100.0,
      "reserved": 20.0,
      "sellable": 75.0,
      "calculated": 95.5,
      "openSalesOrders": 12.0,
      "correction": -5.0,
      "producible": 4.0
    },
    "warehouses": [
      {
        "warehouse": {"id": "1", "name": "Main Warehouse"},
        "physical": 80.0,
        "reserved": 15.0,
        "sellable": 60.0
      },
      {
        "warehouse": {"id": "2", "name": "External Warehouse"},
        "physical": 20.0,
        "reserved": 5.0,
        "sellable": 15.0
      }
    ],
    "...": "..."
  }
}
```

| Field             | Description                                                         |
| ----------------- | ------------------------------------------------------------------- |
| `physical`        | Actual quantity in warehouse                                        |
| `reserved`        | Reserved for existing sales orders                                  |
| `sellable`        | Available for new orders                                            |
| `openSalesOrders` | Quantity in open (unshipped) orders                                 |
| `correction`      | Manual stock corrections                                            |
| `producible`      | Quantity that could be produced from BOM parts                      |
| `warehouses`      | Stock breakdown per warehouse — shows how much is in each warehouse |

> **Note:** While `isStockItem: true` enables tracking, the actual stock quantities are managed through warehouse operations. After creating a product, `stockCount` will be `0` until goods are received.

### Other Limitations

| Operation                             | Status                                                                                                                                                                                            | Alternative                                                                                                      |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| Create project                        | Not available via API                                                                                                                                                                             | Create via Xentral UI                                                                                            |
| Set stock during product creation     | Not inline                                                                                                                                                                                        | Use warehouse endpoints (e.g., [warehousestockitem](https://developer.xentral.com/reference/warehousestockitem)) |
| Set prices during product creation    | Not inline                                                                                                                                                                                        | Use separate `POST /api/v3/salesPrices` endpoint (see [Pricing](#pricing))                                       |
| Upload images during product creation | Not inline                                                                                                                                                                                        | Use separate `POST /api/v1/productMedia` endpoint                                                                |
| Create suppliers                      | Not covered in this guide                                                                                                                                                                         | See supplier API endpoints                                                                                       |
| Storage locations                     | Read-only via `GET /products/{id}/storageLocations` — shows WHERE stock is located, not configurable via API. Once you put stock for the product on a location, it will show up on this endpoint. | Configure via Xentral UI                                                                                         |

***

## Migration Best Practices

| Practice                        | Details                                                                                  |
| ------------------------------- | ---------------------------------------------------------------------------------------- |
| **Test with one product first** | Validate your mapping before bulk import                                                 |
| **Use unique product numbers**  | Product numbers must be unique — duplicates cause `409 Conflict`                         |
| **Batch in small groups**       | Process 50-100 products, then verify before continuing                                   |
| **Monitor rate limits**         | Watch `X-RateLimit-Remaining` header (see [Rate Limiting](#rate-limiting-best-practice)) |
| **Store ID mapping**            | Keep a map of external ID → Xentral product ID in your middleware                        |
| **Prices separately**           | After product creation, create/update sales prices in a second pass                      |
| **Images separately**           | Upload product media after all products are created                                      |

***

## Error Handling

| Status | Meaning                | Common Cause                                                     |
| ------ | ---------------------- | ---------------------------------------------------------------- |
| 400    | Bad Request            | Validation error (missing required fields, invalid format)       |
| 401    | Unauthorized           | Invalid or expired token                                         |
| 403    | Forbidden              | Missing required scope (e.g., `product:create`)                  |
| 404    | Not Found              | Product ID doesn't exist (when updating or adding sub-resources) |
| 406    | Not Acceptable         | Missing `Accept: application/json` header                        |
| 409    | Conflict               | Product number already exists                                    |
| 415    | Unsupported Media Type | Wrong `Content-Type` header                                      |
| 429    | Too Many Requests      | Rate limit exceeded                                              |

> **Note:** Validation errors return `400 Bad Request`, not `422`.

### Common Validation Errors

**Missing required fields:**

```json
{
  "violations": {
    "name": ["Field 'name' is required."],
    "project": ["Field 'project' is required."]
  }
}
```

**Duplicate product number:**

```json
{
  "title": "A product with this number exists already."
}
```

**Missing supplier for purchase price (when no standard supplier is defined):**

```json
{
  "title": "Generic request validation failed.",
  "messages": ["Supplier missing and no standard supplier defined."]
}
```

### Rate Limiting Best Practice

Monitor the `X-RateLimit-Remaining` header to avoid hitting limits:

| Remaining | Recommended Action                                |
| --------- | ------------------------------------------------- |
| Above 50% | Full speed (no delay)                             |
| Below 50% | Add a small delay (e.g., 50ms) between requests   |
| Below 25% | Add a larger delay (e.g., 200ms) between requests |

***

## Related Resources

### API Documentation

* [productcreatev2](https://developer.xentral.com/reference/productcreatev2) - Create Product
* [productlistv2](https://developer.xentral.com/reference/productlistv2) - List Products
* [productviewv2](https://developer.xentral.com/reference/productviewv2) - View Product
* [productupdatev2](https://developer.xentral.com/reference/productupdatev2) - Update Product
* [productupdatemultiplev2](https://developer.xentral.com/reference/productupdatemultiplev2) - Bulk Update Products
* [externalreferencecreate](https://developer.xentral.com/reference/externalreferencecreate) - Create External Reference
* [productmediacreate](https://developer.xentral.com/reference/productmediacreate) - Upload Image
* [productcreatevariants](https://developer.xentral.com/reference/productcreatevariants) - Create Variants
* [productcreatepartsv2](https://developer.xentral.com/reference/productcreatepartsv2) - Create BOM Parts
* [productcreatecrosssellingproducts](https://developer.xentral.com/reference/productcreatecrosssellingproducts) - Create Cross-Selling
* [productcreatesaleschannels](https://developer.xentral.com/reference/productcreatesaleschannels) - Sales Channel Settings
* [postapi-v3-salesprices](https://developer.xentral.com/reference/postapi-v3-salesprices) - Create Sales Price (V3)
* [getapi-v3-salesprices](https://developer.xentral.com/reference/getapi-v3-salesprices) - List Sales Prices (V3)
* [patchapi-v3-salesprices](https://developer.xentral.com/reference/patchapi-v3-salesprices) - Update Sales Price (V3)
* [purchasepricecreate](https://developer.xentral.com/reference/purchasepricecreate) - Create Purchase Price
* [warehousestockitem](https://developer.xentral.com/reference/warehousestockitem) - Warehouse Stock Item
* [Authentication](https://developer.xentral.com/reference/authentication)
* [Rate Limiting](https://developer.xentral.com/reference/rate-limiting)

### Related Guides

* [G1: Create Sales Order](./G1_create-sales-order.md) - Create orders referencing products
* [G2: Create Customer](./G2_create-customer.md) - Create customers for orders

### Xentral Help Center

* [Artikel manuell anlegen](https://help.xentral.com/hc/de/articles/5355868375324) - Product creation in UI
* [Artikelstammdaten pflegen](https://help.xentral.com/hc/de/articles/5355878386972) - Product master data & properties
* [Matrix product: Create product variants](https://help.xentral.com/hc/en-us/articles/360016725120) - Variant creation
* [Artikelbilder verwenden](https://help.xentral.com/hc/de/articles/360020498940) - Product images
* [Create and maintain product properties](https://help.xentral.com/hc/en-us/articles/5355878386972) - Product properties
* [Artikelnummernkreis und SKU](https://help.xentral.com/hc/de/articles/5355892884636) - Number ranges via merchandise groups
* [Fremdnummern in Xentral](https://help.xentral.com/hc/en-us/articles/360016758199) - External references
* [Nummernkreise verwenden](https://help.xentral.com/hc/de/articles/5697692402204) - Number ranges
* [Einheiten für Artikel erstellen](https://help.xentral.com/hc/de/articles/360017611980) - Units
* [Verhalten von Stücklisten auf Belegen](https://help.xentral.com/hc/de/articles/360016774219) - BOM on documents
* [Cross-selling](https://help.xentral.com/hc/en-us/articles/360016719640) - Cross-selling