Zum Inhalt

Plenty Image Upload — Kontext für Claude

Zweck: Diese Datei gibt dir (Claude) den vollständigen Kontext, um Fabian beim Debuggen des Plenty-Image-Uploads zu helfen. Wichtig: Nichts verändern, nichts committen, nichts produktiv hochladen. Nur Endpunkt verifizieren, Request-Struktur klären, Diagnose stellen.


TL;DR — der wahrscheinliche Bug

Fabian POSTet aktuell auf:

POST /rest/items/{id}/images

Das ist falsch. Verifiziert gegen die OpenAPI-v3-Spec (github.com/plentymarkets/api-doc):

  • /rest/items/{id}/imagesnur GET (List images of an item)
  • /rest/items/{id}/images/uploadPOST (Upload a new image, Tag Item)

Der korrekte Upload-Endpoint ist also:

POST /rest/items/{id}/images/upload

⚠️ Hypothese — nicht verifiziert: Die ursprüngliche Annahme war, dass Plenty auf POSTs gegen den falschen Pfad mit 200 + {} antwortet. Das steht so nirgends offiziell, und laut Spec sollte ein POST auf /images (das nur GET definiert) eigentlich 405 Method Not Allowed zurückgeben. Wenn Fabian wirklich 200 + {} sieht, kann das auch ein anderes Symptom sein (Auth-Scope, Token-Problem, etwas in einem Proxy/Gateway davor). Nicht ausschließen, dass der Endpoint nicht das einzige Problem ist.


Korrekter Request

Endpoint

POST https://{plenty-domain}/rest/items/{id}/images/upload

Headers

Authorization: Bearer <access_token>
Content-Type: application/json

Body Schema (verbatim aus OpenAPI v3, geprüft 2026-05-07)

Required laut Spec (requestBody.content.application/json.schema.required):

["itemId", "lang", "name", "type", "uploadFileName", "value"]

ℹ️ Achtung: requestBody.required selbst ist auf false gesetzt — das heißt, ein leerer POST kann je nach Plenty-Routing-Verhalten möglicherweise NICHT mit 400/422 quittiert werden, sondern entweder mit 401 (nur dieser Response ist neben 200 in der Spec dokumentiert) oder mit etwas Anderem. Die Spec gibt sich an dem Punkt selber inkonsistent.

Required Fields:

Field Typ Beschreibung (verbatim aus Spec)
itemId integer "The ID of the item the image is associated with"
lang string "The language of the image name"
name string "The name of the image in the specified language"
type string "The type of referrer […] allowed values are mandant, marketplace, listing"
value number "For type mandant, this is the plentyID of the client. For marketplace/listing, the referrer-ID. -1.00 = available for all referrers of this type."
uploadFileName string "The file name assigned to the uploaded image. Permitted characters: alphanumeric (a-z, A-Z, 0-9), hyphens (-), underscores (_)."

Bilddaten — eines von beiden (Spec markiert beide als optional, aber ohne eines davon kein sinnvoller Upload):

Field Typ Beschreibung
uploadImageData string "The base64 encoded image data of the image"
uploadUrl string "The URL under which the image can be accessed for uploading. Permitted characters: alphanumeric (a-z, A-Z, 0-9), hyphens (-), underscores (_)."

Optional:

Field Typ Beschreibung
fileType string "Possible file formats: JPG, JPEG, PNG, GIF, SVG"
position integer "Position is used for sorting images in the online store"
alternate string Alt-Text in der angegebenen Sprache
names array Array von ItemImageName {imageId, lang, name, alternate} — für Multi-Language-Namen zusätzlich zum flachen lang/name-Paar
availabilities array Array von ItemImageAvailability {imageId, type, value} — für Multi-Referrer-Verfügbarkeit zusätzlich zum flachen type/value-Paar

Die flachen Felder (lang, name, type, value) sind die Required-Form. names/availabilities-Arrays sind eine Erweiterung, falls mehrere Sprachen oder mehrere Verfügbarkeits-Constraints in einem einzigen Upload-Call gesetzt werden sollen.

Response Schema (200 OK) — ItemImage

Field Typ Beschreibung
id integer Bild-ID (für späteren DELETE)
itemId integer Artikel-ID
fileType string jpg, jpeg, png, gif, svg (lowercase im Response!)
path string Interner Pfad, z.B. S3:12345:file.jpg
position integer Sortierposition
createdAt / updatedAt string ISO-Timestamps
md5Checksum / md5ChecksumOriginal string MD5-Hashes
hasLinkedVariations integer 1 wenn an Variante verlinkt, sonst 0
size / width / height integer Pixel-Maße
url / urlMiddle / urlPreview / urlSecondPreview string Public URLs in verschiedenen Größen

Documented Error Responses (Spec)

Code Bedeutung
200 OK — gibt ItemImage zurück
401 "The resource owner or authorization server denied the request […] Check the access token parameter."

Die Spec dokumentiert kein 400/422/404 — Validation-Fehler kommen wahrscheinlich trotzdem als 4xx, sind aber nicht offiziell typisiert. Ein 200 + {} ist laut Spec NICHT vorgesehen — wenn das Symptom auftritt, liegt etwas außerhalb der dokumentierten Pfade falsch (Routing, Proxy, Auth-Scope, falscher Tenant).

Beispiel-Body (flache Form, base64-Variante)

{
  "itemId": 12345,
  "lang": "de",
  "name": "Produktbild Front",
  "type": "mandant",
  "value": -1,
  "uploadFileName": "produkt_front.jpg",
  "uploadImageData": "<reines base64 OHNE data:image/...;base64,-Prefix>",
  "fileType": "JPG",
  "position": 0
}

Beispiel-Body (mit names/availabilities-Arrays — entspricht dem offiziellen Tutorial)

Wird genutzt, wenn das Bild mehrsprachige Namen oder mehrere Verfügbarkeits-Constraints in einem Call setzen soll. Die flachen Required-Felder bleiben trotzdem Pflicht:

{
  "itemId": 12345,
  "lang": "de",
  "name": "Produktbild Front",
  "type": "mandant",
  "value": -1,
  "uploadFileName": "produkt_front.jpg",
  "uploadImageData": "<reines base64>",
  "fileType": "JPG",
  "names": [
    { "lang": "de", "name": "Produktbild Front" },
    { "lang": "en", "name": "Product image front" }
  ],
  "availabilities": [
    { "type": "mandant", "value": -1 },
    { "type": "marketplace", "value": 4.01 }
  ]
}

Erfolgreicher Response (200)

{
  "id": 67,
  "itemId": 12345,
  "fileType": "jpeg",
  "path": "S3:12345:produkt_front.jpg",
  "url": "https://your_store.com/item/images/12345/3000x3000/produkt_front.jpg"
}

Stolpersteine, die zum unerwarteten Response führen können

Selbst wenn Endpoint und Auth stimmen — folgende Punkte sind häufige Fail-Quellen. (Die "silent fail / 200 + {}" Beschreibung ist Erfahrungswissen, nicht im Spec dokumentiert.)

  1. uploadImageData mit data:image/...;base64,-Prefix. Spec verlangt reines base64. Wenn der String aus FileReader.readAsDataURL() oder einem Browser-Canvas kommt, ist da data:image/jpeg;base64, davor — muss raus.

  2. uploadFileName mit Sonderzeichen. Spec ist hier explizit: "Permitted characters: alphanumeric (a-z, A-Z, 0-9), hyphens (-), underscores (_)" + Extension. Spaces, Umlaute, Klammern, Punkte im Namensteil sind nicht erlaubt. Strikt slugifizieren vor dem Senden.

  3. type + value Kombi falsch. Spec definiert value als number. Bei type: "mandant" ist das die plentyID, -1.00 für „alle Referrer dieses Typs". String "-1" ist laut Spec nicht zulässig — JSON-number ist Pflicht.

  4. fileType außerhalb der erlaubten Werte. Spec: nur JPG, JPEG, PNG, GIF, SVG. Anderes ignoriert.

  5. Token ohne Item-Write-Scope. Erfahrungswert (nicht spec-dokumentiert): Wenn der User keine Schreibrechte auf Item-Routes hat, kommt manchmal 200 + {} statt 401/403. Token-Scope im Plenty-Backend prüfen: Einrichtung → Einstellungen → Kontoverwaltung → Konten → User → Routes.

  6. Falsche Domain. https://{tenant}.plentymarkets-cloud-de.com ist veraltet. Aktuell ist https://{tenant}.my.plentysystems.com (Mokebos Instanz: p38991.my.plentysystems.com).

  7. required: false auf dem RequestBody trotz Required-Fields-Liste. Spec-Inkonsistenz — kann dazu führen, dass Plenty bei einzelnen fehlenden Required-Fields nicht sauber 400/422 sondern etwas Unerwartetes zurückgibt. Wenn Validation-Antwort komisch aussieht: einzeln Field-für-Field hinzufügen und schauen, ab wann 200 mit gültigem ItemImage kommt.


Sichere Diagnose — ohne Produktivdaten zu verändern

Susi will nichts produktiv ändern, nur verifizieren, ob der Endpoint und die Body-Struktur stimmen. Empfohlene Reihenfolge:

Schritt 1: Endpoint-Existenz beweisen (zerstörungsfrei)

Mit absichtlich kaputtem Body POSTen. Plenty dokumentiert für diesen Endpoint nur 200 und 401, also kann die Antwort variieren. Interpretation:

  • 400 / 422 → Endpoint existiert, Body ist nur falsch validiert ✅
  • 404 → Pfad ist falsch
  • 405 → Pfad existiert, aber POST ist nicht erlaubt (würde bei /images ohne /upload zu erwarten sein)
  • 401 → Token-Problem (falscher Scope, abgelaufen, Lock)
  • 200 + {} → Anomalie, Endpoint reagiert nicht spec-konform — tieferes Routing-/Auth-Problem prüfen
curl -i -X POST "https://{domain}/rest/items/1/images/upload" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'

Schritt 1b: Gegenprobe — POST auf den falschen Pfad

Wenn Schritt 1 unklare Ergebnisse liefert, mach den gleichen Call gegen /images (ohne /upload). Wenn da derselbe 200 + {} zurückkommt, ist das Routing definitiv kaputt — und dann wäre der Endpoint-Wechsel allein nicht der Fix.

curl -i -X POST "https://{domain}/rest/items/1/images" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'

Erwartet wäre 405 Method Not Allowed (laut Spec ist der Pfad GET-only).

Schritt 2: Realer Upload — aber nur auf einen Test-Artikel

Lege im Plenty-Backend einen Wegwerf-Artikel an (z.B. „TEST — bitte nicht löschen, nur für API-Tests"), der in keinem Verkaufskanal aktiv ist. Notiere die itemId. Verwende ein 1×1 Pixel transparentes PNG als Test-Image — dann ist auch optisch nichts „kaputt", falls das Bild tatsächlich landet.

# 1×1 transparent PNG, base64
TINY_PNG="iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="

curl -i -X POST "https://{domain}/rest/items/{TEST_ITEM_ID}/images/upload" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"itemId\": {TEST_ITEM_ID},
    \"lang\": \"de\",
    \"name\": \"API Test\",
    \"type\": \"mandant\",
    \"value\": -1,
    \"uploadFileName\": \"api_test.png\",
    \"uploadImageData\": \"$TINY_PNG\",
    \"fileType\": \"PNG\"
  }"

Erwartete Antwort: 200 mit JSON-Body inkl. id, path, url.

Schritt 3: Aufräumen (Reset)

Direkt nach erfolgreichem Test das Test-Bild wieder löschen — dann ist der Test-Artikel wieder leer:

curl -X DELETE "https://{domain}/rest/items/{TEST_ITEM_ID}/images/{IMAGE_ID_AUS_RESPONSE}" \
  -H "Authorization: Bearer $TOKEN"

Auth (Plenty REST API Login)

PlentyOne unterstützt zwei Auth-Wege:

Variante A — Legacy /rest/login (User+Passwort)

TOKEN=$(curl -s -X POST "https://{domain}/rest/login" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "username={REST_USER}&password={REST_PASSWORD}" \
  | jq -r .access_token)
  • Token bei Mokebos Instanz 24h gültig (expires_in: 86400)
  • Response-Felder: access_token (snake_case!), token_type: "Bearer", expires_in, refresh_token
  • ⚠️ Bei mehrfach fehlgeschlagenem Login sperrt Plenty den User → im Backend unter Einrichtung → Einstellungen → Kontoverwaltung → Konten → User mit LOGIN ENTSPERREN freischalten

Variante B — OAuth 2.0 Client Credentials (modern, empfohlen)

TOKEN=$(curl -s -X POST "https://{domain}/rest/login" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&client_id={CLIENT_ID}&client_secret={CLIENT_SECRET}" \
  | jq -r .access_token)
  • Standard-Token-Lebensdauer: 1h (3600s) — alle ~50 Min. erneuern
  • Im OpenAPI v3 Spec ist der Endpoint mit security: [{ oAuth2: [] }] markiert — OAuth 2.0 ist die offiziell dokumentierte Auth-Methode für /rest/items/{id}/images/upload

Wenn Fabian gerade neu baut: lieber gleich OAuth 2.0 nehmen statt Legacy-Login. Wenn er bestehenden Code patcht, der schon Legacy-Login benutzt: dabei bleiben.

Token-Scope prüfen

Wenn 200 + {} trotz korrektem Pfad und Body weiter auftritt: User-Berechtigungen im Plenty-Backend prüfen unter Einrichtung → Einstellungen → Kontoverwaltung → Konten → User → {n8n-api} → Routes. Der User braucht Schreibrechte auf Item-Routes (insb. POST /rest/items/{id}/images/upload). Fehlende Scopes können stillen 200-Response erzeugen.


Was Claude tun soll

  1. Lies diese Datei vollständig, bevor du Vorschläge machst.
  2. Frag Fabian zuerst, was er konkret sieht:
  3. Welche URL POSTet er aktuell — /rest/items/{id}/images oder schon /images/upload?
  4. Welchen HTTP-Status bekommt er zurück? (200, 4xx, etwas anderes?)
  5. Welcher Body kommt zurück? ({}, JSON-Fehler, leer?)
  6. Welche Auth-Methode nutzt er? (Legacy /rest/login oder OAuth 2.0 client_credentials?)
  7. Verändere keine Produktivdaten. Folge Schritt 1 → 1b → 2 → 3 oben.
  8. Wenn 200 + {} auch beim /upload-Endpoint mit korrektem Body auftritt:
  9. Erst Token-Scope prüfen (Auth-Sektion oben → Token-Scope prüfen)
  10. Dann die 6 Stolpersteine einzeln durchgehen, in der Reihenfolge (1 = häufigster)
  11. Keine Spekulation. Wenn du dir bei einem Field nicht sicher bist, prüf den OpenAPI-Spec im verlinkten Repo (jq '.paths."/rest/items/{id}/images/upload".post' openApiV3.json).
  12. Trenne sauber, was bewiesen ist und was Hypothese ist. Endpoint /upload ist verifiziert (OpenAPI). Das 200 + {}-Symptom als Folge des falschen Pfads ist Hypothese — Schritt 1b dient genau dazu, das zu verifizieren.

Quellen

Body-Schema selber aus dem Spec ziehen

curl -s "https://raw.githubusercontent.com/plentymarkets/api-doc/master/plentymarkets/openApiV3/openApiV3.json" -o /tmp/plenty-openapi.json
jq '.paths."/rest/items/{id}/images/upload".post' /tmp/plenty-openapi.json

Wenn etwas in dieser Datei für deinen Fall (Fabian) nicht zu stimmen scheint: sag das laut, anstatt Annahmen zu treffen. Lieber nachfragen als das gleiche im Kreis drehen wie gestern.