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

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:


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

TypeisStockItemhasBillOfMaterialsisMatrixProductUse Case
Simple producttruefalsefalseStandard physical goods
ServicefalsefalsefalseConsulting, installation
Matrix parentfalse-trueParent product defining size/color options. Not sold directly, no stock.
Matrix childtruefalsefalseOwn product with own SKU and stock. References parent via matrixProduct field.
JIT (Just-in-Time)falsetruefalseisAssembledJustInTime: true — No stock. Components are picked individually from warehouse.
Production producttruetruefalseManufactured in-house (isProductionProduct: true). Has stock after production completion.
Normal BOMtruetruefalseAssembled/bundled products. Has stock, picked as single item.
Shipping cost productfalsefalsefalseisShippingCostsProduct: true
Discount productfalsefalsefalseisDiscountProduct: 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 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

EndpointPOST /api/v2/products
MethodPOST
AuthBearer Token (Documentation)
Required Scopeproduct:create
Required Fieldsname, project.id
Product NumberAuto-assigned from number range, or provide custom via number field
Sales PricesSeparate endpoint: POST /api/v3/salesPrices (see Pricing)
Purchase PricesSeparate endpoint: POST /api/v2/purchasePrices (API Reference)
StockManaged 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

Full API Reference: Create Product


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

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)

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"

API Reference: List Sales Prices (V3)

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

API Reference: Update Sales Prices (V3)

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

WhatEndpointScope
Create sales pricePOST /api/v3/salesPricessalesPrice:create
Read sales pricesGET /api/v3/salesPricessalesPrice:read
Update sales pricesPATCH /api/v3/salesPricessalesPrice:update
Delete sales priceDELETE /api/v3/salesPrices/{id}salesPrice:delete
Create purchase pricePOST /api/v2/purchasePricespurchasePrice:create
Read purchase prices per productGET /api/v2/products/{id}/purchasePricespurchasePrice:read
Update purchase pricePATCH /api/v2/purchasePrices/{id}purchasePrice:update
Delete purchase priceDELETE /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:

ScenarioNumber Range Used
No specific number range configuredSystem-wide product number range
Project has own number rangeProject's product number range
Merchandise group (Artikelkategorien) with own number rangeMerchandise group's number range
Merchandise group with useMainProductNumberRange: trueSystem-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 Conflict error:

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

Best Practices for Number Ranges

ScenarioApproach
New integrationLet Xentral assign numbers automatically
Data migrationProvide existing numbers, ensure no duplicates
Multi-shopUse 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:

SettingUse CaseEffect
truePhysical goods (shipped from warehouse)Stock tracked, reservations created on orders
false (default)Services, digital products, shipping costsNo 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.

Decision 2: Simple product or variants?

TypeUse CaseAPI Approach
Simple productSingle SKU, no size/color optionsStandard 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?

TypehasBillOfMaterialsisAssembledJustInTimeHas Stock?Behavior
Simple productfalsefalseYes (if isStockItem)No components
Normal BOMtruefalseYesStock is managed on the BOM product. Components are picked from warehouse internally. Same sales process as simple product.
JIT (Just-in-Time)truetrueNoNo stock on the JIT product itself. Components are picked individually from the warehouse and listed separately on documents.
Production producttruefalseYes (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:

OptionApproachBest for
External referencesPOST /api/v1/products/{id}/externalReferencesMulti-shop setups with separate IDs per channel
EAN fieldSet ean on the productProducts with standard barcodes
Product numberUse your shop SKU as the number fieldSingle-shop with unique SKUs
Free fieldsUse freeFields for custom mappingsLegacy 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.

Decision 5: Batch tracking or serial numbers?

FeatureFieldValuesUse Case
Batch trackinghasBatchestrue / falseFood (best-before dates), pharmaceuticals, chemicals
Best-before dateshasBestBeforeDatetrue / falsePerishable goods. Can be used with or without hasBatches.
Serial numbersserialNumbersMode"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"

API Reference: List Products

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

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

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
  }'

API Reference: Create Sales Price (V3)

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}"
  }'

API Reference: Create Product Media

Image keywords:

KeywordPurposeIn Xentral UI
defaultImageMain product image (shown in lists, documents)"Standardbild"
printImageImage used on printed documents (offers, invoices)"Druckbild"
labelImageImage used on labels"Labelbild"
otherImageAdditional 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 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
  }'

API Reference: Create External Reference


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"

API Reference: View Product

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. stockCount will be 0 after 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"

API Reference: List Sales Prices (V3)

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"}'

API Reference: Update Product

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"}
  ]'

API Reference: Bulk Update Products


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"}}
        ]
      }
    ]
  }'

API Reference: Create Variants

Variant Numbering Modes

The mode field controls how variant product numbers are generated:

ModeNameExample (parent: TSHIRT)
"1"Merchandise group number range100042, 100043, ...
"2"Options appended to main numberTSHIRT-S-Red, TSHIRT-M-Blue
"3"Main number + appendix (no separator)TSHIRT001, TSHIRT002
"4"Suffix with configurable separator/digitsTSHIRT-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., 3001)
  • 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

TypeFieldsHas Stock?Behavior on Documents
Normal BOMhasBillOfMaterials: trueYesProduct appears as single line item. Picked normally from warehouse.
JIT (Just-in-Time)hasBillOfMaterials: true, isAssembledJustInTime: trueNoComponents are expanded on delivery notes and invoices — each part appears as its own line. Components are picked individually from the warehouse.
Production BOMhasBillOfMaterials: true, isProductionProduct: trueYesSame 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

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:

TypeDescription
"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

Reference: Verhalten von Stücklisten auf Belegen bestimmen


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
  }'

API Reference: Create External Reference

FieldRequiredDescription
nameNoLabel for this reference (e.g., shop name)
numberNoThe external ID / foreign number
targetNoSales channel reference ({"id": "..."}). Optional — not every external reference needs to be tied to a sales channel.
isActiveNoWhether this reference is active
isScannableNoWhether 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

  1. 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"}}]'

API Reference: Create Product Properties

  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"}]'

API Reference: Update Product Properties

  1. 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
    }
  ]'

API Reference: Create Cross-Selling

FieldRequiredDescription
productYesReference to the related product ({"id": "..."})
activeNoWhether this cross-sell is active (default: true)
assignToEachOtherNoIf 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


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"

API Reference: List Product Categories

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"

API Reference: List Merchandise Groups

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"
  }'

API Reference: Create Sales Channel Settings

FieldDescription
salesChannelReference to the shop connection ({"id": "..."})
isActiveWhether the product is active in this channel
useIndividualSalesChannelSettingsOverride general settings for this specific channel
isStockNumberSyncActiveSync stock quantities to this shop
suggestedRetailPriceRecommended retail price (string)
deliveryTimeDelivery 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/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:

{
  "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
      }
    ],
    "...": "..."
  }
}
FieldDescription
physicalActual quantity in warehouse
reservedReserved for existing sales orders
sellableAvailable for new orders
openSalesOrdersQuantity in open (unshipped) orders
correctionManual stock corrections
producibleQuantity that could be produced from BOM parts
warehousesStock 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

OperationStatusAlternative
Create projectNot available via APICreate via Xentral UI
Set stock during product creationNot inlineUse warehouse endpoints (e.g., warehousestockitem)
Set prices during product creationNot inlineUse separate POST /api/v3/salesPrices endpoint (see Pricing)
Upload images during product creationNot inlineUse separate POST /api/v1/productMedia endpoint
Create suppliersNot covered in this guideSee supplier API endpoints
Storage locationsRead-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

PracticeDetails
Test with one product firstValidate your mapping before bulk import
Use unique product numbersProduct numbers must be unique — duplicates cause 409 Conflict
Batch in small groupsProcess 50-100 products, then verify before continuing
Monitor rate limitsWatch X-RateLimit-Remaining header (see Rate Limiting)
Store ID mappingKeep a map of external ID → Xentral product ID in your middleware
Prices separatelyAfter product creation, create/update sales prices in a second pass
Images separatelyUpload product media after all products are created

Error Handling

StatusMeaningCommon Cause
400Bad RequestValidation error (missing required fields, invalid format)
401UnauthorizedInvalid or expired token
403ForbiddenMissing required scope (e.g., product:create)
404Not FoundProduct ID doesn't exist (when updating or adding sub-resources)
406Not AcceptableMissing Accept: application/json header
409ConflictProduct number already exists
415Unsupported Media TypeWrong Content-Type header
429Too Many RequestsRate limit exceeded

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

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:

RemainingRecommended 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

Related Guides

Xentral Help Center