Products Endpoint

Endpoint

Products

Vollständige CRUD-Operationen für Produktdaten.


Übersicht

OperationMethodEndpointScope
Alle exportierenGET/xpanel/xoport/export/json/productsproducts:read
EinzelproduktGET/xpanel/xoport/export/json/product/{ID}products:read
ID-RangeGET/xpanel/xoport/export/json/products/{START}:{END}products:read
ImportPOST/xpanel/xoport/import/json/productsproducts:write
UpdatePUT/xpanel/xoport/import/json/productsproducts:write
DeleteDELETE/xpanel/xoport/delete/json/productsproducts:delete

Export (GET)

Hinweis: Die Produkt-ID wird als Pfad-Segment übergeben, nicht als Query-Parameter. Verwenden Sie /product/{ID} (Singular) für ein einzelnes Produkt.

URL-Patterns

URLBeschreibung
/export/json/product/123Einzelnes Produkt mit ID 123
/export/json/productsAlle Produkte (paginiert)
/export/json/products/100:200Produkte mit ID 100 bis 200

Query-Parameter

ParameterTypBeschreibung
limitintMax. Anzahl (Default: 50, Max: 250)
offsetintStartposition 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_ean
  • products_price, products_weight, products_quantity, products_cost
  • manufacturers_id, products_status
  • contract, contract_use_current_price, contract_option v2.5.0
  • products_inventory_management, products_min_qty, products_max_qty, products_qty_blocks, products_base_price v2.5.0
  • service, sitemap, non_cart, non_price, products_free_shipping v2.5.0
  • shipping_profile, products_unit_id, base_unit_id, products_sort_order v2.5.0
  • images[] - Produktbilder mit Sortierung
  • categories[] - Kategorie-IDs
  • groups[] - Kundengruppen-Preise
  • customers_prices[] - Kundenindividuelle Preise
  • specials[] - Sonderpreise
  • extra_fields[] - Zusatzfelder
  • badges[] - Produkt-Badges
  • xsells[] - Cross-Selling-Produkte
  • upsells[] - Warenkorb-Ergänzungen („Passt dazu“) v2.18.0+
  • tabs[] - Produkttabs
  • videos[] - Produktvideos
  • suppliers[] - Lieferanten: suppliers_id und products_model (Artikelnummer des Lieferanten). Die Liste ersetzt alle Zuordnungen des Artikels, ein Eintrag ohne products_model leert die Nummer.
  • storages[] - Bestand je Lager v2.63.0+
  • languages{} - Name, Beschreibung, SEO pro Sprache:
    • products_name, products_description, products_short_description
    • products_head_title_tag, products_head_desc_tag, products_head_keywords_tag
    • seo_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)

Es gibt keinen eigenen Merkmals-Endpoint. Merkmale hängen im Produkt. Aufrufe wie /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:

FeldEnthältSKU-Daten (model/ean/quantity/price) gehören zum …
options{}Merkmale ohne MerkmalsgruppeEinzelwert
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: 0 mit 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.
Bis einschließlich API 2.57.1 lieferte der Export gruppierte Merkmale gar nicht — kein leeres Feld, keine Warnung. Wer auf einem älteren Stand „keine Merkmale“ sieht, prüft im Backend, ob die Optionen des Produkts in einer Merkmalsgruppe liegen. Behoben in API 2.58.0 (Shop 4.8.9).

Import (POST/PUT)

POST erstellt neue Produkte, PUT aktualisiert bestehende.

Seit v2.4.2: Produkt-Beschreibungen werden ausschließlich über das verschachtelte languages-Format übergeben (konsistent mit Export). Bei INSERT (POST) werden fehlende Sprachen automatisch mit den Daten der Master-Language befüllt (Auto-Fill).
Neu in v2.5.0: 19 erweiterte Felder für Vertragsdaten (contract, contract_use_current_price), Logistik (products_cost, products_min_qty, products_ean) und Flags (service, sitemap, non_cart, non_price).
Neu in v2.17.0: Produkt-Sonderangebote können über 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.
Neu in v2.18.0: Warenkorb-Ergänzungen können über 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.
Neu in v2.34.0: B-Ware-Automation & weitere Felder: 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.
Neu in v2.67.0: 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.
Kundengruppen-/Händlerpreise (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).
Neu in v2.57.0: Kundenindividuelle Preise (Tab „Kundenindividuelle Preise“ im Produkteditor, Tabelle 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 (Alias price), products_price1…products_price12 + products_price1_qty…products_price12_qty (Staffelpreise), products_qty_blocks, products_min_qty, products_max_qty.
  • Kunden-Adressierung: per customers_id oder customers_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 leerem customers_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".
Round-Trip: Der Produkt-Export lieferte 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.

FeldTypBeschreibung
storages_id (Pflicht)intID des Zusatzlagers aus Tabelle storages (Bezeichnung in storages_info, Pflege im Backend unter Produkte → Lagerverwaltung). Alias: warehouse_id.
products_quantitydecimal(16,8)Bestand in diesem Lager.
products_safe_quantitydecimal(16,8)Sicherheitsbestand dieses Lagers.
products_reorder_leveldecimal(16,8)Meldebestand dieses Lagers.
products_storage_textvarchar(50)Lagerplatz als Freitext, max. 50 Zeichen. Wird nicht automatisch gekürzt.
options_idint, Default 0Merkmals-/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 per REPLACE ersetzt — weggelassene Spalten fielen dabei auf 0.
  • 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_replace und 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.
  • null ist nicht „unverändert“: Ein Feld mit dem Wert null gilt als übergeben und schreibt 0. 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_quantity und products_safe_quantity mit — sonst kann der Datenbankserver den Datensatz je Konfiguration abweisen, ohne dass die Antwort einen Fehler meldet.
  • options_id: Default 0. 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 als 50 gelesen.
  • Voraussetzung EXTENDED_STORAGE_SYSTEM = 'true': Ist das erweiterte Lagersystem deaktiviert, wird storages komplett ignoriert; die Antwort enthält die Warnung STORAGE_SYSTEM_DISABLED.
  • Unbekanntes oder inaktives Lager: Eine storages_id, die nicht existiert oder deren Lager nicht aktiv ist, erzeugt die Warnung STORAGE_NOT_FOUND; der Eintrag wird übersprungen, die restlichen Einträge laufen weiter.
  • Achtung — fehlende storages_id wird still verworfen: Einträge ohne storages_id/warehouse_id oder mit einem Wert ≤ 0 werden ohne Warnung übersprungen. Der Import meldet dann Erfolg, obwohl kein Lagerbestand geschrieben wurde.
Das Hauptlager ist kein Eintrag in 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.
Round-Trip: Ab v2.63.0 liefert der JSON-Export 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

ParameterTypBeschreibung
products_idintProdukt-ID zum Löschen
products_modelstringArtikelnummer zum Löschen

Beispiel

curl -X DELETE "https://shop.de/xpanel/xoport/delete/json/products?products_id=123" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"