Products: Endpoint
Products
Complete CRUD operations for product data.
Overview
| Operation | Method | Endpoint | Scope |
|---|---|---|---|
| Export All | GET | /xpanel/xoport/export/json/products | products:read |
| Single product | GET | /xpanel/xoport/export/json/product/{ID} | products:read |
| ID Range | GET | /xpanel/xoport/export/json/products/{START}:{END} | products:read |
| Import | POST | /xpanel/xoport/import/json/products | products:write |
| Update | PUT | /xpanel/xoport/import/json/products | products:write |
| Delete | DELETE | /xpanel/xoport/delete/json/products | products:delete |
Export (GET)
/product/{ID} (singular) for a single product.URL Patterns
| URL | Description |
|---|---|
/export/json/product/123 | Single product with ID 123 |
/export/json/products | All products (paginated) |
/export/json/products/100:200 | Products with IDs 100 through 200 |
Query Parameters
| Parameter | Type | Description |
|---|---|---|
limit | int | Max. number (Default: 50, Max: 250) |
offset | int | Start position for pagination |
Example Requests
# Einzelprodukt abrufen
curl -X GET "https://shop.de/xpanel/xoport/export/json/product/123" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# Alle Produkte paginiert
curl -X GET "https://shop.de/xpanel/xoport/export/json/products?limit=50&offset=0" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# ID-Range
curl -X GET "https://shop.de/xpanel/xoport/export/json/products/100:200" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"Response Fields
Each product contains:
products_id,products_model,products_eanproducts_price,products_weight,products_quantity,products_costmanufacturers_id,products_statuscontract,contract_use_current_price,contract_optionv2.5.0products_inventory_management,products_min_qty,products_max_qty,products_qty_blocks,products_base_pricev2.5.0service,sitemap,non_cart,non_price,products_free_shippingv2.5.0shipping_profile,products_unit_id,base_unit_id,products_sort_orderv2.5.0images[]- Product images with sortingcategories[]- Category IDsgroups[]- Customer group pricescustomers_prices[]- Customer-specific pricesspecials[]- Special pricesextra_fields[]- Additional fieldsbadges[]- Product badgesxsells[]- Cross-selling productsupsells[]- Shopping cart suggestions (“Goes well with this”) v2.18.0+tabs[]- Product tabsvideos[]- Product videossuppliers[]- Suppliers:suppliers_idandproducts_model(the supplier's article number). The list replaces all of the product's assignments; an entry withoutproducts_modelclears the number.storages[]- Inventory by warehouse v2.63.0+languages{}- Name, description, SEO per language:products_name,products_description,products_short_descriptionproducts_head_title_tag,products_head_desc_tag,products_head_keywords_tagseo_name,products_url,products_checkout_description,products_image_title
options{}- Attributes without attribute groups (option name + values per language, SKU data per value)option_groups[]- Combined attribute groups (e.g., linked “Color + Size”), SKU data per combination v2.58.0+contenteditor{}- ContentEditor data per language
Attributes (Options)
/Export/JSON/products_attributes or
/Export/JSON/products_options therefore return the correct response:
501 Method not found.Depending on the attribute type, the export provides two different structures:
| Field | Contains | SKU data (model/ean/quantity/price) belongs to … |
|---|---|---|
options{} | Attributes without an attribute group | Singlevalue |
option_groups[] v2.58.0+ | Combined attribute groups, e.g., linked “Color + Size” | Combination of values |
There are intentionally two fields: for a characteristic group, there is one SKU per combination (“black / L” = one EAN), not per individual value. Merging them would create item numbers that do not exist. A product can have values in both fields.
"option_groups": [
{
"group": 1,
"options": [
{ "id": 27, "languages": { "de": { "name": "Farbe" } } },
{ "id": 26, "languages": { "de": { "name": "Größe" } } }
],
"combinations": [
{
"detail_id": 2344,
"values": [
{ "options_id": 27, "options_values_id": 2483, "languages": { "de": { "name": "schwarz" } } },
{ "options_id": 26, "options_values_id": 304, "languages": { "de": { "name": "L" } } }
],
"model": "MS-01-0040-001-L",
"ean": "4001",
"quantity": "5",
"price": "0.0000",
"sort_id": 1
}
]
}
]
options[]Listed in display order;combinations[].values[]follows the same order—position 0 is always the first group member.detail_id: 0Empty SKU fields mean: nothing is maintained for this combination (not an error).- The import continues to expect the flat
options[]format (v2.39.0)—convert on the client side for round trips.
Import (POST/PUT)
POST creates new products; PUT updates existing ones.
languages format (consistent with export). For INSERT (POST), missing languages are automatically populated with data from the master language (auto-fill).contract, contract_use_current_price), logistics (products_cost, products_min_qty, products_ean), and flags (service, sitemap, non_cart, non_price).specials[]. Supported URLs include customers_group_id, specials_new_products_price, or discount_percent, specials_begin,specials_end, specials_price_id,specials_price_name, specials_type, and deletion via action: delete.upsells[]. Accepts an array of products_model strings or objects with {products_model, sort_order}. Replace semantics—existing entries are replaced. Self-references and unknown models are silently skipped.warranty_period (an empty string "" overrides the default 24m), products_xoport (markers, e.g., googleblacklist), images[] (additional images by filename, replaces the gallery, including alt text), storages[] (Per-warehouse inventory when multi-warehouse is active—fields, semantics, and pitfalls in the "Inventory & Reorder Levels per Warehouse" section) and downloads[] (product downloads, d_src = file at files/). POST /media uploads files using entity_type=download to files/.warranty_type can now be imported— physical, and digital are allowed; the spelling is corrected. The array_key_exists semantics apply: if the key is missing, the column remains unchanged; an empty string "" clears it. ⚠ An unknown value is rejected with a warning in warnings[] and is not automatically corrected—this field helps determine whether an item is classified as a physical product or digital content.groups[]): For each product, different prices can be imported for each customer group (e.g., retailer groups). groups is an array of objects (not an ID list): each entry requires customers_group_id (> 0) and sets the net price for the group using customers_group_price —the gross price is automatically calculated based on the tax class. Optional tiered pricing: products_price1…products_price8 + products_price1_qty…products_price8_qty. Performs an upsert for each group; groups not included remain unchanged. The group “ 0 ” = “End customers” is set via products_price. You can determine the group IDs via the product export (field groups).products_customers_prices) can be imported via customers_prices[] —including tiered pricing and the ability to selectively delete individual customer prices.Customer-Specific Prices (customers_prices[]) v2.57.0
For each product, different prices can be imported for individual customers. The customer is identified via customers_id or customers_id_extern (WaWi customer number).
{
"type": "products",
"data": [{
"products_model": "ABC-123",
"customers_prices": [
{"customers_id": 48, "customers_price": "9,99", "products_price1": 9.5, "products_price1_qty": 10},
{"customers_id_extern": "20074", "customers_price": 8.88},
{"customers_id": 55, "_action": "delete"}
]
}]
}- Partial Upsert: Only the columns included in the import are written. Available:
customers_price(aliasprice),products_price1…products_price12+products_price1_qty…products_price12_qty(tiered pricing),products_qty_blocks,products_min_qty,products_max_qty. - Customer Addressing: via
customers_idorcustomers_id_extern— the WaWi customer number must be unique; ambiguous numbers trigger a warning, and the entry is skipped. _action: "delete"Removes the customer price for the respective customer.customers_prices_replace: true= Full sync: First, all customer prices for the product are deleted; then the ones sent along are written. Ifcustomers_prices: []is empty, all customer prices for the product are removed.- Insert defaults: If
customers_priceis missing, the current product price is used; quantity blocks and minimum quantity = 1. customers_price≤ 0 is ignored (warning)—removal occurs exclusively via `_action: "delete"`.
customers_prices `—exported data can be re-imported unchanged.Inventory Levels & Reorder Points per Warehouse (storages[]) v2.45.0
When the multi-warehouse system is active, inventory, safety stock, reorder point, and storage location are maintained for each warehouse (table products_to_storages). The array has been available since v2.34.0; starting with v2.45.0, only the fields that are actually passed are written—a partial payload (e.g., only ` products_reorder_level`) no longer sets the remaining columns for this warehouse to ` 0`.
| Field | Type | Description |
|---|---|---|
storages_id (Required) | int | ID of the additional warehouse from the " storages " table (name in storages_info; maintained in the backend under Products → Warehouse Management). Alias: warehouse_id. |
products_quantity | decimal(16,8) | Stock in this warehouse. |
products_safe_quantity | decimal(16,8) | Safety stock for this warehouse. |
products_reorder_level | decimal(16,8) | Reorder point for this warehouse. |
products_storage_text | varchar(50) | Storage location as free text, max. 50 characters. Is not automatically truncated. |
options_id | int, Default 0 | Characteristic/variant inventory. 0 = Inventory without characteristic reference. |
Set only the reorder point for a warehouse —all other values for this warehouse remain unchanged:
{
"type": "products",
"data": [{
"products_model": "ABC-123",
"storages": [
{"storages_id": 3, "products_reorder_level": 10}
]
}]
}Complete example with two warehouses, a characteristic inventory, and a separately set total inventory:
{
"type": "products",
"data": [{
"products_model": "ABC-123",
"products_quantity": 42,
"storages": [
{"storages_id": 1, "products_quantity": 30, "products_safe_quantity": 2, "products_reorder_level": 10, "products_storage_text": "Regal A-04"},
{"storages_id": 3, "products_quantity": 12, "products_reorder_level": 5},
{"storages_id": 3, "options_id": 2483, "products_quantity": 4}
]
}]
}- Partial update (as of v2.45.0): Only fields present in the payload (
INSERT … ON DUPLICATE KEY UPDATE) are written. Up to v2.44.0, the warehouse row was replaced usingREPLACE—omitted columns were set to0. - Key per inventory line:
(storages_id, products_id, options_id). If the triple matches, the existing line is updated; otherwise, a new row is inserted. - No full mirroring: Inventory rows not in the array remain unchanged. There is no `
storages_replace` and no `_action: "delete"`—removing inventory rows is only possible in the backend. - No-op without user data: An entry containing only the key writes nothing and, in particular, does not create a row with zero values.
nullis not “unchanged”: A field with the valuenullis considered to have been passed and writes0. If a value is to remain unchanged, omit the field.- New warehouse row: Include inventory. If no row exists yet for the triplet, it is created solely from the fields passed. Therefore, when populating for the first time, send
products_quantityandproducts_safe_quantitywith — otherwise, the database server may reject the record depending on the configuration without the response reporting an error. options_id: Default0. Values greater than 0 maintain the inventory of a characteristic/variant combination in this warehouse; product and characteristic inventory are separate rows.- The decimal separator is the period: `
"50.5"` is valid; `"50,5"` is interpreted as `50`. - Prerequisite
EXTENDED_STORAGE_SYSTEM = 'true': If the extended warehouse system is deactivated,storagesis completely ignored; the response contains the warningSTORAGE_SYSTEM_DISABLED. - Unknown or inactive warehouse: An `
storages_id` that does not exist or whose warehouse is inactive triggers the warning `STORAGE_NOT_FOUND`; the entry is skipped, and the remaining entries continue to be processed. - Caution — missing
storages_idis silently discarded: Entries withoutstorages_id/warehouse_idor with a value of≤ 0are skipped without a warning. The import then reports success, even though no inventory levels were written.
storages: The “main warehouse” reorder point from the product editor is the product column products.products_reorder_level —a different field from storages[]. Starting with v2.63.0, it can be imported along with products_safe_quantity as a top-level field for the product; in older versions, it can only be imported via the backend or the CSV/XML column reorder_level. storages_id: 1 is already the first auxiliary warehouse —if you expect the reorder point for the main warehouse to appear there, you are entering it in the wrong warehouse.products_quantity Is not calculated as a sum of the warehouse levels: The product’s total inventory is an independent column. If you set warehouse inventories via ` storages `, you must separately include ` products_quantity ` in the same request—otherwise, the old total inventory will remain.storages[] `—with the same set of fields—so exported inventory data can be reimported unchanged. If the extended inventory system is disabled, the key is missing entirely (not as an empty array). All inventory lines for the product are output, including those for inactive warehouses— STORAGE_NOT_FOUND then confirms their re-import. Up to v2.62.0, the export did not include inventory levels; in those versions, you’ll need to use the CSV Porter (columns storage_<ID>_*) or the backend.Example Request
curl -X POST "https://shop.de/xpanel/xoport/import/json/products" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "products",
"data": [{
"products_model": "TEST-001",
"products_price": 99.99,
"products_quantity": 50,
"manufacturers_id": 1,
"contract": 1,
"contract_use_current_price": 1,
"products_ean": "4012345678901",
"categories": [5, 12],
"groups": [
{"customers_group_id": 2, "customers_group_price": 1975.00},
{"customers_group_id": 3, "customers_group_price": 1975.00}
],
"suppliers": [{"suppliers_id": 3, "products_model": "LF-4711"}],
"specials": [{
"customers_group_id": 0,
"discount_percent": 10,
"specials_price_name": {"de": "Muttertag", "en": "Mother Day"},
"status": 1,
"specials_begin": "2026-05-05 00:00:00",
"specials_end": "2026-05-11 23:59:59"
}],
"languages": {
"de": {
"products_name": "Testprodukt",
"products_description": "Beschreibung...",
"products_head_title_tag": "SEO Titel",
"products_head_desc_tag": "Meta Description",
"seo_name": "testprodukt"
}
}
}]
}'Delete (DELETE)
Query Parameters
| Parameters | Type | Description |
|---|---|---|
products_id | int | Product ID to delete |
products_model | string | Item number to delete |
Example
curl -X DELETE "https://shop.de/xpanel/xoport/delete/json/products?products_id=123" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"