PlentyOne API @ Mokebo — Guide für Fabian¶
Audience: Du willst über die PlentyOne-REST-API arbeiten (lesen, schreiben — in deinem Fall Bilder). Diese Anleitung dokumentiert wie wir das bei Mokebo aktuell machen, gegroundet auf zwei produktiv laufende Workflows. Keine Theorie, keine Spekulation — wenn etwas hier steht, machen wir es exakt so.
Stand: Mai 2026. Quellen:
SUB PlentyOne Lookup,01 Retouren-Erfassung,00 Stack Heartbeat — Connection Test.
1. Setup — was bei Mokebo schon da ist¶
| Item | Wert |
|---|---|
| Hostname | p38991.my.plentysystems.com |
| API-User | n8n-api (REST-API-User, kein Backend-Login) |
| n8n-Instanz | https://ai-mokebo.app.n8n.cloud/ |
| n8n-Projekt | Susi Projects — Workflow + Credential müssen im selben Projekt liegen, sonst sieht der Custom-Auth-Dropdown die Credential nicht |
| Credential | Plenty Login Body — Custom Auth Credential. Liegt in Susi Projects und ist geshared. Enthält Username + Passwort als JSON-Body-Injection. |
Du brauchst nichts neu anzulegen. Die Credential existiert, jeder Workflow im Susi Projects kann sie referenzieren.
Historie / Warum so: Bis 05.05.2026 lief das über Workspace-Variables (
$vars.PLENTY_USER/$vars.PLENTY_PASSWORD). Die sind aber ein n8n-Pro-Feature — auf unserem Cloud-Starter-Plan werden sie zuundefinedaufgelöst und die Liste ist read-only. Custom Auth Credential ist der Workaround und bleibt der neue Standard.
2. Der Login-Node — exakt wie wir es machen¶
Knall das genau so in jeden neuen Plenty-Workflow rein. Das ist der identische Node aus SUB PlentyOne Lookup und 01 Retouren-Erfassung:
| Setting | Wert |
|---|---|
| Node-Typ | HTTP Request |
| Node-Name | Login PlentyOne ← bitte exakt so, andere Nodes referenzieren den Namen |
| Method | POST |
| URL | https://p38991.my.plentysystems.com/rest/login |
| Authentication | Generic Credential Type |
| Generic Auth Type | Custom Auth |
| Custom Auth | Plenty Login Body (siehe Sektion 1) |
| Send Body | true |
| Body Content Type | JSON |
| Specify Body | Using Fields Below |
| Body Parameters | Leer lassen! Die Custom Auth Credential injiziert username + password automatisch. Wenn du hier zusätzlich Felder einträgst, kommt es zu Konflikten/Doppelsendung. |
Was ist „Custom Auth"? Ein n8n-Credential-Typ, der ein freies JSON-Object (
{"body": {...}}oder{"headers": {...}}) ins Request-Object merged. Bei uns enthält die Credential den Body mit Username + Passwort. Damit wandert das Passwort nirgends hardcodet durch den Workflow.
Output ($('Login PlentyOne').item.json):
accessToken ist gültig 24 Stunden. Wir cachen den Token nicht — jeder Workflow loggt sich bei jedem Run neu ein. Das ist die Mokebo-Konvention. Klingt verschwenderisch, ist aber bei <1000 Runs/Tag völlig egal und vermeidet komplexe Refresh-Logik.
3. Token in nachfolgenden Calls verwenden¶
In jedem weiteren HTTP-Request-Node setzt du einen einzigen Header:
Das = davor ist die n8n-Syntax für „dieser Wert ist eine Expression". Kein anderer Header nötig (kein Accept, kein Content-Type — Plenty liefert per default JSON).
Beispiel-Request: einen Auftrag holen
GET https://p38991.my.plentysystems.com/rest/orders/{{ $json.orderId }}?with[]=addresses&with[]=orderItems
Headers:
Authorization: =Bearer {{ $('Login PlentyOne').item.json.accessToken }}
4. Eager-Loading mit with[] — Pflicht-Wissen¶
Plenty-Endpunkte liefern per default nur die Kerndaten. Verschachtelte Felder (Adressen, Items, Properties) musst du explizit anfordern, sonst kommt das Feld als null zurück.
Macht mit with[]=feldname in den Query-Parametern. Mehrere Felder = with[] mehrmals:
Was wir produktiv eager-loaden:
with[]=... |
Was es lädt |
|---|---|
addresses |
Alle Adressen (Rechnung + Lieferung) am Auftrag |
addressRelations |
Mapping welche Adresse welcher Typ ist (Lieferadresse = typeId: 2) |
orderItems |
Die Auftrags-Positionen |
orderItems.properties |
Properties pro Position (verschachtelt!) |
properties |
Auftrags-Properties (Marktplatz-Bestellnummer, externe IDs, …) |
orderReferences |
Eltern-Kind-Beziehungen (Auftrag → Retoure → Gutschrift) |
shippingPackages |
Versandpakete inkl. Tracking |
documents |
PDFs am Auftrag |
Verschachtelte Felder funktionieren mit Punkt-Notation: with[]=orderItems.properties lädt sowohl orderItems als auch jeweils ihre properties.
Pro-Tipp: Lieber 1× mit vielen with[] aufrufen als 5× ohne — Plenty-Calls zählen aufs Rate Limit, eager-Loading nicht extra.
5. Mokebo-spezifische Konstanten (das was sonst niemand weiß)¶
Diese IDs leben aktuell in den Code-Nodes der Production-Workflows. Du brauchst sie wahrscheinlich auch:
5.1 Auftragstypen (typeId / orderType)¶
| ID | Typ |
|---|---|
1 |
Sales Order (Standard-Bestellung) |
3 |
Retoure |
4 |
Gutschrift |
Für Filter: ?orderType=3 filtert auf Retouren.
5.2 Auftragsstatus (was wir filtern)¶
8= Storno → wir filtern den raus (statusId !== 8)9.01bis9.02= aktive Retouren-Stati bei Mokebo. Filter:?statusFrom=9.01&statusTo=9.02
5.3 Property typeId an Aufträgen / Order-Items¶
Plenty lagert viele Werte als Properties aus. Bei uns relevant:
typeId |
Bedeutung | Wo |
|---|---|---|
7 |
Marktplatz-Bestellnummer (externalOrderId als Property) | am Auftrag (order.properties[]) |
35 |
Retourengrund (mokebo-spezifisch) | am Order-Item (orderItems[].properties[]) |
Auslesen: (order.properties || []).find(p => p.typeId === 7)?.value
5.4 Address-Relations typeId¶
typeId |
Bedeutung |
|---|---|
2 |
Lieferadresse |
addressRelations ist die Verknüpfungs-Tabelle: [{addressId, typeId}, …]. Wenn du die Lieferadresse willst:
const deliveryRelation = (order.addressRelations || []).find(r => r.typeId === 2);
const deliveryAddress = order.addresses.find(a => a.id === deliveryRelation.addressId);
5.5 Verwendete Auftragsherkünfte (referrerId)¶
Aus dem Regex-Extraktor in SUB PlentyOne Lookup:
referrerId |
Kanal |
|---|---|
2 |
Amazon |
9 |
Shopify |
14.00 |
XXXLutz |
143.x |
Otto (referrerId beginnt mit 143) |
152 |
bol.com |
160.1 |
Otto (alte ID) |
177.01 |
OBI |
Custom-Filter im Workflow: String(entry.referrerId).startsWith('143') für „alle Otto-Aufträge".
5.6 Country-IDs (Standard PlentyOne)¶
Lebt im Code-Node der Retouren-Erfassung als COUNTRY_MAP. Die wichtigsten:
1=DE, 2=AT, 3=BE, 4=CH, 8=DK, 9=ES, 12=FR, 14=GB, 19=IE,
20=IT, 25=NL, 27=PL, 28=PT, 33=SE, 51=HR, 165=US
5.7 Retourengründe (mokebo-spezifisch)¶
39 IDs gemappt auf Klartext, lebt im Code-Node der Retouren-Erfassung als REASON_MAP. Bei Bedarf da rauskopieren — Susi pflegt das, wenn neue Gründe in Plenty hinzukommen.
6. Patterns aus den Production-Workflows¶
6.1 Auftrag per Plenty-ID holen¶
GET /rest/orders/{id}?with[]=addresses&with[]=orderItems&with[]=shippingPackages&with[]=documents&with[]=orderReferences
6.2 Auftrag per externer Order-ID (Marktplatz-Bestellnummer) finden¶
Response ist {entries: [...]}. Filter clientseitig auf typeId === 1 und statusId !== 8, dann den neuesten nehmen.
⚠️ Falle: Bei Shopify-Bestellnummern (MKB-XXXXX) nicht über externalOrderId suchen — Plenty speichert dort die Shopify-interne ID, nicht das MKB-Label. Workaround: Per requester_email über Kontakte gehen.
6.3 Kontakt per E-Mail finden¶
Response: {entries: [{id, ...}, ...]}. Erste ID nehmen.
6.4 Aufträge eines Kontakts holen¶
6.5 Retouren in Status-Range holen¶
6.6 Kommentare zu einem Auftrag holen¶
Response ist direkt ein Array [{text, ...}, ...] (kein entries-Wrapper).
6.7 Retoure → Hauptauftrag verlinken¶
In der Retoure findest du die Verknüpfung über orderReferences:
const parentRef = (retoure.orderReferences || []).find(ref => ref.referenceType === 'parent');
const hauptauftragId = parentRef?.referenceOrderId;
7. Sub-Calls aus einem Code-Node¶
Wenn du in einem Code-Node (n8n) zusätzliche HTTP-Calls machen willst (z.B. weil du pro Item nochmal Plenty fragen musst), nutze this.helpers.httpRequest:
const token = $('Login PlentyOne').first().json.accessToken;
const order = await this.helpers.httpRequest({
method: 'GET',
url: `https://p38991.my.plentysystems.com/rest/orders/${orderId}?with[]=properties`,
headers: { 'Authorization': `Bearer ${token}` },
json: true,
});
Genau dieses Pattern läuft in 01 Retouren-Erfassung produktiv, pro Retourenauftrag werden 3 Sub-Calls gemacht (Hauptauftrag holen + Retoure-Items mit Properties + Comments). Funktioniert robust.
8. Pagination¶
Plenty paginiert fast jede Liste. Response sieht so aus:
Bei Mokebo nutzen wir itemsPerPage=50 (nicht das Maximum 250) — guter Balance-Punkt.
Für Schleifen: HTTP Request Node hat eingebautes Pagination-Feature unter Options → Pagination → Update Parameter in Each Request. Nutze das, dann brauchst du keinen Code-Loop. Stop-Bedingung: $response.body.isLastPage === true.
9. Naming-Konventionen bei Mokebo¶
Hält sich an alle existierenden Workflows:
| Prefix | Was |
|---|---|
00 MAIN xxx |
Haupt-Workflows |
01 xxx, 02 xxx, … |
Nummerierte Hauptflows |
SUB xxx oder [SUB] xxx |
Subworkflows (Execute-Workflow-Trigger) |
XX xx |
alte/deprecated Workflows |
@xxx |
System-Workflows (z.B. @Execution Tracker) |
Sticky Notes an jedem logischen Block. Code-Nodes mit Header-Kommentar (// === Was tut der Block ===).
Variables-Konvention bei Code-Nodes mit Plenty-Logik:
- token aus $('Login PlentyOne').first().json.accessToken
- IDs/Maps in CAPS (COUNTRY_MAP, REASON_MAP)
- Source-of-truth-Kommentar dazu („Quelle: Plenty Backend → …")
10. Für deinen Use Case: Bilder hoch- und runterladen¶
⚠️ Honest disclosure: Wir haben noch nie Bilder über die Plenty-API geschrieben. Alle Workflows bei Mokebo lesen aktuell nur (Aufträge, Kontakte, Comments, Properties). Du betrittst hier Neuland. Diese Sektion ist Recherche-Pointer, kein verifiziertes Pattern.
10.1 Wichtige Vorab-Klärung¶
Bilder in PlentyOne hängen entweder am Artikel (= alle Varianten teilen das Bild) oder an einer spezifischen Variante (= z.B. die rote T-Shirt-Variante hat andere Fotos als die blaue). Das ändert den Endpunkt:
| Use Case | Endpunkt-Basis |
|---|---|
| Bild für gesamten Artikel | /rest/items/{itemId}/images |
| Bild für eine Variante | /rest/items/{itemId}/variations/{variationId}/images |
| Sichtbarkeit pro Marktplatz/Mandant/Sprache | /rest/items/{itemId}/images/{imageId}/availabilities |
→ Frag Susi welche Granularität du brauchst, bevor du anfängst.
10.2 Empfohlene Reihenfolge¶
- Erst lesen, dann schreiben. Mach einen GET auf
/rest/items/{id}/imagesan einem existierenden Artikel um zu sehen wie die Response-Struktur aussieht. Notier dir das Format der Felder. - Test-Artikel anlegen. Bevor du an echten Mokebo-Artikeln rumschreibst, lass dir von Susi einen Sandbox-Artikel anlegen oder geh in die Plenty-Test-Umgebung wenn ihr eine habt.
- Body-Format checken. Plenty unterstützt mehrere Upload-Modi:
- Binary upload (multipart/form-data)
- Base64 in JSON
- URL-Reference (Plenty zieht das Bild von einer URL) Welches du nehmen kannst, hängt davon ab wie deine Bilder vorliegen. URL-Reference ist meist am einfachsten in n8n.
- Doku zur Hand: developers.plentymarkets.com/en-gb/plentymarkets-rest-api/index.html — sucht nach „items.images". Postman-Collection auf github.com/plentymarkets/api-doc ist auch praktisch zum Probieren.
- Image-Availabilities nicht vergessen: Ein hochgeladenes Bild ist erstmal nicht im Webshop sichtbar. Du musst es zusätzlich pro Mandant/Marktplatz/Sprache sichtbar schalten via
POST /rest/items/{itemId}/images/{imageId}/availabilities.
10.3 Wenn du Mokebo-spezifische IDs brauchst¶
- Mandant-ID (
plentyId): Bei Mokebo aktuell wahrscheinlich nur einer. Susi fragen, falls Default0nicht funktioniert. - Marktplatz-Sichtbarkeiten: Image-Availabilities arbeiten mit Referrer-IDs (siehe 5.5). Wenn du z.B. ein Bild nur für Shopify hochladen willst, brauchst du
referrerId: 9.
11. Stolperfallen die wir tatsächlich getroffen haben¶
| Problem | Ursache | Lösung |
|---|---|---|
MKB-XXXXX per externalOrderId gibt nichts zurück |
Plenty speichert bei Shopify nicht das MKB-Label, sondern die Shopify-interne ID | Per requester_email über Kontakte suchen |
properties ist plötzlich null |
with[]=properties im Query vergessen |
Eager-Loading immer dazu |
| Token läuft mitten im langen Run ab | Workflow läuft >24h | Bei sehr langen Runs zwischendurch nochmal Login-Node aufrufen |
addressRelations lädt aber addresses ist leer |
Beide eager loaden, nicht nur eins | with[]=addresses&with[]=addressRelations |
update_workflow aus n8n-MCP zerstört Credentials in allen Nodes |
newCredential('Name') matcht nicht mit existierenden Credentials gleichen Namens, sondern legt einen leeren Placeholder an |
Patches lieber in der UI machen, nicht via MCP |
| Login wirft 403/401 obwohl Passwort korrekt | Plenty sperrt den n8n-api-User-Login automatisch nach mehrfach fehlgeschlagenen Versuchen (z.B. weil ein Workflow mit undefined retried oder ein Mensch manuell mit altem Passwort probiert hat). Cloud-Account-Login (account.plentysystems.com) geht trotzdem weiter — das ist eine separate Auth-Schicht und täuscht |
PlentyOne-Backend → Einrichtung → Einstellungen → Kontoverwaltung → Konten → n8n-api öffnen. Wenn unten links der Button LOGIN ENTSPERREN sichtbar ist → klicken, neues Passwort setzen, speichern, neues Passwort in der Plenty Login Body Credential aktualisieren |
| Custom-Auth-Dropdown im Login-Node sagt „No credentials yet" obwohl Credential existiert | Workflow und Credential liegen in unterschiedlichen Projekten, oder n8n-Frontend-Cache ist veraltet | Zuerst Hard-Reload (Cmd + Shift + R). Dann prüfen ob Workflow + Credential beide in Susi Projects liegen. Falls Credential in einem anderen Projekt: über Sharing-Tab teilen oder verschieben. |
12. Cheat Sheet zum Kopieren¶
12.1 n8n: Login-Node-JSON (paste-ready)¶
Im Workflow-Editor reicht: HTTP-Request-Node anlegen, Settings setzen wie in Sektion 2. Ein bestehender Login-Node aus den Production-Workflows kann auch dupliziert werden — Login PlentyOne aus 01 Retouren-Erfassung ist die Referenz-Implementation.
12.2 curl (zum Probieren auf der Konsole)¶
# Token holen — JSON-Body!
TOKEN=$(curl -s -X POST https://p38991.my.plentysystems.com/rest/login \
-H "Content-Type: application/json" \
-d '{"username":"n8n-api","password":"PASSWORD"}' \
| jq -r '.accessToken')
# Beliebigen Call machen
curl -s "https://p38991.my.plentysystems.com/rest/items/12345/images" \
-H "Authorization: Bearer $TOKEN" | jq
12.3 Python (lokales Skript)¶
import requests
USER = "n8n-api"
PASSWORD = "..." # aus Vault, NICHT hardcoden
BASE = "https://p38991.my.plentysystems.com"
# Login — JSON-Body
resp = requests.post(f"{BASE}/rest/login", json={"username": USER, "password": PASSWORD})
resp.raise_for_status()
token = resp.json()["accessToken"]
headers = {"Authorization": f"Bearer {token}"}
# Beispiel: Bilder eines Artikels lesen
imgs = requests.get(f"{BASE}/rest/items/12345/images", headers=headers).json()
print(imgs)
13. Wenn du steckst — Eskalations-Reihenfolge¶
- Response-Body lesen. Plenty gibt bei 4xx-Fehlern in der Regel klare Fehler-JSONs zurück. Erste Anlaufstelle.
- Susi fragen — sie kennt das Mokebo-spezifische Setup (Custom-Stati, Mandant, Eigenschaften-IDs). Speziell für Bilder relevant: ob ihr bestimmte Standards habt (Auflösung, Sortierung, Hauptbild-Konvention).
- Bestehende Workflows lesen —
SUB PlentyOne Lookupund01 Retouren-Erfassungsind die Goldmine. Wenn du ein Pattern brauchst, gucke da zuerst. - PlentyOne Developer Docs: developers.plentymarkets.com
- Forum: forum.plentymarkets.com — überraschend aktiv und kompetent.
Letztes Update: 2026-05-06 — Auth-Pattern auf Custom Auth Credential umgestellt (Variables sind Pro-only und auf Starter unbrauchbar). Maintained von Susi (susanna@mokebo.de). Bei Updates: Susi pingen, sie führt das zentral.