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:
Das ist falsch. Verifiziert gegen die OpenAPI-v3-Spec (github.com/plentymarkets/api-doc):
/rest/items/{id}/images→ nurGET(List images of an item)/rest/items/{id}/images/upload→POST(Upload a new image, TagItem)
Der korrekte Upload-Endpoint ist also:
⚠️ 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 nurGETdefiniert) eigentlich405 Method Not Allowedzurückgeben. Wenn Fabian wirklich200 + {}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¶
Headers¶
Body Schema (verbatim aus OpenAPI v3, geprüft 2026-05-07)¶
Required laut Spec (requestBody.content.application/json.schema.required):
ℹ️ Achtung:
requestBody.requiredselbst ist auffalsegesetzt — das heißt, ein leerer POST kann je nach Plenty-Routing-Verhalten möglicherweise NICHT mit400/422quittiert werden, sondern entweder mit401(nur dieser Response ist neben200in 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.)
-
uploadImageDatamitdata:image/...;base64,-Prefix. Spec verlangt reines base64. Wenn der String ausFileReader.readAsDataURL()oder einem Browser-Canvas kommt, ist dadata:image/jpeg;base64,davor — muss raus. -
uploadFileNamemit 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. -
type+valueKombi falsch. Spec definiertvaluealsnumber. Beitype: "mandant"ist das die plentyID,-1.00für „alle Referrer dieses Typs". String"-1"ist laut Spec nicht zulässig — JSON-numberist Pflicht. -
fileTypeaußerhalb der erlaubten Werte. Spec: nurJPG,JPEG,PNG,GIF,SVG. Anderes ignoriert. -
Token ohne Item-Write-Scope. Erfahrungswert (nicht spec-dokumentiert): Wenn der User keine Schreibrechte auf Item-Routes hat, kommt manchmal
200 + {}statt401/403. Token-Scope im Plenty-Backend prüfen:Einrichtung → Einstellungen → Kontoverwaltung → Konten → User → Routes. -
Falsche Domain.
https://{tenant}.plentymarkets-cloud-de.comist veraltet. Aktuell isthttps://{tenant}.my.plentysystems.com(Mokebos Instanz:p38991.my.plentysystems.com). -
required: falseauf dem RequestBody trotz Required-Fields-Liste. Spec-Inkonsistenz — kann dazu führen, dass Plenty bei einzelnen fehlenden Required-Fields nicht sauber400/422sondern etwas Unerwartetes zurückgibt. Wenn Validation-Antwort komisch aussieht: einzeln Field-für-Field hinzufügen und schauen, ab wann200mit gültigemItemImagekommt.
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 falsch405→ Pfad existiert, aber POST ist nicht erlaubt (würde bei/imagesohne/uploadzu 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 → UsermitLOGIN ENTSPERRENfreischalten
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¶
- Lies diese Datei vollständig, bevor du Vorschläge machst.
- Frag Fabian zuerst, was er konkret sieht:
- Welche URL POSTet er aktuell —
/rest/items/{id}/imagesoder schon/images/upload? - Welchen HTTP-Status bekommt er zurück? (
200,4xx, etwas anderes?) - Welcher Body kommt zurück? (
{}, JSON-Fehler, leer?) - Welche Auth-Methode nutzt er? (Legacy
/rest/loginoder OAuth 2.0 client_credentials?) - Verändere keine Produktivdaten. Folge Schritt 1 → 1b → 2 → 3 oben.
- Wenn
200 + {}auch beim/upload-Endpoint mit korrektem Body auftritt: - Erst Token-Scope prüfen (Auth-Sektion oben → Token-Scope prüfen)
- Dann die 6 Stolpersteine einzeln durchgehen, in der Reihenfolge (1 = häufigster)
- 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). - Trenne sauber, was bewiesen ist und was Hypothese ist. Endpoint
/uploadist verifiziert (OpenAPI). Das200 + {}-Symptom als Folge des falschen Pfads ist Hypothese — Schritt 1b dient genau dazu, das zu verifizieren.
Quellen¶
- PlentyOne REST API — OpenAPI v3 Spec (GitHub raw JSON) — verifiziert 2026-05-07, Endpoint
/rest/items/{id}/images/uploadPOST mit Required["itemId","lang","name","type","uploadFileName","value"] - PlentyOne REST API repo
- Item data tutorial (Developers Portal) — zeigt Beispiel mit
names/availabilities-Arrays statt flacher Form - Item images (PlentyONE Manual)
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.