Create a product (V2 API)
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
- ERP Context
- At a Glance
- Pricing
- Product Number Assignment
- Prerequisites
- Before You Start
- Workflow
- Step-by-Step Guide
- Product Variants (Matrix Products)
- Bill of Materials (BOM)
- External References (Foreign Numbers)
- Product Properties
- Cross-Selling
- Categories and Merchandise Groups
- Sales Channel Settings and Product Texts
- What the API Cannot Do
- Error Handling
- 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 - Create API Token
- 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)
- Purchase Prices - Supplier pricing with quantity breaks (see Pricing)
- Bill of Materials (BOM) - Component list for JIT or production products (see BOM)
- Production Orders - Manufacturing instructions (V3 API)
- External References - Shop IDs, barcodes, foreign numbers (see External References)
- Properties - Custom attributes like color, material, weight class (see Properties)
- Cross-Selling - Related product recommendations (see Cross-Selling)
- Sales Channels - Per-shop settings for visibility, SEO, delivery time (see Sales Channel Settings)
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)
- 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
idis 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) |
| 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) |
| Purchase Prices | Separate endpoint: POST /api/v2/purchasePrices (API Reference) |
| Stock | Managed through warehouse endpoints (see warehousestockitem) |
Minimal example:
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
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
customerorcustomerGroupon a sales price, but not both at the same time.
Create a base sales price:
Required Scope:
salesPrice:create
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
}'Response: 201 Created — The response body contains the created resource with its ID.
Create a customer-specific quantity break:
Required Scope:
salesPrice:create
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
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"Response:
{
"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
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
Purchase Prices
Purchase prices are used in purchase orders. They require a supplier reference.
Required Scope:
purchasePrice:create
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
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:
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 Conflicterror:{"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
Prerequisites
- Xentral account with API access
- Personal Access Token (PAT) with scope
product:create(Guide) - 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: truedoes NOT set initial stock quantities. Stock is managed through warehouse operations (goods receipt, inventory counts). See 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 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
hideJustInTimeItemsOnDocumentsflag 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/productswith filters to search for existing products by number, EAN, or name before creating duplicates. See 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
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"Response:
{
"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
namefield supportscontainsandstartsWithoperators on the products endpoint (unlike customers V2, which only supportsequals).
Step 2: Create Product
Minimal product (just name and project):
Required Scope:
product:create
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"}
}'Response: 201 Created
The product ID is in the Location header:
Location: https://{instance}.xentral.biz/api/v2/products/42
More complex product example:
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.
Step 3: Add Prices
After creating the product, add sales prices and optionally purchase prices via separate endpoints. See Pricing for the full documentation.
Quick example — create a base sales price:
Required Scope:
salesPrice:create
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
}'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
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}"
}'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}/versionsto 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 for the full documentation.
Required Scope:
productExternalReferences:create
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
}'Step 6: Verify Product
Required Scope:
product:read
curl -s "https://{instance}.xentral.biz/api/v2/products/{productId}" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json"Response:
{
"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.
stockCountwill be0after creation. Stock is managed through warehouse operations, not the product API.
Check sales prices:
Required Scope:
salesPrice:read
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"Check stock levels:
No scope required for this endpoint.
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
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"}'Response: 204 No Content - The update was successful. No body is returned.
Bulk update (multiple products):
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"}
]'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
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
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"}}
]
}
]
}'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
# 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
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
hideJustInTimeItemsOnDocumentsflag 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
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 |
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
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
}'| 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:
curl -s "https://{instance}.xentral.biz/api/v1/products/{productId}/externalReferences" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json"API Reference: List External References
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
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
- Create a property definition (global, reusable across products):
Required Scope:
productProperties:create
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"}}]'- Assign property values to a specific product (update, not create):
No scope required for this endpoint.
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"}]'- Read properties for a product:
No scope required for this endpoint.
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
Cross-Selling
Cross-selling links related products together for recommendations (e.g., "Customers also bought...").
Required Scope:
productCrossSelling:create
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
}
]'| 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: trueto create a mutual recommendation. Otherwise, only the source product will show the cross-sell reference.Reference: Cross-selling
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
curl -s "https://{instance}.xentral.biz/api/v2/productsCategories" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json"Create a category:
Required Scope:
productCategories:create
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):
{
"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
curl -s "https://{instance}.xentral.biz/api/v1/productsMerchandiseGroups" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json"Assign during product creation:
{
"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)
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.
{
"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:
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"
}'| 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:
# 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/warehousestockitemImportant: 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
warehousestockitemendpoint.
Stock response structure:
{
"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: trueenables tracking, the actual stock quantities are managed through warehouse operations. After creating a product,stockCountwill be0until 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) |
| Set prices during product creation | Not inline | Use separate POST /api/v3/salesPrices endpoint (see 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) |
| 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, not422.
Common Validation Errors
Missing required fields:
{
"violations": {
"name": ["Field 'name' is required."],
"project": ["Field 'project' is required."]
}
}Duplicate product number:
{
"title": "A product with this number exists already."
}Missing supplier for purchase price (when no standard supplier is defined):
{
"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 - Create Product
- productlistv2 - List Products
- productviewv2 - View Product
- productupdatev2 - Update Product
- productupdatemultiplev2 - Bulk Update Products
- externalreferencecreate - Create External Reference
- productmediacreate - Upload Image
- productcreatevariants - Create Variants
- productcreatepartsv2 - Create BOM Parts
- productcreatecrosssellingproducts - Create Cross-Selling
- productcreatesaleschannels - Sales Channel Settings
- postapi-v3-salesprices - Create Sales Price (V3)
- getapi-v3-salesprices - List Sales Prices (V3)
- patchapi-v3-salesprices - Update Sales Price (V3)
- purchasepricecreate - Create Purchase Price
- warehousestockitem - Warehouse Stock Item
- Authentication
- Rate Limiting
Related Guides
- G1: Create Sales Order - Create orders referencing products
- G2: Create Customer - Create customers for orders
Xentral Help Center
- Artikel manuell anlegen - Product creation in UI
- Artikelstammdaten pflegen - Product master data & properties
- Matrix product: Create product variants - Variant creation
- Artikelbilder verwenden - Product images
- Create and maintain product properties - Product properties
- Artikelnummernkreis und SKU - Number ranges via merchandise groups
- Fremdnummern in Xentral - External references
- Nummernkreise verwenden - Number ranges
- Einheiten für Artikel erstellen - Units
- Verhalten von Stücklisten auf Belegen - BOM on documents
- Cross-selling - Cross-selling
Updated 1 day ago