Klicke hier um ein unverbindliches Erstgespräch buchen!

Posted by Markus Wals

Fulfiller-Anbindung an weclapp: der komplette API-Leitfaden mit Chargenführung

API Chargen Fulfillment Logistik weclapp

Fulfiller-Anbindung an weclapp: der komplette API-Leitfaden mit Chargenführung

Wir bauen und betreiben Fulfillment 4 weclapp, haben Fulfiller- und WMS-Anbindungen für unsere Kunden implementiert und im Betrieb und schulen jede Woche Logistikdienstleister auf die weclapp-API. Trotzdem sehen wir immer wieder dieselben Fehler. Zuletzt vergangene Woche: Ein Fulfiller hat den Bestand eines Händlers per Inventurbeleg nach weclapp geschrieben, mehrmals täglich. Nach fünf Tagen standen 30 Inventurbelege im System, Warenausgänge wurden als Inventurdifferenz hin und her gebucht, und die Trackingnummern kamen ohne Versandstatus an. Der Go-Live stand. Dieser Artikel ist der Leitfaden, den so eine Anbindung von Anfang an gebraucht hätte: der komplette Ablauf, alle Requests, Chargenführung inklusive.

Das Zielbild: weclapp führt, der Fulfiller meldet

Eine Fulfiller-Anbindung ist kein Full-Sync zwischen zwei ERP-Systemen. weclapp bleibt das führende System für Aufträge, Lieferungen, Bestand und Bewertung. Der Fulfiller holt sich Lieferungen ab, meldet zurück, was physisch passiert ist, und korrigiert Bestand nur als Differenz. Das sind genau sieben Berührungspunkte, und ihre Reihenfolge ist Teil des Vertrags.

Architektur der Fulfiller-Anbindung an weclapp: Shop, weclapp als führendes System und Fulfiller mit sieben nummerierten API-Berührungspunkten

  1. Lieferungen abholen: alle Lieferungen im eigenen Lager mit Status „Lieferschein gedruckt“.
  2. Status zurückmelden: ein Zusatzfeld am Lieferschein als Handshake, damit nichts doppelt importiert wird.
  3. Picks setzen: Lagerplatz, Charge und tatsächliche Menge je Position.
  4. Versand melden: Status SHIPPED, Versanddienstleister und Trackingnummer in einem Request.
  5. Bestellung als Avis abholen: bestätigte Bestellungen des Händlers an dein Lager, mit Liefertermin und Mengen.
  6. Wareneingang je Lieferung: gegen die Bestellung, bei Teillieferungen auch mehrere, mit Charge und Mindesthaltbarkeit. Der Bewertungspreis kommt aus der Bestellung.
  7. Bestand abgleichen: Differenz zwischen physischem Bestand und weclapp-Bestand als Lagerbewegung buchen. Nie als Inventur.

Fünf Grundregeln, bevor du den ersten Request schickst

1. Nur das eigene Lager

Ein Händler kann mehrere Fulfiller haben, etwa einen je Land. Jede Abfrage von Lieferungen und Beständen filtert deshalb auf die Lager-ID, die dir der Händler zugewiesen hat: warehouseId-eq=<deine Lager-ID>. Ohne diesen Filter versendest du fremde Aufträge.

„Es gibt verschiedene Läger. Das wäre ganz gefährlich, wenn du da drauf keinen Filter machst.“
Markus Wals, weclapp API Deep Dive für Fulfiller

2. Immer projizieren, nie nachladen

Jeder Listenaufruf bekommt einen properties-Parameter mit genau den Feldern, die du brauchst. Ungefilterte Listen laden das komplette Entitätsschema, das kostet Sekunden statt Millisekunden und belastet den Mandanten. Referenzierte Daten wie Artikelnummer oder Carrier-Name ziehst du im selben Request per includeReferencedEntities mit der Doppelpunkt-Syntax (article:articleNumber) und lädst sie nie einzeln pro Artikel nach. Listen paginierst du mit pageSize=1000, Seite für Seite, bis eine Seite weniger als 1000 Einträge liefert.

3. PUT nur mit ignoreMissingProperties=true

Ein PUT ohne diesen Parameter ist ein Full Update: Alles, was du nicht mitschickst, wird geleert. Das gilt auch für Listen im Body. Schickst du bei einer Lieferung nur eine Position mit, sind die anderen weg.

„Wenn du ignoreMissingProperties nicht schickst, dann ist das ein Full Update. Wenn du zufällig alle Pflichtfelder erwischst und den Rest nicht mitschickst, dann nullst du alles andere.“
Markus Wals, weclapp API Deep Dive für Fulfiller

4. Differenz statt Absolutwert

Bestand setzt du in weclapp nie absolut. Du liest den Ist-Bestand, bildest die Differenz zu deinem physischen Bestand und buchst nur diese Differenz als Lagerbewegung. Die Inventur ist dafür nicht gedacht, dazu unten mehr.

5. Reihenfolge und Wiederholbarkeit

Erst Versandmeldungen verarbeiten, dann abgleichen. Wareneingänge sofort bei Vereinnahmung, nicht über den nächsten Abgleich. Jeder Lauf muss wiederholbar sein: Ein abgebrochener Request darf beim nächsten Durchlauf nichts doppelt anlegen. Auf 429 antwortest du mit Backoff, auf 409 (Optimistic Lock) und Timeouts mit Nachlesen statt blindem Wiederholen.

Schritt 1: Lieferungen abholen

In weclapp entsteht aus dem Auftrag eine Lieferung. Sobald der Lieferschein gedruckt ist, hat die Lieferung den Status DELIVERY_NOTE_PRINTED. Das ist der Übergabepunkt an den Fulfiller, so machen es auch WMS-Anbindungen wie Pulpo oder TM3. Vorher ist die Lieferung noch nicht freigegeben.

Der Request holt alle offenen Lieferungen deines Lagers, mit Positionen, Artikelreferenz, Carrier und Empfängeradresse in einem Aufruf. Auf status akzeptiert weclapp nur -eq, kein -in.

curl --location 'https://testhandel.weclapp.com/webapp/api/v2/shipment?page=1&pageSize=1000&properties=id%2Cstatus%2CshipmentNumber%2CshipmentItems.quantity%2CshippingCarrierId%2CshipmentItems.articleId%2Carticle%3AarticleNumber%2Carticle%3Aean%2Carticle%3Aid%2Carticle%3Aname%2CshippingCarrier%3Aid%2CshippingCarrier%3Aname%2CpackageReferenceNumber%2CpackageTrackingUrl%2CpackageWeight%2CrecipientAddress%2CrecordEmailAddresses%2CmainSalesOrderId%2CsalesOrder%3Aid%2CsalesOrder%3AorderNumber%2CsalesOrder%3AsalesChannel&includeReferencedEntities=shippingCarrierId%2CshipmentItems.articleId%2CmainSalesOrderId&warehouseId-eq=2315&status-eq=DELIVERY_NOTE_PRINTED' \
  --header 'Accept: application/json' \
  --header 'AuthenticationToken: ****'

Zwei Dinge fallen beim ersten Aufruf auf. Erstens: Auf der Lieferposition steht keine Artikelnummer, die kommt nur über die Artikelreferenz. Zweitens: shipmentNumber ist deine Referenznummer für den kompletten Prozess, bis zurück zum Versandlabel. Die Carrier-IDs liest du einmalig über GET /shippingCarrier und mappst sie auf deine Versandarten.

Welche Artikel überhaupt zu dir kommen, liest du ebenfalls einmal sauber projiziert: nur lagerfähige, aktive Artikel, mit Chargen- und Seriennummernpflicht und der primären Bezugsquelle für den Einkaufspreis.

curl --location 'https://testhandel.weclapp.com/webapp/api/v2/article?pageSize=1000&properties=id%2CarticleNumber%2Cname%2CavailableInSale%2Cactive%2CarticleType%2CprimarySupplySourceId%2CbatchNumberRequired%2CserialNumberRequired%2CarticleSupplySource%3Aid%2CarticleSupplySource%3AarticlePrices%2CarticleSupplySource%3AsupplierId&includeReferencedEntities=primarySupplySourceId&articleType-eq=STORABLE&active-eq=true&availableInSale-eq=true' \
  --header 'Accept: application/json' \
  --header 'AuthenticationToken: ****'

Schritt 2: Status zurückmelden, ohne Dashboard-Zugriff

Der Händler will in weclapp sehen, wo seine Lieferung steht: abgeholt, in Kommissionierung, versendet. Und du willst beim nächsten Lauf nur die Lieferungen holen, die du noch nicht kennst. Beides löst ein Zusatzfeld vom Typ Liste am Lieferschein. Lege es per API an, nicht händisch in der Oberfläche, sonst sind die IDs bei jedem Händler anders und deine Anbindung nicht reproduzierbar.

curl --location 'https://testhandel.weclapp.com/webapp/api/v2/customAttributeDefinition' \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --header 'AuthenticationToken: ****' \
  --data '{
  "attributeKey": "fulfillerShipmentStatus",
  "attributeType": "LIST",
  "entities": ["shipment"],
  "label": "Versandstatus",
  "mandatory": false,
  "selectableValues": [
    { "defaultValue": true,  "positionNumber": 1, "value": "Neu" },
    { "defaultValue": false, "positionNumber": 2, "value": "Importiert" },
    { "defaultValue": false, "positionNumber": 3, "value": "Bereit zur Kommissionierung" },
    { "defaultValue": false, "positionNumber": 4, "value": "Kommissionierung" },
    { "defaultValue": false, "positionNumber": 5, "value": "Versendet" }
  ],
  "showInOverview": true
}'

Die Antwort liefert die ID der Definition und die IDs der Werte. Nach dem Import setzt du den Wert auf „Importiert“, wieder mit ignoreMissingProperties=true:

curl --location --request PUT 'https://testhandel.weclapp.com/webapp/api/v2/shipment/id/215289?ignoreMissingProperties=true' \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --header 'AuthenticationToken: ****' \
  --data '{
    "customAttributes": [{
        "attributeDefinitionId": "7396436",
        "selectedValueId": "7396437"
    }]
}'

Ab jetzt filterst du beim Abholen zusätzlich auf den Wert des Zusatzfelds. Für einen einzelnen Wert reicht der Kurzfilter customAttribute<DefinitionsID>.id-eq:

curl --location 'https://testhandel.weclapp.com/webapp/api/v2/shipment?page=1&pageSize=1000&properties=id%2Cstatus%2CshipmentNumber%2CshipmentItems.quantity%2CshippingCarrierId%2CshipmentItems.articleId%2Carticle%3AarticleNumber%2Carticle%3Aean%2Carticle%3Aid%2Carticle%3Aname%2CshippingCarrier%3Aid%2CshippingCarrier%3Aname%2CpackageReferenceNumber%2CpackageTrackingUrl%2CpackageWeight%2CrecipientAddress%2CrecordEmailAddresses%2CmainSalesOrderId%2CsalesOrder%3Aid%2CsalesOrder%3AorderNumber%2CsalesOrder%3AsalesChannel%2CcustomAttributes&includeReferencedEntities=shippingCarrierId%2CshipmentItems.articleId%2CmainSalesOrderId&warehouseId-eq=2315&status-eq=DELIVERY_NOTE_PRINTED&customAttribute7396436.id-eq=7396437' \
  --header 'Accept: application/json' \
  --header 'AuthenticationToken: ****'

Mehrere Werte gleichzeitig, etwa „Neu“ oder „Importiert“, holst du über den Ausdrucksfilter filter= mit logischen Verknüpfungen:

curl --location 'https://testhandel.weclapp.com/webapp/api/v2/shipment?page=1&pageSize=1000&filter=(customAttribute7396436.id%20%3D%20%277396437%27)%20or%20(customAttribute7396436.id%20%3D%20%277396438%27)&properties=id%2Cstatus%2CshipmentNumber%2CshipmentItems.quantity%2CshippingCarrierId%2CshipmentItems.articleId%2Carticle%3AarticleNumber%2Carticle%3Aean%2Carticle%3Aid%2Carticle%3Aname%2CshippingCarrier%3Aid%2CshippingCarrier%3Aname%2CpackageReferenceNumber%2CpackageTrackingUrl%2CpackageWeight%2CrecipientAddress%2CrecordEmailAddresses%2CmainSalesOrderId%2CsalesOrder%3Aid%2CsalesOrder%3AorderNumber%2CsalesOrder%3AsalesChannel%2CcustomAttributes&includeReferencedEntities=shippingCarrierId%2CshipmentItems.articleId%2CmainSalesOrderId&warehouseId-eq=2315&status-eq=DELIVERY_NOTE_PRINTED' \
  --header 'Accept: application/json' \
  --header 'AuthenticationToken: ****'

Schritt 3: Kommissionierung melden: Picks mit Lagerplatz und Charge

Picks (Entnahmen) hängen an der Lieferposition, nicht am Artikel. Sie tragen Lagerplatz, Menge und bei chargengeführter Ware die Charge mit Mindesthaltbarkeit. Über die Pick-Menge bildest du auch Teil- und Mindermengen ab: Die gemeldete Menge ist die tatsächliche Liefermenge. Der Endpunkt dafür ist POST /shipment/id/{id}/updatePicks. Er ist in der aktuellen OpenAPI v2 dokumentiert (Pflichtfelder picks und updateShipQuantity), funktioniert aber genauso unter /api/v1. Drei Requests, in dieser Reihenfolge:

1. Lieferung mit Positionen holen. Du brauchst die ID der Lieferung und die IDs der Positionen.

export WC_TOKEN='DEIN_TESTHANDEL_API_TOKEN'
curl -s -H "AuthenticationToken: $WC_TOKEN" \
  "https://testhandel.weclapp.com/webapp/api/v1/shipment?shipmentNumber-eq=LS1550&properties=id,shipmentNumber,warehouseId,shipmentItems.id,shipmentItems.articleId,shipmentItems.quantity" | jq .

2. Lagerplatz und Charge für den Artikel holen. Aus dem Lagerbestand kommen storagePlaceId und bei Bedarf batchNumberId.

curl -s -H "AuthenticationToken: $WC_TOKEN" \
  "https://testhandel.weclapp.com/webapp/api/v1/warehouseStock?articleId-eq=16986&warehouseId-eq=13682&includeReferencedEntities=batchNumberId,storagePlaceId" | jq .

3. Picks setzen. Vorher bestehende Picks mit GET /shipment/id/{id}/picks prüfen.

curl -i -X POST "https://testhandel.weclapp.com/webapp/api/v1/shipment/id/196858/updatePicks" \
  -H "AuthenticationToken: $WC_TOKEN" -H "Content-Type: application/json" \
  -d '{"removeUnconfirmed":true,"updateShipQuantity":true,"useReservedSerialNumbersFromOtherShipments":true,"picks":[{"shipmentItemId":"196867","pickQuantity":1,"storagePlaceId":"13685"}]}'

So baut Fulfillment 4 weclapp den Body. Die IDs sind Beispielwerte, du ersetzt sie durch die Werte aus Schritt 1 und 2.

Feld Bedeutung
removeUnconfirmed räumt unbestätigte Alt-Picks weg. Nötig, wenn ein Pick den Lagerplatz wechselt.
updateShipQuantity setzt die Liefermenge auf die gepickte Menge. Ein Massenlauf ohne Mengenänderung schickt hier false.
useReservedSerialNumbersFromOtherShipments erlaubt Seriennummern, die bereits an anderen Lieferungen hängen.
picks[].id nur beim Update eines bestehenden Picks. Bei Lagerplatzwechsel weglassen, sonst lehnt weclapp ab.
picks[].shipmentItemId Pflicht. Die Position, nicht der Artikel.
picks[].pickQuantity Pflicht, größer 0. Zahl oder String, beides wird akzeptiert.
picks[].storagePlaceId Pflicht bei neuem Pick.
optional batchNumber, batchExpirationDate, serialNumbers: [], levelId, internalTransportReferenceId

Die Chargen- und Seriennummern-Variante sieht so aus:

curl -i -X POST "https://testhandel.weclapp.com/webapp/api/v1/shipment/id/196858/updatePicks" \
  -H "AuthenticationToken: $WC_TOKEN" -H "Content-Type: application/json" \
  -d '{"removeUnconfirmed":true,"updateShipQuantity":true,"useReservedSerialNumbersFromOtherShipments":true,"picks":[
    {"shipmentItemId":"196867","pickQuantity":2,"storagePlaceId":"13685","batchNumber":"CH-2026-04","serialNumbers":[]},
    {"shipmentItemId":"196868","pickQuantity":1,"storagePlaceId":"13685","serialNumbers":["SN-0001"]}
  ]}'

Zwei Hinweise aus der Praxis. Erstens: Reservierung auf Chargenebene schaltest du in weclapp nicht ein. Sie blockiert den Chargenwechsel, wenn eine Charge knapp wird, und die Charge kennt ohnehin nur der Fulfiller beim Pick. Zweitens: Wer ohne Picks direkt auf SHIPPED geht, bekommt die fehlenden Entnahmen von weclapp automatisch gebucht, aber vom Standardlagerplatz und ohne deine Charge. Für saubere Chargenführung meldest du die Picks vorher.

Schritt 4: Versand melden: Status SHIPPED, Carrier, Tracking

Eine Trackingnummer allein ist wertlos. Erst der Statuswechsel auf SHIPPED bucht in weclapp den Warenausgang aus der Lieferung, löst die Versandbestätigung an den Shop aus und startet die Folgeprozesse, etwa die Rechnung. Und weil du als Fulfiller den Versanddienstleister selbst wählst, musst du ihn mitschreiben: Passen Carrier und Trackingnummer nicht zusammen, lehnen Shops und Marktplätze die Versandbestätigung ab. Amazon sperrt bei wiederholten Fehlern den Händler.

curl --location --request PUT 'https://testhandel.weclapp.com/webapp/api/v2/shipment/id/215289?ignoreMissingProperties=true' \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --header 'AuthenticationToken: ****' \
  --data '{
    "status": "SHIPPED",
    "packageReferenceNumber": "4-351",
    "packageTrackingNumber": "00340433923095602571",
    "packageWeight": "11.4",
    "shippingCarrierId": "38114"
}'
  • packageReferenceNumber ist deine eigene Referenz, packageWeight das Bruttogewicht inklusive Verpackung.
  • Das Beispiel gilt für Sendungen mit einem Paket. Bei mehreren Paketen schreibst du das Array parcels[] mit Carrier und Trackingnummer je Paket. Die Einzelpaket-Felder lässt du dann weg, weclapp lehnt sie sonst ab.
  • Melde den Status beim tatsächlichen physischen Abgang, nicht beim Anlegen des Labels. Sonst kollidiert der Warenausgang mit deinem Bestandsabgleich.
  • Einzeln pro Lieferung, kein Batch. Läuft ein Request in einen Timeout oder 409, liest du die Lieferung nach, bevor du wiederholst: Der erste Request kann serverseitig längst verarbeitet sein. Und eine Lieferung, die schon SHIPPED ist, versendest du nicht noch einmal.

Schritt 5: Bestellung als Avis abholen

Nachschub beginnt beim Händler. Er bestellt in weclapp aus der Disposition heraus beim Lieferanten, und der Lieferort ist dein Lager. Sobald die Bestellung bestätigt ist, ist sie für dich ein Avis: Du weißt, welche Artikel in welcher Menge zu welchem Termin bei dir ankommen, bevor der Lkw vor der Rampe steht. Die Bestellnummer ist die Avis-Referenz, plannedDeliveryDate der erwartete Termin. Genau so läuft es in den EDI-Anbindungen, die wir für Logistikpartner implementiert haben, dort als Avis-Nachricht. Bei einer REST-Anbindung liest du die Bestellungen direkt, wieder nur für dein Lager:

curl --location 'https://testhandel.weclapp.com/webapp/api/v2/purchaseOrder?page=1&pageSize=1000&properties=id%2CpurchaseOrderNumber%2Cstatus%2CplannedDeliveryDate%2CsupplierId%2CwarehouseId%2CpurchaseOrderItems.id%2CpurchaseOrderItems.articleId%2CpurchaseOrderItems.quantity%2CpurchaseOrderItems.receivedQuantity%2CpurchaseOrderItems.plannedDeliveryDate%2Carticle%3AarticleNumber%2Carticle%3Aname%2Carticle%3AbatchNumberRequired&includeReferencedEntities=purchaseOrderItems.articleId&warehouseId-eq=2315&status-eq=CONFIRMED' \
  --header 'Accept: application/json' \
  --header 'AuthenticationToken: ****'

Eine Bestellung wandert in weclapp durch ORDER_ENTRY_IN_PROGRESS, ORDER_ENTRY_COMPLETED und ORDER_DOCUMENTS_PRINTED nach CONFIRMED. Erst ab CONFIRMED ist sie ein Avis, und erst dann lässt weclapp einen Wareneingang gegen sie zu. Versuchst du es vorher, antwortet weclapp mit 400: „It is not possible to create collective incoming goods for the selected purchase orders because one or more purchase orders have not yet been confirmed by the supplier.“ receivedQuantity je Position sagt dir, was schon angekommen ist, damit du Teillieferungen erkennst. Liegt der Liefertermin weniger als zwei Werktage in der Zukunft, ist eine Warnung im Log sinnvoll, mehr nicht: Der Avis geht trotzdem raus.

Alle Requests in Schritt 5 und 6 sind am 08.09.2026 genau so gegen unser Testsystem gelaufen. Die IDs im Text sind die echten Werte aus diesem Lauf: Bestellung P1727 über 100 Stück des chargenpflichtigen Artikels K14101 an das Lager 2315.

Schritt 6: Wareneingang gegen die Bestellung anlegen

Kommt die Ware, legst du den Wareneingang gegen die Bestellung an, nicht als lose Bewegung. Damit schließt sich der Kreis für den Händler: Die Bestellung zeigt die gelieferte Menge, die Disposition rechnet richtig, die Rechnungsprüfung hat einen Beleg, und der Bewertungspreis kommt aus dem Bestellpreis. Je physischer Lieferung ein Wareneingang. Kommt eine Bestellung in drei Teillieferungen, sind es drei Wareneingänge gegen dieselbe Bestellung. Kommen mehrere Bestellungen auf einer Palette, hängst du sie über additionalPurchaseOrderIds an denselben Wareneingang. Fünf Requests, in dieser Reihenfolge. Bei einer Volllieferung entfällt der zweite.

1. Wareneingang aus der Bestellung erzeugen. weclapp legt je Bestellposition eine Wareneingangsposition an.

curl --location 'https://testhandel.weclapp.com/webapp/api/v2/purchaseOrder/id/7398324/createIncomingGoods' \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --header 'AuthenticationToken: ****' \
  --data '{ "additionalPurchaseOrderIds": [] }'

Die Antwort ist der Wareneingang WE1696 (ID 7398350) im Status INCOMING_SHIPPED, mit einer Wareneingangsposition 7398352 je Bestellposition, verknüpft über purchaseOrderItemId.

2. Nur bei Teillieferung: Positionsmengen auf die gelieferte Menge setzen. createIncomingGoods übernimmt je Position die komplette offene Bestellmenge. Kommen von 100 bestellten Stück erst 60, setzt du die Positionsmenge vorher auf 60. Sonst scheitert später die Einlagerung mit „In item 1 the booking quantity does not match the unbooked item quantity“. Alle Positionen mit ihrer id mitschicken, fehlende Positionen würden gelöscht.

curl --location --request PUT 'https://testhandel.weclapp.com/webapp/api/v2/incomingGoods/id/7398485?ignoreMissingProperties=true' \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --header 'AuthenticationToken: ****' \
  --data '{ "incomingGoodsItems": [ { "id": "7398487", "quantity": "60" } ] }'

Der nächste createIncomingGoods auf dieselbe Bestellung bekommt dann nur noch die Restmenge 40 als Position. Ist die Bestellung voll geliefert, lehnt weclapp einen weiteren Wareneingang ab: „It is not possible to create collective incoming goods because the purchase order P1729 does not contain any open items.“

3. Buchungszeilen lesen. weclapp legt je Position eine Vorschlagszeile an: Positionsmenge, Standardlagerplatz und schon der Bewertungspreis aus der Bestellung, hier 20,00. Du brauchst daraus die incomingGoodsItemId.

curl --location 'https://testhandel.weclapp.com/webapp/api/v2/incomingGoods/id/7398350/incomingBookings' \
  --header 'Accept: application/json' \
  --header 'AuthenticationToken: ****'

4. Mengen, Chargen und Lagerplätze buchen. Eine Zeile je Position und Charge. Zwei Chargen für dieselbe Position sind zwei Zeilen mit derselben incomingGoodsItemId. Die Mindesthaltbarkeit heißt hier expirationDate, wieder in Epoch-Millisekunden. Zeilen ohne id ersetzen die Vorschlagszeile; willst du eine bestehende Zeile behalten, schickst du sie mit ihrer id mit. Den articleValuationPrice aus der Vorschlagszeile übernimmst du in jede Buchungszeile. So machen es die von uns implementierten Anbindungen, die jede Vorschlagszeile feldweise zurückspiegeln. weclapp hat den Preis im Test auch ohne Angabe vererbt, explizit ist trotzdem sicherer, und du kannst ihn überschreiben, wenn der Lieferschein einen anderen Preis nennt als die Bestellung.

curl --location 'https://testhandel.weclapp.com/webapp/api/v2/incomingGoods/id/7398350/updateIncomingBookings' \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --header 'AuthenticationToken: ****' \
  --data '{
  "incomingBookings": [
    { "incomingGoodsItemId": "7398352", "quantity": "60", "batchNumber": "CH-2026-04", "expirationDate": 1882908000000, "storagePlaceId": "2317", "articleValuationPrice": "20" },
    { "incomingGoodsItemId": "7398352", "quantity": "40", "batchNumber": "CH-2026-05", "expirationDate": 1885500000000, "storagePlaceId": "2317", "articleValuationPrice": "20" }
  ]
}'

Die Antwort bestätigt beide Zeilen, jede mit dem geerbten Bewertungspreis 20,00.

5. Einlagern. Ohne diesen Statuswechsel bleibt die Ware im Zustand „angekommen, aber nicht eingelagert“ und taucht im verfügbaren Bestand nicht auf. Einen eigenen Aktions-Endpunkt gibt es dafür nicht, der Status wird über den normalen PUT gesetzt. Eine version musst du dabei nicht mitschicken. Erst dieser Schritt bucht den Bestand, und erst hier prüft weclapp die Charge: Eine Buchungszeile ohne Charge nimmt updateIncomingBookings für einen chargenpflichtigen Artikel noch an, die Einlagerung scheitert dann mit „Batch number is missing for booking records of item 1“.

curl --location --request PUT 'https://testhandel.weclapp.com/webapp/api/v2/incomingGoods/id/7398350?ignoreMissingProperties=true' \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --header 'AuthenticationToken: ****' \
  --data '{ "status": "INCOMING_MOVED_INTO_STORE" }'

Und so sieht es danach in weclapp aus, per API nachgelesen:

Objekt Ergebnis nach Schritt 6
Wareneingang WE1696 Status INCOMING_MOVED_INTO_STORE, Lager 2315
Bestellung P1727 receivedQuantity 100 von 100, Status bleibt CONFIRMED
Lagerbestand K14101 60 Stück Charge CH-2026-04 und 40 Stück Charge CH-2026-05 auf Lagerplatz 2317, jeweils mit Mindesthaltbarkeit
Lagerbewegungen zwei Buchungen vom Typ IN_PURCHASE_ORDER mit valuationPrice 20,00 und Bezug auf die Wareneingangsposition
Teillieferung P1729 zwei Wareneingänge gegen dieselbe Bestellung: WE1697 mit 60 Stück, WE1698 mit 40 Stück, receivedQuantity erst 60, dann 100

Der Bewertungspreis kam ohne eigenes Zutun aus der Bestellung, die Bewegungsart ist „Wareneingang aus Bestellung“, und die Disposition sieht die Bestellung als geliefert. Genau das bekommt der Händler bei einem Inventur-Sync nie.

Ware ohne Bestellung

Muster, Nachlieferungen ohne Beleg, Rückläufer aus einer Aktion: Nicht alles, was bei dir ankommt, hat eine Bestellung. Dafür gibt es bookIncomingMovement, und der nimmt alles mit, was chargengeführte Ware braucht: Chargennummer, Mindesthaltbarkeit, Ziel-Lagerplatz und den Bewertungspreis.

curl --location 'https://testhandel.weclapp.com/webapp/api/v2/warehouseStockMovement/bookIncomingMovement' \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --header 'AuthenticationToken: ****' \
  --data '{
  "articleId": "36782",
  "batchNumber": "12345",
  "batchNumberExpirationDate": 1883208663000,
  "quantity": "10",
  "movementNote": "Wareneingang ohne Bestellung, Lieferschein 4711",
  "targetStoragePlaceId": "2317",
  "articleValuationPrice": "10"
}'

Der Bewertungspreis ist hier kein Nice-to-have. Ohne articleValuationPrice ist die Bestandsbewertung des Händlers kaputt, und die repariert niemand gern rückwirkend. Beim Wareneingang gegen die Bestellung kommt der Bestellpreis mit der Vorschlagszeile, bei der losen Bewegung musst du ihn selbst liefern: articleValuationPrice ist dann der Einkaufspreis. Den findest du nicht am Artikel, sondern an der primären Bezugsquelle. Der Artikel-Request aus Schritt 1 zieht sie deshalb per articleSupplySource:articlePrices mit. Lies sie regelmäßig neu, Lieferantenstammdaten synchronisierst du nicht.

„Den Einkaufspreis kriegst du nicht auf derselben Ebene wie den Artikel, sondern über die Supply Sources.“
Markus Wals, weclapp API Deep Dive für Fulfiller

Schritt 7: Bestand abgleichen: Differenz buchen, nie Inventur

Der periodische Abgleich ist ein Korrekturmechanismus für echte Differenzen, kein Bestandsexport. Du liest den weclapp-Bestand deines Lagers je Artikel, Lagerplatz und Charge, inklusive der offenen Picks:

curl --location 'https://testhandel.weclapp.com/webapp/api/v2/warehouseStock?pageSize=1000&properties=id%2CarticleId%2Cpicks%2Cquantity%2CbatchNumberId%2CstoragePlaceId%2Carticle%3Aid%2Carticle%3AarticleNumber%2CbatchNumber%3Aid%2CbatchNumber%3AbatchNumber%2Cpick%3Aid%2Cpick%3AshipmentItemId%2Cpick%3Aquantity&includeReferencedEntities=picks.id%2CbatchNumberId&warehouseId-eq=2315' \
  --header 'Accept: application/json' \
  --header 'AuthenticationToken: ****'

Der frei verfügbare Bestand je Artikel ist quantity minus die Summe der Picks. Ein Feld dafür liefert weclapp nicht, du rechnest es selbst. Genau deshalb läuft der Abgleich erst, nachdem alle Versandmeldungen verarbeitet sind, und klammert Mengen aus, die bei dir schon kommissioniert, aber noch nicht als versendet gemeldet sind. Sonst buchst du gegen laufende Aufträge und erzeugst genau das Hin und Her, das der Händler in seinen Lagerbewegungen sieht.

Die Differenz buchst du als Bewegung, mit einer Notiz, die den Lauf identifiziert:

curl --location 'https://testhandel.weclapp.com/webapp/api/v2/warehouseStockMovement/bookOutgoingMovement' \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --header 'AuthenticationToken: ****' \
  --data '{
  "articleId": "36782",
  "batchNumber": "1220",
  "movementNote": "Bestandsabgleich Fulfiller 2026-09-08 06:00",
  "quantity": "8819.8",
  "sourceStoragePlaceId": "2317"
}'

Für Zugänge ohne Bestellbezug nimmst du bookIncomingMovement aus Schritt 6, mit Bewertungspreis. Bewegungen buchst du je Artikel nacheinander, nicht parallel, sonst kollidieren sie an der Bestandsversion. Nach einem Reset der Bewegungsdaten vor dem Go-Live bucht der erste Lauf den kompletten physischen Bestand als Wareneingang ein. Das ist dann der saubere Anfangsbestand.

Warum nicht die Inventur?

Weil sie für etwas anderes gebaut ist. Die weclapp-Inventur ist laut Hersteller eine Stichtagsinventur. Während sie läuft, ist das Lager gesperrt und wird erst nach Abschluss wieder für die Kommissionierung freigegeben. Differenzen bucht sie als eigene Bewegungsart „Warenein- bzw. -ausgang aus Inventur“. Ein Konnektor, der mehrmals täglich Inventurbelege anlegt, sperrt bei jedem Lauf das Lager, verwandelt jeden Wareneingang in eine Inventurdifferenz und flutet die Historie, die der Händler für seine Jahresinventur und den Steuerberater braucht. Die Movement-Endpunkte existieren genau für diesen Fall.

Chargenführung durchgängig, Seriennummern als Erweiterung

Chargen ziehen sich durch alle sieben Berührungspunkte, und weclapp ist dafür gut geeignet, gerade bei Lebensmitteln und Getränken:

  • Artikel: batchNumberRequired sagt dir, welche Artikel eine Charge brauchen. Beim Wareneingang prüft weclapp das in dem Moment, in dem Bestand entsteht, also bei der Einlagerung. Für Picks gilt dasselbe: chargenpflichtige Artikel brauchen die Charge am Pick.
  • Wareneingang: je Charge eine Buchungszeile mit batchNumber und expirationDate, bei der losen Bewegung batchNumber plus batchNumberExpirationDate. Die Mindesthaltbarkeit ist ab jetzt in weclapp auswertbar, etwa für Reichweiten- und Verfallslisten.
  • Picks: die tatsächlich entnommene Charge je Position. Deine FEFO-Logik läuft im WMS, weclapp bekommt das Ergebnis.
  • Abgleich: die Differenz je Artikel und Charge, nicht nur je Artikel. Der Bestand aus Schritt 7 liefert die Charge über batchNumber:batchNumber mit.

Seriennummern funktionieren nach demselben Muster, nur eins tiefer: serialNumberRequired am Artikel, serialNumbers[] in Picks und Bewegungen, und beim Pick der Schalter useReservedSerialNumbersFromOtherShipments, falls eine Nummer schon an einer anderen Lieferung hängt. Eine Regel dazu aus unseren Anbindungen: Innerhalb eines Wareneingangs muss jede Seriennummer eindeutig sein, auch über mehrere Chargen derselben Position hinweg, sonst lehnt weclapp die Einlagerung ab („The same serial number is already selected in another booking item for this article in position N“). Wer Chargen sauber hat, bekommt Seriennummern in einem Nachmittag dazu.

Was du nicht bauen musst

  • Stornos: vorerst per E-Mail. Eine vollständige technische Stornokette kostet mehr, als sie in den ersten Monaten bringt. Ist die Ware schon unterwegs, sind Rücksendung und Neuversand billiger als ein angehaltener Prozess.
  • Eine Rückmeldung an den Lieferanten des Händlers: Die Bestellung bleibt Sache des Händlers. Du meldest, was angekommen ist, mehr nicht.
  • Lieferantenstammdaten: nicht synchronisieren. Du liest den Einkaufspreis, mehr nicht.
  • Kennzahlen über additionalProperties: Das sind Anzeigewerte, keine Buchungsgrundlage. Gebucht wird über den Lagerbestand und die Bewegungen.

Go-Live-Checkliste

  1. Gegen ein Testsystem entwickeln, nie gegen den Produktivmandanten des Händlers.
  2. Den kompletten Kreislauf einmal durchspielen: Bestellung bestätigen, Avis lesen, Wareneingang in zwei Teillieferungen, Auftrag, Picks mit Charge, Versand, Abgleich ohne Differenz.
  3. Je Datenflussrichtung einen Positivfall, einen Negativfall und den Fehlerpfad durchspielen. Die Negativfälle aus unserem Testlauf: Wareneingang auf eine unbestätigte Bestellung (400, „not yet been confirmed by the supplier“), Einlagern ohne Charge (400, „Batch number is missing“), Wareneingang auf eine voll gelieferte Bestellung (400, „does not contain any open items“). Dazu: Lieferung aus fremdem Lager wird ignoriert, Timeout beim Versand führt zum Nachlesen statt zum Doppelversand.
  4. status == success ist kein Beweis. Beweis ist das Zielobjekt in weclapp mit den richtigen Feldwerten, per API nachgelesen. Genau dieses Nachlesen übernimmt wals.pro AI 4 weclapp auf dem Testsystem für dich.
  5. Vor dem Go-Live die Bewegungsdaten des Mandanten zurücksetzen lassen und den Inventur- oder Testsync vorher abschalten, sonst ist das System nach dem Reset sofort wieder voll.
  6. In der ersten Stunde nach Go-Live die echten Läufe inhaltlich prüfen: Bewegungsarten, Chargen, Versandstatus, Versandbestätigung im Shop.

Werkzeuge, die die Anbindung beschleunigen

Drei Dinge entscheiden darüber, ob so eine Anbindung zwei Wochen oder zwei Monate dauert.

Ein Testsystem

weclapp bietet eigene Testsysteme an. Die werden nicht pro Nutzer:in bezahlt, sondern pauschal pro System und Monat, und das ist günstiger als eine einzelne Lizenz im Produktivsystem. Das lohnt sich vom ersten Tag an: Du entwickelst nie am Produktivmandanten deines Kunden, du kannst Bewegungsdaten jederzeit zurücksetzen lassen, und deine End-to-End-Tests laufen gegen echte Belegketten statt gegen Mocks. Wenn du kein eigenes Testsystem willst, stellen wir dir eines samt Stammdaten und Unterstützung bereit.

Grundschulung und Postman Collection

Wer neu auf der weclapp-API ist, startet mit unserem Webinar weclapp API Deep Dive auf YouTube: Authentifizierung, Filter, Projektion, referenzierte Entitäten und die Belegkette, in einer Stunde. Die Requests aus diesem Artikel und viele weitere gibt es fertig in unserer Postman Collection 4 weclapp, mit Umgebungsvariablen für Testsystem und Mandant.

weclapp Toolbox

Die Chrome-Erweiterung weclapp Toolbox öffnet zu jedem Datensatz in der Oberfläche die API-Ansicht, kopiert IDs und exportiert Requests als cURL. Jede ID in diesem Artikel ist so entstanden.

wals.pro AI 4 weclapp

Die KI-Plattform kennt das Schema und die API-Doku deines Mandanten und liest Daten zurück. Das macht sie in allen drei Phasen zum wichtigsten Werkzeug neben der Toolbox:

  • Planung: Dein Assistent findet die IDs, die du brauchst: Lager, Lagerplätze, Carrier, Zusatzfeld-Definitionen und ihre Werte. Kein Copy-Paste aus der Oberfläche.
  • Entwicklung: Feldnamen, Filter, Pflichtfelder und Endpunkte beantwortet er aus Schema und Doku, statt dass du sie in der OpenAPI suchst.
  • Validierung: Er fährt die End-to-End-Szenarien auf dem Testsystem und liest die Ergebnisse nach. Ist die Lieferung wirklich SHIPPED, steht die Charge am Wareneingang, stimmt der Bestand nach dem Abgleich. Jede Schreibaktion läuft über Vorschau und Freigabe.

Wie das mit Lagerbuchungen in der Praxis aussieht, zeigt der Artikel Wareneingang und Versand per KI buchen.

Schulung

Und wenn du die Anbindung nicht allein bauen willst: Wir schulen jede Woche Fulfiller auf genau diesen Ablauf, mit dem Testsystem und den Requests aus diesem Artikel.

👉 Schulung oder Vorgespräch buchen: wals.pro/termin
👉 Kostenlos starten, Open Beta bis 31.12.2026: ai.wals.pro