Products Endpoint
Products
Vollständige CRUD-Operationen für Produktdaten.
Übersicht
| Operation | Method | Endpoint | Scope |
|---|---|---|---|
| Alle exportieren | GET | /xpanel/xoport/export/json/products | products:read |
| Einzelprodukt | 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) für ein einzelnes Produkt.URL-Patterns
| URL | Beschreibung |
|---|---|
/export/json/product/123 | Einzelnes Produkt mit ID 123 |
/export/json/products | Alle Produkte (paginiert) |
/export/json/products/100:200 | Produkte mit ID 100 bis 200 |
Query-Parameter
| Parameter | Typ | Beschreibung |
|---|---|---|
limit | int | Max. Anzahl (Default: 50, Max: 250) |
offset | int | Startposition für Pagination |
Beispiel-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-Felder
Jedes Produkt enthält:
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[]- Produktbilder mit Sortierungcategories[]- Kategorie-IDsgroups[]- Kundengruppen-Preisecustomers_prices[]- Kundenindividuelle Preisespecials[]- Sonderpreiseextra_fields[]- Zusatzfelderbadges[]- Produkt-Badgesxsells[]- Cross-Selling-Produkteupsells[]- Warenkorb-Ergänzungen („Passt dazu“) v2.18.0+tabs[]- Produkttabsvideos[]- Produktvideossuppliers[]- Lieferanten:suppliers_idundproducts_model(Artikelnummer des Lieferanten). Die Liste ersetzt alle Zuordnungen des Artikels, ein Eintrag ohneproducts_modelleert die Nummer.storages[]- Bestand je Lager v2.63.0+languages{}- Name, Beschreibung, SEO pro Sprache: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{}- Merkmale ohne Merkmalsgruppe (Optionsname + Werte je Sprache, SKU-Daten je Wert)option_groups[]- kombinierte Merkmalsgruppen (z. B. gekoppeltes „Farbe + Größe“), SKU-Daten je Kombination v2.58.0+contenteditor{}- ContentEditor-Daten pro Sprache
Merkmale (Optionen)
/Export/JSON/products_attributes oder
/Export/JSON/products_options antworten deshalb korrekt mit
501 Method not found.Je nach Merkmalstyp liefert der Export zwei unterschiedliche Strukturen:
| Feld | Enthält | SKU-Daten (model/ean/quantity/price) gehören zum … |
|---|---|---|
options{} | Merkmale ohne Merkmalsgruppe | Einzelwert |
option_groups[] v2.58.0+ | kombinierte Merkmalsgruppen, z. B. gekoppeltes „Farbe + Größe“ | Kombination der Werte |
Es sind bewusst zwei Felder: bei einer Merkmalsgruppe existiert je Kombination eine SKU („schwarz / L“ = eine EAN), nicht je Einzelwert. Ein Zusammenfalten würde Artikelnummern erzeugen, die es nicht gibt. Ein Produkt kann beide Felder tragen.
"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[]steht in Anzeigereihenfolge;combinations[].values[]folgt derselben Reihenfolge — Position 0 ist immer das erste Gruppenmitglied.detail_id: 0mit leeren SKU-Feldern bedeutet: für diese Kombination ist nichts gepflegt (kein Fehler).- Der Import erwartet weiterhin das flache
options[]-Format (v2.39.0) — für Roundtrips clientseitig umwandeln.
Import (POST/PUT)
POST erstellt neue Produkte, PUT aktualisiert bestehende.
languages-Format übergeben (konsistent mit Export). Bei INSERT (POST) werden fehlende Sprachen automatisch mit den Daten der Master-Language befüllt (Auto-Fill).contract, contract_use_current_price), Logistik (products_cost, products_min_qty, products_ean) und Flags (service, sitemap, non_cart, non_price).specials[] importiert werden. Unterstützt werden customers_group_id, specials_new_products_price oder discount_percent, specials_begin/specials_end, specials_price_id/specials_price_name, specials_type und Löschung via action: delete.upsells[] importiert werden. Akzeptiert ein Array von products_model-Strings oder Objekten mit {products_model, sort_order}. Replace-Semantik — bestehende Einträge werden ersetzt. Self-Referenzen und unbekannte Modelle werden stillschweigend übersprungen.warranty_period (leerer String "" überschreibt den Standard 24m), products_xoport (Marker, z. B. googleblacklist), images[] (Zusatzbilder per Dateiname, ersetzt die Galerie, inkl. Alt-Texte), storages[] (Pro-Lager-Bestand bei aktivem Mehrlager — Felder, Semantik und Fallstricke im Abschnitt Lagerbestände & Meldebestände je Lager) und downloads[] (Produkt-Downloads, d_src = Datei in files/). Dateien lädt POST /media mit entity_type=download nach files/ hoch.warranty_type ist jetzt importierbar – zulässig sind physical und digital, die Schreibweise wird geheilt. Es gilt array_key_exists-Semantik: fehlt der Schlüssel, bleibt die Spalte unangetastet, ein leerer String "" leert sie. ⚠ Ein unbekannter Wert wird mit einer Warnung in warnings[] abgelehnt und nicht zurechtgebogen – das Feld entscheidet mit darüber, ob eine Position als Ware oder als digitaler Inhalt gilt.groups[]): Pro Produkt lassen sich abweichende Preise je Kundengruppe (z. B. Händlergruppen) importieren. groups ist ein Array von Objekten (keine ID-Liste): jeder Eintrag benötigt customers_group_id (> 0) und setzt mit customers_group_price den Netto-Preis der Gruppe — der Bruttopreis wird automatisch aus der Steuerklasse berechnet. Optionale Staffelpreise: products_price1…products_price8 + products_price1_qty…products_price8_qty. Upsert je Gruppe; nicht enthaltene Gruppen bleiben unverändert. Gruppe 0 = Endkunden wird über products_price gesetzt. Die Gruppen-IDs ermitteln Sie per Produkt-Export (Feld groups).products_customers_prices) können über customers_prices[] importiert werden — inkl. Staffelpreisen und gezieltem Löschen einzelner Kundenpreise.Kundenindividuelle Preise (customers_prices[]) v2.57.0
Pro Produkt lassen sich abweichende Preise für einzelne Kunden importieren. Der Kunde wird per customers_id oder customers_id_extern (WaWi-Kundennummer) adressiert.
{
"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: Nur mitgesendete Spalten werden geschrieben. Verfügbar:
customers_price(Aliasprice),products_price1…products_price12+products_price1_qty…products_price12_qty(Staffelpreise),products_qty_blocks,products_min_qty,products_max_qty. - Kunden-Adressierung: per
customers_idodercustomers_id_extern— die WaWi-Kundennummer muss eindeutig sein; mehrdeutige Nummern erzeugen eine Warnung, der Eintrag wird übersprungen. _action: "delete"entfernt den Kundenpreis des jeweiligen Kunden.customers_prices_replace: true= Vollspiegel: erst werden alle Kundenpreise des Produkts gelöscht, dann die mitgesendeten geschrieben. Zusammen mit leeremcustomers_prices: []werden alle Kundenpreise des Produkts entfernt.- Insert-Defaults: Fehlt
customers_price, wird der aktuelle Produktpreis übernommen; Mengenblöcke und Mindestmenge = 1. customers_price≤ 0 wird ignoriert (Warnung) — Entfernen erfolgt ausschließlich über_action: "delete".
customers_prices schon immer mit — exportierte Daten können unverändert re-importiert werden.Lagerbestände & Meldebestände je Lager (storages[]) v2.45.0
Bei aktivem Mehrlager-System werden Bestand, Sicherheitsbestand, Meldebestand und Lagerplatz je Lager gepflegt (Tabelle products_to_storages). Das Array gibt es seit v2.34.0; seit v2.45.0 werden nur die tatsächlich übergebenen Felder geschrieben — ein Teil-Payload (z. B. nur products_reorder_level) setzt die übrigen Spalten dieses Lagers nicht mehr auf 0.
| Feld | Typ | Beschreibung |
|---|---|---|
storages_id (Pflicht) | int | ID des Zusatzlagers aus Tabelle storages (Bezeichnung in storages_info, Pflege im Backend unter Produkte → Lagerverwaltung). Alias: warehouse_id. |
products_quantity | decimal(16,8) | Bestand in diesem Lager. |
products_safe_quantity | decimal(16,8) | Sicherheitsbestand dieses Lagers. |
products_reorder_level | decimal(16,8) | Meldebestand dieses Lagers. |
products_storage_text | varchar(50) | Lagerplatz als Freitext, max. 50 Zeichen. Wird nicht automatisch gekürzt. |
options_id | int, Default 0 | Merkmals-/Variantenbestand. 0 = Bestand ohne Merkmalsbezug. |
Nur den Meldebestand eines Lagers setzen — alle übrigen Werte dieses Lagers bleiben unverändert:
{
"type": "products",
"data": [{
"products_model": "ABC-123",
"storages": [
{"storages_id": 3, "products_reorder_level": 10}
]
}]
}Vollständiges Beispiel mit zwei Lagern, einem Merkmalsbestand und separat gesetztem Gesamtbestand:
{
"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}
]
}]
}- Partielles Update (ab v2.45.0): Geschrieben werden nur Felder, die im Payload vorhanden sind (
INSERT … ON DUPLICATE KEY UPDATE). Bis v2.44.0 wurde die Lagerzeile perREPLACEersetzt — weggelassene Spalten fielen dabei auf0. - Schlüssel je Lagerzeile:
(storages_id, products_id, options_id). Gleiches Tripel = Update der bestehenden Zeile, sonst Insert. - Kein Vollspiegel: Lagerzeilen, die nicht im Array stehen, bleiben unangetastet. Es gibt kein
storages_replaceund kein_action: "delete"— Lagerzeilen entfernen geht nur im Backend. - No-Op ohne Nutzdaten: Ein Eintrag, der nur den Schlüssel enthält, schreibt nichts und legt insbesondere keine Zeile mit Nullwerten an.
nullist nicht „unverändert“: Ein Feld mit dem Wertnullgilt als übergeben und schreibt0. Soll ein Wert unangetastet bleiben, lassen Sie das Feld weg.- Neue Lagerzeile: Bestand mitsenden. Existiert für das Tripel noch keine Zeile, entsteht sie nur aus den übergebenen Feldern. Senden Sie beim Erstbefüllen deshalb
products_quantityundproducts_safe_quantitymit — sonst kann der Datenbankserver den Datensatz je Konfiguration abweisen, ohne dass die Antwort einen Fehler meldet. options_id: Default0. Werte > 0 pflegen den Bestand einer Merkmals-/Variantenkombination in diesem Lager; Produkt- und Merkmalsbestand sind getrennte Zeilen.- Dezimaltrenner ist der Punkt:
"50.5"ist gültig,"50,5"wird als50gelesen. - Voraussetzung
EXTENDED_STORAGE_SYSTEM = 'true': Ist das erweiterte Lagersystem deaktiviert, wirdstorageskomplett ignoriert; die Antwort enthält die WarnungSTORAGE_SYSTEM_DISABLED. - Unbekanntes oder inaktives Lager: Eine
storages_id, die nicht existiert oder deren Lager nicht aktiv ist, erzeugt die WarnungSTORAGE_NOT_FOUND; der Eintrag wird übersprungen, die restlichen Einträge laufen weiter. - Achtung — fehlende
storages_idwird still verworfen: Einträge ohnestorages_id/warehouse_idoder mit einem Wert≤ 0werden ohne Warnung übersprungen. Der Import meldet dann Erfolg, obwohl kein Lagerbestand geschrieben wurde.
storages: Der Meldebestand „Hauptlager“ aus dem Produkteditor ist die Produktspalte products.products_reorder_level — ein anderes Feld als storages[]. Ab v2.63.0 ist sie zusammen mit products_safe_quantity als Feld auf oberster Ebene des Produkts importierbar; auf älteren Ständen nur über das Backend oder die CSV-/XML-Spalte reorder_level. storages_id: 1 ist bereits das erste Zusatzlager — wer dort den Meldebestand des Hauptlagers erwartet, schreibt in das falsche Lager.products_quantity wird nicht aus den Lagern summiert: Der Gesamtbestand des Produkts ist eine unabhängige Spalte. Wer Lagerbestände per storages setzt, muss products_quantity im selben Request separat mitsenden — sonst bleibt der alte Gesamtbestand stehen.storages[] mit — mit demselben Feldsatz, exportierte Lagerdaten sind also unverändert re-importierbar. Ist das erweiterte Lagersystem deaktiviert, fehlt der Schlüssel ganz (nicht als leeres Array). Ausgegeben werden alle Lagerzeilen des Produkts, auch die zu inaktiven Lagern — deren Re-Import quittiert dann STORAGE_NOT_FOUND. Bis v2.62.0 lieferte der Export keine Lagerbestände; dort führt der Weg über den CSV-Porter (Spalten storage_<ID>_*) oder das Backend.Beispiel-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-Parameter
| Parameter | Typ | Beschreibung |
|---|---|---|
products_id | int | Produkt-ID zum Löschen |
products_model | string | Artikelnummer zum Löschen |
Beispiel
curl -X DELETE "https://shop.de/xpanel/xoport/delete/json/products?products_id=123" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"