REST-API, um KYTH.Karl aus deinem eigenen System (Dashboard / ERP / WMS) zu steuern: Bestellungen lesen, Carrier vergleichen, Labels erstellen + drucken. Das Sandbox-/Live- Verhalten richtet sich nach deiner Carrier-Konfiguration in KYTH.Karl.
https://karl.kyth.systems/api/v1 (Breaking Changes nur in einer neuen Version)Authorization: Bearer <API-Key> auf jedem Requestkarl_live_…) wird genau einmal im Klartext angezeigt — sofort sicher speichern. KYTH.Karl speichert nur einen Hash; der Key kann nicht erneut angezeigt werden.1. GET /api/v1/orders → offene Bestellungen anzeigen
2. POST /api/v1/orders/{id}/rates → Carrier-Optionen + Preise holen
3. (Mitarbeiter wählt einen Carrier) → "Carrier wechseln"
4. POST /api/v1/orders/{id}/labels → Label erstellen + drucken
Query: status (unfulfilled|partial|fulfilled|restocked|unshipped, Default unfulfilled), limit (max 250), after (Cursor).
{
"orders": [
{ "id": "1042", "name": "#1042", "created_at": "…", "email": "kunde@example.com",
"total_weight_g": 2500, "customs_status": "ok",
"recipient": { "first_name": "Max", "last_name": "Müller", "address1": "Hauptstr. 1",
"zip": "25980", "city": "Sylt", "country_code": "DE", "phone": "…" },
"line_items": [ { "title": "Widget", "sku": "W1", "variant_id": "44012345678", "quantity": 2, "open_quantity": 2, "grams": 1200, "hs_code": "…" } ],
"package_resolved": { "length_cm": 24, "width_cm": 18, "height_cm": 17, "weight_g": 700,
"source": "variant_metafield" } }
],
"count": 1, "has_next": false, "next_cursor": ""
}
open_quantity (seit 1.3.77): die noch nicht versendete Menge der Position (0 bis quantity).
Ist ein Teil der Bestellung schon versendet, rechnen package_resolved und das Label für den Rest nur mit
dieser Menge — das Gesamtgewicht der Bestellung gilt dann nicht als Quelle, weil es Versendetes mitzählt.
package_resolved.decided_by_line_item (die Position, deren Maße den Karton bestimmen) ist die Stelle in
line_items der Bestellung (ab 0, alle Positionen wie oben); gewählt wird nur unter den offenen.
Eine einzelne Bestellung (Schema wie oben, ein Objekt).
Carrier-Vergleich (Smart Routing). package ist optional — lässt du es weg,
löst KYTH.Karl die Maße selbst auf (wie in der App) und nennt die Quelle im package_used-Feld.
Woher kommen die Maße? KYTH.Karl liest sie aus dem Shopify-Metafeld
(Varianten-Ebene, ersatzweise Produkt-Ebene)
karl_dims.dimensions (Wert im Format LxBxH in cm, z. B. 24x18x17;
alternativ drei Dezimalzahl-Felder karl_dims.length_cm / width_cm / height_cm).
Auflösungsreihenfolge: karl_dims an der Variante → dieselben Felder am Produkt (nur als
vollständiges Tripel, nie aus beiden Ebenen gemischt) → Standardpaket aus den Einstellungen.
Bei mehreren Positionen gewinnt das größte Volumen. Das Gewicht ist die Summe der
Positionsgewichte. Schickst du package mit, gilt dein Wert unverändert.
// Request — Variante A: KYTH.Karl löst die Maße auf
{}
// Request — Variante B: eigene Maße
{ "package": { "weight_g": 2500, "length_cm": 30, "width_cm": 20, "height_cm": 15 } }
// Response
{ "options": [
{ "carrier": "dhl", "service": "V01PAK", "service_name": "DHL Paket",
"price": 4.95, "currency": "EUR", "eta_days": 1, "surcharge": null, "recommended": true,
// Volumengewicht-Transparenz (je Option):
"actual_weight_g": 700, "volumetric_weight_g": 1224, "billable_weight_g": 1224,
"volumetric_divisor": 5000,
"weight_hint": "Volumengewicht bindend — ein kleinerer Karton spart Porto." }
], "count": 1,
"package_used": { "weight_g": 700, "length_cm": 24, "width_cm": 18, "height_cm": 17,
"source": "variant_metafield" } } // source: variant_metafield | default_package | request | none
Volumengewicht: Express-Carrier berechnen nach billable_weight_g =
max(actual, volumetric), wobei volumetric = L×B×H ÷ divisor. Ist
weight_hint gesetzt, bindet das Volumengewicht — dann entscheidet der Karton über den
Preis, nicht das Artikelgewicht. volumetric_weight_g/volumetric_divisor
sind null bei Carriern ohne Volumengewicht (z. B. DHL Paket).
FedEx-Wertdeklaration im Tarif: FedEx-Optionen fragen FedEx mit dem Declared Value an,
den das Label melden würde (Warenwert der Bestellung + Shop-Einstellung, oder declared_value im
Body). Jede Option nennt declared_value, declared_value_fee (die Gebühr laut FedEx,
in price enthalten), declared_value_reason und postage
(price ohne die Gebühr). Bei anderen Carriern sind die drei declared_value-Felder
null und postage = price. Details im Abschnitt
„FedEx-Wertdeklaration“ unten.
Packstation und DHL-Postfiliale (seit 1.3.17): Ist die Lieferadresse eine DHL Packstation
oder DHL-Postfiliale in Deutschland (erkannt an address1/address2, z. B.
„Packstation 138“ oder „Postfiliale 512“ — nie an Firma oder Name), enthält options nur
DHL Paket (carrier: "dhl"), auch keine Portokasse. Die Antwort trägt dann zusätzlich
packstation_schutz; ohne Abholstelle fehlt das Feld. Ist DHL Paket im
Shop nicht nutzbar, ist options leer und meldung nennt den Grund. Ist DHL Paket
nutzbar, hat für die Sendung aber keinen passenden Tarif (Gewicht, Maße, Versicherung), ist options
ebenfalls leer und meldung sagt das (ohne „Bitte wähle DHL Paket“). absender_land ist das
Absenderland des Shops. Für Empfänger außerhalb Deutschlands ändert sich nichts.
"packstation_schutz": { "aktiv": true, "abholstelle": "packstation", // oder "postfiliale"
"erlaubt": ["dhl"], "dhl_eingerichtet": true, "absender_land": "DE",
"meldung": "An eine DHL Packstation kann nur DHL Paket zustellen. Bitte wähle DHL Paket." }
Adresse fürs Label (seit 1.3.20): Ändert KYTH.Karl die Lieferadresse fürs Label (z. B.
„12 Hauptstraße“ → „Hauptstraße 12“, eine Eircode in die Form „D02 X285“) oder hat es dazu etwas zu melden
(Packstation, fehlende Postnummer, Eircode), trägt die Antwort zusätzlich address — sonst fehlt das Feld.
Der Carrier steht hier noch nicht fest; gesperrt wird erst beim Label. Die options ändern sich dadurch
nicht. Aufbau im Abschnitt „Adresse fürs Label“ unten.
service ist der buchbare Code — gib ihn unverändert
an /labels weiter. service_name ist nur für die Anzeige.service der Anzeigename
(z. B. „UPS Standard“), obwohl /labels den Code erwartet — ein daraus erzeugtes Label
scheiterte. Zeigt dein System service als Text an, stell die Anzeige auf service_name um.
Namen werden bei /labels übergangsweise weiter akzeptiert und in den Code übersetzt.Label erstellen + (optional) drucken. Header Idempotency-Key ist Pflicht (max 200 Zeichen).
// Header: Idempotency-Key: <eindeutige ID pro Versuch, z.B. UUID>
// Request
{ "carrier": "ups", "service": "11", // carrier + service UNVERÄNDERT aus /rates
"package": { "weight_g": 2500, "length_cm": 30, "width_cm": 20, "height_cm": 15 },
"print": "companion" } // "companion" (Druck via KYTH.Karl-Companion) | "none" (nur PDF zurück)
// Response
{ "tracking_number": "1Z…", "carrier": "ups", "service": "11",
"cost": 6.20, "currency": "EUR", "label_base64": "JVBERi0…",
"printed": true, "fulfilled": true,
"declared_value": null, "declared_value_fee": null, // FedEx: siehe „FedEx-Wertdeklaration“
"invoice_source": null, "plt_uploaded": null } // DHL Express: Handelsrechnung (seit 1.3.24)
Das Label wird automatisch im Shopify-Fulfillment hinterlegt (Tracking an den Kunden). Bei print: "none" druckst du das label_base64-PDF selbst.
Vor der Buchung prüft KYTH.Karl, ob der Dienst für diese Sendung wirklich angeboten wird.
Nicht buchbar → 400 service_unavailable; in details.available stehen die buchbaren
Codes, und der Idempotency-Key bleibt frei. Prüfung gerade nicht möglich (Carrier nicht erreichbar) →
503 service_check_failed; es wurde nichts gebucht, später mit demselben Key erneut senden.
Die Prüfung ist eine echte Preisabfrage — direkt nach /rates ist sie schnell.
Packstation und DHL-Postfiliale: Geht die Sendung an eine DHL Packstation oder
DHL-Postfiliale in Deutschland und ist ein anderer Carrier als dhl gewählt oder DHL Paket im Shop
nicht nutzbar → 422 packstation_nur_dhl_paket, noch vor der Dienst-Prüfung. Es wird nichts gebucht,
der Idempotency-Key bleibt frei. details nennt abholstelle (packstation |
postfiliale), carrier, dhl_eingerichtet und erlaubt
(["dhl"]).
Bundesstaat (USA, Kanada, Australien, Mexiko): Fehlt in der Lieferadresse der Bundesstaat bzw.
die Provinz oder passt er nicht zum Land → 422 bundesstaat_fehlt, ebenfalls vor der Dienst-Prüfung —
die Carrier nehmen solche Sendungen nicht an. Es wird nichts gebucht, der Idempotency-Key bleibt frei. Steht in
der Shopify-Adresse nur der Name (province: "New York"), leitet KYTH.Karl den Code selbst ab.
details nennt feld (province_code), land, grund
(fehlt | ungueltig) und wert.
Postnummer und Eircode (seit 1.3.20): Mit carrier: "dhl" an eine DHL Packstation oder
DHL-Postfiliale ohne Postnummer → 422 packstation_postnummer_fehlt (bis 1.3.19 kam das als
400 carrier_error). Mit carrier: "fedex" nach Irland ohne Eircode oder mit einer Eircode in
falscher Form → 422 eircode_fehlt bzw. 422 eircode_ungueltig. Beides ebenfalls vor der
Dienst-Prüfung: Es wird nichts gebucht, der Idempotency-Key bleibt frei; Code und Text sind dieselben wie im
Trockenlauf. Mehr im Abschnitt „Adresse fürs Label“.
Unvollständige Lieferadresse: Fehlt in der Lieferadresse der Bestellung ein Pflichtfeld (Straße,
PLZ, Ort, Land, Name oder Firma) — etwa die PLZ bei einer irischen Adresse ohne Eircode —, antwortet der Kauf mit
400 invalid_recipient, details.missing nennt die Felder (bis 1.3.19 kam das als
400 carrier_error). Ebenfalls vor der Dienst-Prüfung, der Idempotency-Key bleibt frei; der Trockenlauf
meldet denselben Code und Text vorher in errors.
Teilweise versendete Bestellung (seit 1.3.77): Ist ein Teil der Bestellung schon versendet (in der
App, außerhalb von KYTH.Karl oder über die API), wird das Label eine Teilsendung über die offenen Positionen:
Zollerklärung, Versicherung und Warenwert enthalten nur die noch offenen Mengen (open_quantity), und
KYTH.Karl markiert in Shopify nur diese Positionen als versendet; das erste Teil-Label bleibt aktiv. Tragen die
Positionen keine Kennung → 422 partial_shipment_unavailable (dann wäre nur die ganze Bestellung
markierbar) — vor der Dienst-Prüfung, nichts gebucht, der Idempotency-Key bleibt frei. Der Trockenlauf meldet
denselben Code in errors; checks.partial_shipment gibt es nur bei einer teilweise versendeten
Bestellung. Ausnahme: eine eBay-Bestellung (ebay:…), für die KYTH.Karl schon ein aktives Label hat — eBay
führt eine Position auch dann als offen, wenn nur die Sendungsnummer dort nicht ankam. Dann bucht KYTH.Karl kein
Teil-Label, der Kauf endet wie bis 1.3.76 mit 409 (der Text nennt das vorhandene Label), und der
Trockenlauf meldet keine checks.partial_shipment.
Trockenlauf (dry_run): Setz "dry_run": true im Body — als echten
Wahrheitswert, nicht als Text "true" (das ergibt 400 invalid_request) —, um deinen
Flow zu testen, ohne echtes Porto zu kaufen. KYTH.Karl validiert Paket, Zoll und
Carrier/Service-Verfügbarkeit und meldet alle Probleme auf einmal — ohne Buchung,
Charge, Fulfillment oder Druck. Kein Idempotency-Key nötig (kein Seiteneffekt).
Der Trockenlauf spiegelt den echten Pfad: package gehört mitgeschickt (aus /rates
package_used) — fehlt es, meldet der Report checks.package: false + einen
invalid_package-Fehler, genau wie die echte Erstellung.
// Request
{ "carrier": "ups", "service": "11", "dry_run": true,
"package": { "weight_g": 700, "length_cm": 24, "width_cm": 18, "height_cm": 17 } }
// Response
{ "dry_run": true, "valid": true, "carrier": "ups", "service": "11", "service_name": "UPS Standard",
"checks": { "package": true, "customs_item_data": true, "customs_validation": true, "address": true,
"service_available": true },
"errors": [], // bei valid:false z.B. {"code":"customs_validation_failed", …}
"warnings": [],
"address": { "vorher": { … }, "nachher": { … }, "korrekturen": [], "abholstelle": null,
"hinweise": [], "fehler": [] }, // seit 1.3.20, siehe „Adresse fürs Label“
"package_used": { "weight_g": 700, "length_cm": 24, "width_cm": 18, "height_cm": 17, "source": "variant_metafield" },
"estimated_cost": 6.20, "currency": "EUR", "postage": 6.20,
"declared_value": null, "declared_value_fee": null, "declared_value_reason": null, "customs_value": null }
// Response — dieselbe Anfrage an eine DHL Packstation (UPS stellt dort nicht zu)
{ "dry_run": true, "valid": false, "carrier": "ups", "service": "11",
"checks": { "abholstelle": false, … },
"errors": [ { "code": "packstation_nur_dhl_paket",
"message": "An eine DHL Packstation kann nur DHL Paket zustellen. Bitte wähle DHL Paket." } ],
… }
checks.abholstelle gibt es nur, wenn die Lieferadresse eine DHL Packstation oder DHL-Postfiliale in
Deutschland ist: true, wenn der gewählte Carrier dort buchbar ist (dhl, DHL Paket im Shop
nutzbar), sonst false plus der Fehler packstation_nur_dhl_paket — die echte Buchung
antwortet dann mit 422 packstation_nur_dhl_paket.
Fehlt bei einem Empfänger in den USA, in Kanada, Australien oder Mexiko der Bundesstaat bzw. die Provinz, meldet
der Trockenlauf valid: false, checks.recipient_state: false (das Feld gibt es nur bei diesen
vier Ländern) und in errors:
{ "code": "bundesstaat_fehlt", "field": "province_code", "country": "US",
"message": "Für Sendungen in die USA braucht der Carrier den Bundesstaat bzw. die Provinz des Empfängers. Bitte in der Adresse ergänzen." }
Die echte Buchung antwortet dann mit 422 bundesstaat_fehlt.
Seit 1.3.20 zeigt der Trockenlauf außerdem, was aufs Label geht: address,
checks.address (false bei fehlender Postnummer bzw. bei FedEx nach Irland ohne gültige
Eircode, dazu der Fehler in errors) und je automatischer Korrektur einen Eintrag
adresse_korrigiert in warnings — siehe „Adresse fürs Label“.
CPSC (US-Produktsicherheit): Trägt eine Position eine CPSC-Angabe (Shopify-Metafeld
karl_compliance.cpsc, Format Certifier-ID / Product-ID / Version-ID), übermittelt KYTH.Karl
sie bei Sendungen in die USA an FedEx elektronisch (Registry-Verweis) und an UPS im Kommentar der Handelsrechnung
(höchstens 150 Zeichen je Sendung). Das blockiert nie. Hinweise stehen in warnings — im Trockenlauf und
in der Label-Antwort — als { "code", "message", "items" } (items = 0-basierte
Positionen): cpsc_format_nicht_erkannt (Wert nicht im Format; er geht nur als Text auf die Rechnung,
FedEx bekommt keinen Registry-Verweis) und cpsc_ups_kommentar_voll (passte nicht mehr ins
UPS-Kommentarfeld — über ups.com nachreichen).
Seit 1.3.23 geht die CPSC-Angabe bei DHL Express (Ziel USA) als eine Bemerkung („Remarks“, höchstens 300 Zeichen) auf die Handelsrechnung, die DHL erstellt. Passt nicht alles hinein, meldet die Label-Antwort cpsc_dhl_express_bemerkung_voll in warnings ({ "code", "message" }, ohne items; der Trockenlauf meldet es nicht) — die vollständigen Angaben dann dem Paket beilegen.
Handelsrechnung bei DHL Express (seit 1.3.23): DHL erstellt sie selbst aus den Zolldaten. Nur in zwei Rückfällen kommt sie von KYTH.Karl; die Label-Antwort meldet das in warnings als { "code", "message" }: rechnung_zum_druck (DHL hat gebucht, aber keine Rechnung erzeugt — elektronisch liegt bei DHL keine; die Rechnung von KYTH.Karl in der App unter „Zollrechnung“ ausdrucken und dem Paket beilegen), rechnung_fehlt (dasselbe, und auch KYTH.Karl konnte keine erstellen) und rechnung_von_karl (DHL lehnte die Rechnungserstellung ab, KYTH.Karl hat seine eigene elektronisch mitgeschickt — nur zur Info).
Handelsrechnung bei UPS über eine Zollgrenze in die EU (seit 1.3.24): UPS bekommt die elektronische Handelsrechnung (mit dem Grund der Ausfuhr) von KYTH.Karl nur bei Zielen außerhalb der EU. Überquert eine UPS-Sendung trotzdem eine Zollgrenze — etwa ein Schweizer Absender nach Deutschland —, meldet die Label-Antwort in warnings rechnung_beilegen: eine Handelsrechnung ausdrucken und dem Paket beilegen; unter „Zollrechnung“ liegt für diese Sendung keine.
Seit 1.3.24 steht dasselbe zusätzlich als Feld in der Label-Antwort (auch in der Wiederholung und in GET /api/v1/labels/{idempotency_key}):
invoice_source: "dhl" (DHL hat die Handelsrechnung erstellt — der Normalfall), "karl" (DHL lehnte ab, KYTH.Karl hat seine eigene elektronisch mitgeschickt), "karl_druck" (DHL hat keine erstellt, die von KYTH.Karl liegt nur zum Ausdrucken bereit — dem Paket beilegen) oder null (keine Handelsrechnung, z. B. innerhalb der EU).plt_uploaded: true = die Handelsrechnung liegt elektronisch bei DHL (Paperless Trade), false = nicht (bei karl_druck immer).null (keine Angabe — nicht „keine Rechnung“). Labels, die vor 1.3.24 gekauft wurden, melden in der Wiederholung und im Status null.Weitere optionale Felder: incoterm (DAP | DDP | DDU; weglassen =
Shop-Standard), insurance_value und — nur FedEx — declared_value
(siehe nächster Abschnitt).
declared_value) und insurance_valuedeclared_value gilt nur für FedEx: der Betrag, den KYTH.Karl FedEx als
Declared Value meldet — in der Währung der Sendung, höchstens der Warenwert. Ohne Wertdeklaration
haftet FedEx bei Verlust oder Schaden nur nach Gewicht (Montrealer Übereinkommen, rund 30 € je kg).
null — die Shop-Einstellung entscheidet (Einstellungen → Carrier →
FedEx → Declared Value: „Warenwert bis Freigrenze deklarieren“ und „Sendungen über … € voll deklarieren“).0 — keine Wertdeklaration für diese Sendung.declared_value_note in warnings).true/false, negativ) → 400 invalid_request, auch im
Trockenlauf. Bei anderen Carriern bleibt das Feld ohne Wirkung (Hinweis declared_value_not_applicable).insurance_value ist der Versicherungswert in EUR für UPS, DHL und die übrigen Carrier
(weglassen = Shop-Einstellung, 0 = keine, Betrag = dieser Wert). Bei FedEx meldet KYTH.Karl seit
1.3.16 ausschließlich den Declared Value: Schickst du dort nur insurance_value, liest KYTH.Karl ihn
als declared_value (0 = keine) und sagt es in warnings
(insurance_value_as_declared_value). Mit beiden Feldern zählt declared_value.
Die Gebühr nennt FedEx selbst. Sie hängt vom Paket ab — gemessen fällt oft nichts an, ab etwa 50 € deklariertem Wert (je nach Gewicht) rund 0,92 € je Stufe. KYTH.Karl rechnet sie nie selbst aus: Tarif und Trockenlauf fragen FedEx mit genau dem Betrag an, den das Label melden wird, und zeigen die Gebühr von FedEx, bevor du buchst.
declared_value ({ amount, currency } oder
null = keiner), declared_value_fee (in currency, in price bzw.
estimated_cost enthalten), declared_value_reason und postage
(ohne die Gebühr). declared_value_fee: null heißt: FedEx hat keinen Betrag genannt — unbekannt,
nicht 0; dann ist auch postage null. Der Trockenlauf nennt zusätzlich
customs_value (Warenwert = Zollwert, Obergrenze jeder Deklaration).GET /api/v1/labels/{idempotency_key}:
declared_value ist der Betrag, der tatsächlich an FedEx ging;
declared_value_fee in EUR, aus der Label-Antwort von FedEx
(declared_value_fee_source: "fedex_ship") oder aus dem geprüften Tarif ("fedex_rate"),
sonst null. Bei anderen Carriern sind alle diese Felder null.declared_value_reason: free_limit (bis Freigrenze), full_above (voller
Warenwert über der Schwelle), override, override_none, off,
no_goods_value, mixed_currency, no_fx_rate.// Trockenlauf FedEx, Warenwert 190 EUR, Einstellung „bis Freigrenze 100 €“ an
{ "dry_run": true, "valid": true, "carrier": "fedex", "service": "INTERNATIONAL_PRIORITY",
"estimated_cost": 48.57, "currency": "EUR", "postage": 47.65,
"declared_value": { "amount": 100.0, "currency": "EUR" }, "declared_value_reason": "free_limit",
"declared_value_fee": 0.92, "declared_value_fee_currency": "EUR",
"customs_value": { "amount": 190.0, "currency": "EUR" }, … }
// dieselbe Sendung ohne Wertdeklaration
{ "carrier": "fedex", "service": "INTERNATIONAL_PRIORITY", "dry_run": true, "declared_value": 0, … }
address), Postnummer und EircodeSeit 1.3.20 zeigen Trockenlauf, Entwurf und Tarife, was aufs Label geht — dieselbe Deutung, mit der KYTH.Karl beim Kauf bucht, ohne Carrier-Aufruf. KYTH.Karl schreibt nichts an Shopify zurück: Die Bestellung bleibt, wie sie ist; die Korrekturen gelten nur fürs Label.
// Trockenlauf DHL an eine Packstation, die Postnummer steht im Feld Firma
"valid": true,
"checks": { "address": true, … },
"warnings": [ { "code": "adresse_korrigiert",
"message": "Empfängeradresse automatisch korrigiert: Postnummer aus dem Feld Firma übernommen (12345678 → Postnummer 12345678)",
"before": "12345678", "after": "Postnummer 12345678" } ],
"address": {
"vorher": { "address1": "Packstation 138", "address2": "", "company": "12345678", "zip": "10115" },
"nachher": { "street": "Packstation 138", "address2": "", "zip": "10115" },
"korrekturen": [ { "typ": "postnummer_aus_firma", "label": "Postnummer aus dem Feld Firma übernommen",
"vorher": "12345678", "nachher": "Postnummer 12345678" } ],
"abholstelle": { "art": "packstation", "nummer": "138", "postnummer": "12345678",
"strasse": "Packstation 138", "firma_ist_postnummer": true },
"hinweise": [], "fehler": [] }
// dieselbe Packstation ohne Postnummer → "valid": false, "checks": { "address": false, … }
"errors": [ { "code": "packstation_postnummer_fehlt", "field": "postnummer",
"message": "Für eine Packstation braucht DHL die Postnummer des Empfängers (6–10 Ziffern). Keine Postnummer gefunden" } ]
// FedEx nach Irland ohne Eircode
"errors": [ { "code": "eircode_fehlt", "field": "zip", "country": "IE",
"message": "FedEx braucht für Irland eine Eircode (z. B. D02 X285). Bitte in der Adresse ergänzen." } ]
vorher ist deine Eingabe, nachher das, was an den Carrier geht (Straße, Zusatz, PLZ).
korrekturen nennt jede Änderung mit typ, label, vorher und
nachher — etwa dach_number_moved („12 Hauptstraße“ → „Hauptstraße 12“, nur Deutschland,
Österreich, Schweiz), fields_combined („5“ und „Bergweg“ → „Bergweg 5“), eircode_aufgeraeumt,
abholstelle_gedeutet, postnummer_aus_firma, firma_geleert und
postnummer_gekuerzt. Im Trockenlauf steht jede Korrektur zusätzlich in warnings als
adresse_korrigiert mit before und after. „15 Rue de la Paix“ (Frankreich) bleibt
unverändert.abholstelle: DHL Packstation oder DHL-Postfiliale, so wie DHL sie bucht (strasse z. B.
„Packstation 138“, die Postnummer als eigene Angabe), sonst null. Die Postnummer (6–10 Ziffern) gehört in
address2; steht sie im Feld Firma, übernimmt KYTH.Karl sie von dort. Fehlt die Stationsnummer, meldet
hinweise packstation_nummer_fehlt — keine Sperre.fehler sperren den Kauf: packstation_postnummer_fehlt (bei carrier: "dhl") sowie bei
FedEx nach Irland eircode_fehlt und eircode_ungueltig. Der Trockenlauf meldet sie in
errors (valid: false, checks.address: false), der Kauf antwortet mit
422 und demselben Code und Text.zip ist die Eircode. KYTH.Karl bringt sie für jeden Carrier in die Form
„D02 X285“ (Großbuchstaben, 3 + 4 Zeichen). Gesperrt wird nur bei FedEx, wenn sie fehlt oder nicht die richtige Form
hat; DHL Paket, DHL Express, UPS, DPD und die Österreichische Post bekommen bei falscher Form nur einen Hinweis. Fehlt
sie ganz, ist zip leer — ohne PLZ bucht KYTH.Karl mit keinem Carrier ein Label (invalid_recipient,
im Trockenlauf in errors, dazu der Hinweis eircode_fehlt). KYTH.Karl prüft die
Form, nicht ob FedEx den Bezirk kennt — eine Eircode in richtiger Form kann FedEx trotzdem ablehnen
(dann carrier_error).address mit offenem Carrier und nur, wenn es etwas zu sagen gibt. Eine
fehlende Postnummer an einer Packstation in Deutschland steht dort schon in fehler, eine Eircode nur in
hinweise; gesperrt wird erst beim Label.Label ohne Bestellung — wie „Manuelles Label“ in der App, etwa für Walk-ins, B2B-Sendungen
oder Ersatzlieferungen, die es in Shopify nicht als Bestellung gibt. Empfänger und Paket kommen aus dem Body,
Absender ist die Absenderadresse aus den Einstellungen. Es gibt kein Shopify-Fulfillment
(fulfilled ist immer false).
// 1) POST /api/v1/rates
{ "recipient": { "company": "Muster AG", "first_name": "Max", "last_name": "Muster",
"address1": "Bahnhofstrasse 1", "zip": "8001", "city": "Zürich", "country_code": "CH",
"phone": "+41 44 000 00 00", "email": "max@muster.ch" },
"package": { "weight_g": 2500, "length_cm": 30, "width_cm": 20, "height_cm": 15 } }
// → { "options": [ … wie bei /orders/{order_id}/rates … ], "count": 4,
// "package_used": { "weight_g": 2500, "length_cm": 30, "width_cm": 20, "height_cm": 15, "source": "request" } }
// 2) POST /api/v1/labels Header: Idempotency-Key
{ "recipient": { … wie oben … },
"package": { "weight_g": 2500, "length_cm": 30, "width_cm": 20, "height_cm": 15 },
"carrier": "fedex", "service": "INTERNATIONAL_PRIORITY", // unverändert aus /api/v1/rates
"reference": "Ersatzlieferung 4711", // optional, steht wie eine Bestellnummer im Archiv
"items": [ { "description": "Holzrelief Eiche", "quantity": 1, "weight_g": 2300,
"value": 120.00, "currency": "EUR", "hs_code": "44209990", "origin_country": "DE" } ],
"print": "companion" }
// → { "tracking_number": "…", "carrier": "fedex", "service": "INTERNATIONAL_PRIORITY",
// "cost": 93.69, "currency": "EUR", "label_base64": "JVBERi0…", "printed": true, "fulfilled": false }
Empfänger: Pflicht sind country_code, address1, zip,
city und company oder ein Name. Fehlt etwas → 400 invalid_recipient, in
details.missing stehen die fehlenden Felder.
Bundesstaat (USA, Kanada, Australien, Mexiko): dazu province_code (z. B.
"NY", "ON", "NSW") oder province mit dem Namen
("New York" — KYTH.Karl leitet den Code ab). Fehlt er oder passt er nicht zum Land →
422 bundesstaat_fehlt (Trockenlauf: Fehler in errors,
checks.recipient_state: false). Nichts gebucht, der Idempotency-Key bleibt frei.
Packstation und DHL-Postfiliale: Steht in address1 oder address2 eines
Empfängers in Deutschland eine DHL Packstation oder DHL-Postfiliale (z. B. „Packstation 138“, die Postnummer
in address2), liefert /api/v1/rates nur DHL-Paket-Optionen plus
packstation_schutz, und /api/v1/labels nimmt nur carrier: "dhl" an — sonst
422 packstation_nur_dhl_paket (Trockenlauf: checks.abholstelle: false), wie bei
/orders/{order_id}/labels.
Postnummer und Eircode: wie bei /orders/{order_id}/labels —
422 packstation_postnummer_fehlt (carrier: "dhl", Postnummer in address2) und
422 eircode_fehlt / 422 eircode_ungueltig (carrier: "fedex" nach Irland), vor
Varianten, Dienst-Prüfung und Idempotency-Key. Der Trockenlauf nennt address (siehe „Adresse fürs
Label“).
items sind die Zollpositionen. Ins Drittland (z. B. Schweiz) Pflicht: je Position
description, weight_g und value (sonst 422 customs_data_missing) sowie hs_code und
origin_country (sonst 422 customs_validation_failed). Jede Position braucht außerdem currency (sonst 400 invalid_items). weight_g und value gelten je Stück. Optional je Position compliance: { "cpsc": "Certifier-ID / Product-ID / Version-ID" } — die CPSC-Angabe für US-Sendungen (siehe oben); nur der Schlüssel cpsc, Text bis 300 Zeichen, sonst 400 invalid_items. Ein anderes Format ist kein Fehler: Der Text geht dann nur auf die Rechnung, warnings meldet cpsc_format_nicht_erkannt. Ohne variant_id oder sku setzt KYTH.Karl keine Zollwerte selbst ein, die Währung nie (siehe „Positionen aus Shopify übernehmen“).
dry_run, incoterm, insurance_value, declared_value und print
funktionieren wie bei /orders/{order_id}/labels (siehe „FedEx-Wertdeklaration“). Grundlage der
Deklaration ist der Warenwert der items (Wert × Menge); bei Positionen in verschiedenen Währungen
deklariert KYTH.Karl nichts (declared_value_reason: "mixed_currency").
/api/v1/rates kennt keine items: Für FedEx-Gebühren im Tarif schick dort
goods_value (Warenwert) mit goods_currency (ISO-Code, dazu Pflicht) und optional
declared_value — ohne sie fragt KYTH.Karl FedEx ohne Wertdeklaration an.
422 idempotency_key_conflict — nie mit dem Label der ersten Anfrage. Eine echte Wiederholung
(gleicher Key, gleicher Inhalt) bekommt das bereits erstellte Label zurück.variant_id / sku)Statt der Einzelwerte darf eine Position variant_id oder sku tragen. Dann belegt
KYTH.Karl, was fehlt, aus der Shopify-Variante — aus denselben Feldern, die es bei Bestellungen liest:
| Feld | Quelle in Shopify |
|---|---|
hs_code | HS-Code für das Zielland (Quelle country_rule), sonst der HS-Code der Variante |
origin_country | Ursprungsland der Variante |
description | Metafeld karl_customs.description oder custom.customs_description, sonst der Produkttitel |
weight_g | Gewicht der Variante (je Stück) |
value | Preis der Variante — nur wenn die Shopwährung der currency der Position entspricht |
compliance | Metafeld karl_compliance.cpsc (Variante vor Produkt) als { "cpsc": "…" } — nur wenn die Position keine CPSC-Angabe mitschickt |
"items": [ { "variant_id": "44012345678", "quantity": 12, "value": 77.35, "currency": "EUR" } ]
currency bleibt Pflicht.422 customs_validation_failed und nennt Produkt und Variante in details.variants.sku wird nur nachgeschlagen, wenn ein Wert fehlt — bei Empfängern in den USA auch, wenn nur die CPSC-Angabe fehlt. Ist sie in Shopify unbekannt oder mehrfach vergeben, bleibt die Position wie mitgeschickt — mit Hinweis in items_resolved[].notes (eine eigene ERP-SKU stört also nicht). Eine unbekannte variant_id → 400 variant_not_found.package darf fehlen, wenn die Varianten Maße haben (karl_dims): Karton der größten Variante, Gewicht = Stückgewicht × Menge, package_used.source = variant_metafield. Ohne Maße bleibt es bei 400 invalid_package — kein Standardpaket als stiller Ersatz.items_resolved: die Werte, die Quelle je Feld (request, variant, country_rule, product_title; compliance: request oder variant) und description_sent — die Beschreibung, wie sie beim Carrier ankommt.warnings: items_heavier_than_package (Positionen zusammen schwerer als das Paket) und description_truncated (FedEx übernimmt 450 Zeichen, DHL Paket 256, DHL Express 512, Österreichische Post 50, UPS 35).Entwürfe — Labels vorbereiten, aber noch nicht kaufen. Es sind dieselben Entwürfe wie „Als Entwurf speichern“ im Manuellen Label: API-Entwürfe erscheinen in der App und umgekehrt.
// Anlegen: Body wie POST /api/v1/labels, dazu "name", ohne dry_run — es wird nichts gekauft
POST /api/v1/drafts
{ "name": "THE ICON Paket 1 FedEx",
"recipient": { … }, "package": { "weight_g": 9900, "length_cm": 40, "width_cm": 40, "height_cm": 40 },
"carrier": "fedex", "service": "INTERNATIONAL_PRIORITY", "incoterm": "DDP",
"reference": "S-26-00067 THE ICON", "items": [ … ], "print": "companion" }
// → { "id": 101, "name": "THE ICON Paket 1 FedEx", "created_at": "…", "updated_at": "…",
// … gespeicherte Felder …, "valid": true, "checks": { … }, "errors": [], "warnings": [],
// "estimated_cost": 93.69, "currency": "EUR", "items_resolved": [ … ] }
GET /api/v1/drafts?reference=S-26-00067%20THE%20ICON&limit=20 // neueste zuerst, "next_after" → ?after=
GET /api/v1/drafts/101
PATCH /api/v1/drafts/101 { "service": "INTERNATIONAL_ECONOMY" } // jedes Feld ersetzt das gespeicherte
DELETE /api/v1/drafts/101
POST /api/v1/drafts/101/labels Header: Idempotency-Key { "print": "none" } // optional: dry_run, print
id. Gleiche Namen werden nie zusammengeführt; mehrere Entwürfe für denselben Empfänger (ein Paket je Entwurf) stehen nebeneinander. name ist frei wählbar, Standard „Firma – Datum“.PATCH neue items schickst.dry_run und print an. Nach dem Kauf ist der Entwurf gelöscht; eine Wiederholung mit demselben Idempotency-Key bekommt trotzdem das Label.dhl wird gespeichert, der Prüfbericht meldet aber valid: false mit dem Fehler packstation_nur_dhl_paket; das Buchen antwortet mit 422 packstation_nur_dhl_paket.valid: false mit bundesstaat_fehlt; das Buchen antwortet mit 422 bundesstaat_fehlt.address (siehe „Adresse fürs Label“). Fehlt an einer DHL Packstation die Postnummer (carrier: "dhl") oder bei FedEx nach Irland eine gültige Eircode, wird der Entwurf gespeichert, der Prüfbericht meldet valid: false; das Buchen antwortet mit 422 packstation_postnummer_fehlt bzw. 422 eircode_fehlt / eircode_ungueltig.declared_value (FedEx-Wertdeklaration) wird gespeichert wie geschickt — auch 0 (keine) bleibt erhalten und erscheint in der App. In der Antwort von POST/PATCH bleibt declared_value das gespeicherte Feld; was KYTH.Karl FedEx daraus melden würde, steht in declared_value_resolved (dazu declared_value_fee, declared_value_reason, customs_value, postage).409 draft_limit_reached. Unbekannte id → 404 draft_not_found.Kauf-/Status eines Label-Requests abfragen — ohne einen erneuten POST zu
wagen. Praktisch nach einem Timeout: hat der erste Versuch gekauft oder nicht?
{ "idempotency_key": "7d3f-uuid",
"status": "completed", // "in_progress" | "completed" | "unknown"
"purchased": true, // true = Label existiert
"tracking_number": "1Z…", "carrier": "ups", "service": "11",
"cost": 6.20, "currency": "EUR", "printed": true, "fulfilled": true,
"declared_value": null, "declared_value_fee": null, // FedEx: an FedEx gemeldeter Wert + Gebühr (EUR)
"invoice_source": null, "plt_uploaded": null } // DHL Express: Handelsrechnung (seit 1.3.24)
Unbekannter Key → 404 idempotency_key_not_found.
Label stornieren — beim Carrier, nicht nur in KYTH.Karl. Funktioniert für Labels mit und ohne Bestellung. Gehört zu einer Bestellung kein aktives Versandlabel mehr, nimmt KYTH.Karl auch das Shopify-Fulfillment zurück.
POST /api/v1/labels/1ZGV13950417264545/void
{ "notify_customer": false } // optional; ohne Angabe entscheidet deine Mail-Vorlage
// Antwort
{ "tracking_number": "1ZGV13950417264545", "carrier": "ups",
"voided": true, // true = der Carrier hat bestätigt
"carrier_confirmed": true,
"message": "UPS Label storniert" }
voided: true).
Bestätigt der Carrier nicht, bleibt das Label in KYTH.Karl unverändert (aktiv bzw. ersetzt) und die Antwort ist
200 mit voided: false plus Grund in message — du kannst es erneut versuchen."notify_customer": true raus.message den manuellen Weg.404 label_not_found. Schon storniert, gerade in Stornierung
oder aus KYTH.Karl entfernt → 409 label_not_active.Sendungsstatus und voraussichtliches Zustelldatum zu einer Tracking-Nummer, mit den
Stationen beim Carrier. Ist der gespeicherte Stand älter als 15 Minuten (oder gibt es noch keinen),
fragt KYTH.Karl vor der Antwort beim Carrier nach — über die API höchstens einmal je Sendung und
15 Minuten, Antwort nach spätestens etwa 20 Sekunden. Klappt das nicht, kommt der letzte Stand mit
"stale": true, nie ein 5xx. Bei DHL Paket fragt KYTH.Karl über den GKP-Login deines Shops
(eigenes Kontingent von 1.000 Abfragen am Tag, bis zu 3 Abfragen je Sekunde für alle Shops zusammen);
zugestellte Sendungen fragt KYTH.Karl dort nicht erneut ab. Über den gemeinsamen Zugang — Sendungen von
vor dem Umstieg und solange DHL deinen Zugang ablehnt — geht höchstens eine Abfrage alle 5 Sekunden zu
DHL: Mehrere DHL-Sendungen gleichzeitig abgerufen, kommen dann einige mit "stale": true — ein
erneuter Abruf frischt sie auf; am besten nacheinander abrufen. Sendungen, die niemand abruft,
behalten ihren letzten Stand (auch in der Liste). Nur Sendungen deines eigenen Shops.
{ "tracking_number": "1Z…", "carrier": "ups", "service": "11",
"status": "transit", // label_created | pickup | transit | out_for_delivery | delivered
// | returned | exception | unknown | not_supported | pending
"status_label": "Unterwegs", "delivered": false,
"estimated_delivery": "2026-07-21",
"tracking_url": "https://www.ups.com/track?tracknum=1Z…",
"is_return": false,
"order": { "id": "7335812137023", "name": "#1028" },
"created_at": "2026-07-17T10:59:33Z",
"status_updated_at": "2026-07-18T06:00:12Z",
"created_by": "Anna", // wer die Sendung erstellt hat
"created_by_kind": "staff", // staff | system | api
"stale": false, // true = Stand älter als 15 Min., Auffrischen ging gerade nicht
"exception": null, // letztes Zustellproblem: { code, text, at, location }
"events": [ // Stationen, neueste zuerst (höchstens 30)
{ "at": "2026-07-18T07:55:00", "code": "I", "text": "Arrived at Facility", "location": "Neuss, DE" } ] }
status: label_created = Label erstellt, beim Carrier nur angekündigt;
exception = Zustellproblem (Details in exception); unknown = der
Carrier kennt die Nummer nicht. Bei DPD, Österreichischer Post und Internetmarke (ohne Live-Tracking)
bleibt label_created stehen; not_supported kommt nur bei älteren Sendungen vor.events und exception gibt es nur im Einzelabruf. at ist Ortszeit
ohne Zeitzone (FedEx und DHL Paket in deutscher Zeit, UPS und DHL Express in Ortszeit des Scans),
code der Code des Carriers. exception bleibt nach einer späteren Zustellung
sichtbar.status_updated_at: wann KYTH.Karl den Stand zuletzt erfolgreich beim Carrier abgefragt hat.created_by_kind: staff = ein am Packplatz angemeldeter Mitarbeiter
(created_by ist sein Name; null, wenn niemand angemeldet war),
system = Automatik (created_by nennt den Auslöser, z. B.
"Auto-Druck" oder "Retourenportal"; null, wenn keiner bekannt ist), api = ein API-Key
(created_by ist sein Name). Sendungen von vor Version 1.3.0 tragen bei beiden null.404 shipment_not_found.Alle Sendungen eines Zeitraums, neueste zuerst — für den Tagesabgleich, ohne jede
Sendung einzeln abzufragen. Jede Zeile hat dasselbe Format wie beim Einzelabruf oben, ohne
stale, exception und events; die Liste fragt nicht beim Carrier nach.
Query: from und to (JJJJ-MM-TT, Kalendertage in deutscher Zeit,
to inklusive; ohne to gilt heute, ohne from derselbe Tag wie
to), limit (1–200, Standard 50), after (Wert aus next_after).
GET /api/v1/shipments?from=2026-09-18&to=2026-09-18&limit=100
{ "shipments": [ { "tracking_number": "1Z…", "carrier": "ups", "status": "transit",
"created_by": "Anna", "created_by_kind": "staff" } ], // gekürzt
"count": 100,
"next_after": "MjAyNi0wOS0xOFQxNDowMjoxMS41MTIzNDV8NDgxMg" } // null = letzte Seite
next_after unverändert als after mitschicken, bis es
null ist. Den Wert nicht selbst bauen oder auslesen.from nach to, limit außerhalb 1–200 oder ein
fremder after-Wert → 400 invalid_request.Das Standardpaket lesen/setzen — dein Sicherheitsnetz für alle Varianten ohne gepflegte Maße. Für Onboarding-Automatisierung, damit du es nicht nur im UI setzen kannst.
// GET → aktuelles Standardpaket
{ "configured": true, "name": "Standardpaket",
"length_cm": 30, "width_cm": 20, "height_cm": 15, "max_weight_kg": 30.0,
"default_weight_g": 500 } // Standardgewicht (nur lesbar; wird in der App gesetzt)
// PUT → Maße setzen (max_weight_kg optional)
{ "length_cm": 30, "width_cm": 20, "height_cm": 15, "max_weight_kg": 31.5 }
Maschinenlesbare Beschreibung (OpenAPI 3.0) aller Endpoints — öffentlich, ohne API-Key. Damit generieren sich Clients selbst und KI-Assistenten raten nicht. Enthält keine Shop-Daten.
Gilt überall, wo der Server die Maße selbst bestimmt (package weggelassen,
package_resolved, Trockenlauf):
karl_dims → Standardpaket. (Ein „gelerntes
Preset" gibt es nicht mehr — abgeschaltet; das Metafeld gewinnt immer gegen das Standardpaket.)karl_dims.dimensions: LxBxH in cm, z. B.
24x18x17. Toleriert x/×/*, Komma und ein „cm"-Suffix.
Nicht parsebare Werte fallen still aufs Standardpaket zurück (kein Fehler).Datenqualitäts-Check für die Paketmaße: Wie viele deiner versandrelevanten Varianten haben ein
gepflegtes karl_dims-Metafeld? Nützlich vor einem Rollout, um Lücken zu finden, statt sie
erst beim Label zu bemerken. Nur lesend, kein Shopify-Write.
{
"variants_total": 320, // alle Varianten im Shop
"shipping_relevant": 300, // davon mit Gewicht > 0 (versandrelevant)
"with_dimensions": 240, // davon mit gepflegtem karl_dims-Metafeld
"without_dimensions": 60,
"coverage_pct": 80.0, // with_dimensions / shipping_relevant
"missing_sample": [ // bis zu 10 Beispiele ohne Maße
{ "variant_id": "44012345678", "product": "Vase", "title": "Groß", "weight_g": 1200 }
],
"weight_suspicious": [ // Verdacht auf kopiertes Gewicht (bis zu 20)
{ "product": "T-Shirt", "weight_g": 200, "variants": 5, "sizes": ["L","M","S","XL","XXL"],
"reason": "identisches Gewicht (200 g) über 5 Größen" }
],
"truncated": false // true = Shop hat mehr Varianten als eine Abfrage abdeckt (4000)
}
weight_suspicious meldet Produkte, bei denen mehrere Größen exakt dasselbe Gewicht tragen
(≥ 3 Varianten über ≥ 2 Größen) — ein typisches Zeichen dafür, dass ein Gewicht kopiert statt
je Größe gepflegt wurde. Nur ein Hinweis, kein Fehler.
Label-Erstellung kostet Geld und löst Fulfillment aus — ein Netzwerk-Retry darf nie doppelt wirken:
Idempotency-Key (z.B. UUID v4).409 request_in_progress, falls er noch läuft — kurz warten, erneut).Alle Fehler haben denselben Aufbau:
{ "error": { "code": "customs_validation_failed",
"message": "HS-Code fehlt für Artikel X.",
"message_en": "Missing HS code for item X.",
"details": { } } }
| HTTP | code | Bedeutung |
|---|---|---|
| 401 | unauthorized | API-Key fehlt, ungültig oder widerrufen |
| 403 | feature_not_enabled | Shop hat kein Enterprise / api_access |
| 404 | order_not_found | Bestellung nicht gefunden |
| 404 | draft_not_found | Entwurf nicht gefunden (oder schon gebucht) |
| 404 | shipment_not_found | Keine Sendung mit dieser Tracking-Nummer in deinem Shop |
| 400 | invalid_package | package (weight_g/length_cm/width_cm/height_cm > 0) fehlt |
| 400 | invalid_recipient | Empfänger fehlt oder ist unvollständig (/api/v1/rates, /api/v1/labels; seit 1.3.20 auch /api/v1/orders/{order_id}/labels, wenn in der Lieferadresse der Bestellung ein Pflichtfeld fehlt) — fehlende Felder in details.missing |
| 400 | invalid_items | items ist keine Liste oder eine Position ist ungültig (z. B. currency fehlt, compliance ist kein Objekt mit dem Text cpsc) |
| 400 | variant_not_found | variant_id gibt es in diesem Shop nicht |
| 400 | idempotency_key_required | Idempotency-Key-Header fehlt (bei /labels) |
| 400 | invalid_idempotency_key | Key zu lang (> 200 Zeichen) |
| 400 | invalid_request | carrier/service fehlt oder ist kein Text, dry_run ist kein Wahrheitswert, oder declared_value/goods_value ist keine Zahl ab 0 (bei goods_value fehlt goods_currency); bei GET /api/v1/shipments: Datum nicht im Format JJJJ-MM-TT, from nach to, limit außerhalb 1–200 oder after nicht aus next_after |
| 400 | service_unavailable | Dienst für diese Sendung nicht buchbar — details.available nennt die buchbaren Codes; der Idempotency-Key bleibt frei |
| 409 | request_in_progress | gleicher Idempotency-Key wird gerade bearbeitet |
| 409 | draft_limit_reached | schon 50 Entwürfe — alte löschen |
| 503 | shopify_unavailable | Produktdaten aus Shopify gerade nicht ladbar (bei variant_id) — nichts gebucht, später erneut senden |
| 422 | idempotency_key_conflict | Idempotency-Key bereits für eine andere Bestellung genutzt — neuen Key verwenden |
| 422 | customs_validation_failed | Zoll-Pflichtdaten (HS-Code, Ursprungsland, Warenbeschreibung, MRN/ABD) fehlen — siehe details; details.code zollangaben_fehlen, wenn nur die Beschreibung fehlt oder der Carrier-Baustein eine Lücke fand (KYTH.Karl setzt keine Ersatzwerte ein) |
| 422 | customs_data_missing | Drittland: per-Artikel Gewicht/Warenwert fehlt in Shopify |
| 422 | packstation_nur_dhl_paket | Empfänger ist eine DHL Packstation oder DHL-Postfiliale in Deutschland — dorthin bucht KYTH.Karl nur DHL Paket (carrier: "dhl"); ein anderer Carrier ist gewählt oder DHL Paket im Shop nicht nutzbar. details: abholstelle, carrier, dhl_eingerichtet, erlaubt. Nichts gebucht, der Idempotency-Key bleibt frei (bei /labels) |
| 422 | bundesstaat_fehlt | Empfänger in den USA, in Kanada, Australien oder Mexiko ohne gültigen Bundesstaat bzw. Provinz (province_code). details: feld, land, grund (fehlt | ungueltig), wert. Nichts gebucht, der Idempotency-Key bleibt frei (bei /labels) |
| 422 | packstation_postnummer_fehlt | DHL Packstation oder DHL-Postfiliale ohne Postnummer (6–10 Ziffern) bei carrier: "dhl" — bis 1.3.19 kam das als 400 carrier_error. details: feld (postnummer). Nichts gebucht, der Idempotency-Key bleibt frei (bei /labels) |
| 422 | eircode_fehlt | FedEx nach Irland ohne Eircode in zip. details: feld (zip), land (IE), grund, wert. Nichts gebucht, der Idempotency-Key bleibt frei (bei /labels) |
| 422 | eircode_ungueltig | FedEx nach Irland, die Eircode hat nicht die Form „D02 X285“ (3 + 4 Zeichen). KYTH.Karl prüft nur die Form, nicht ob FedEx den Bezirk kennt. details wie bei eircode_fehlt, wert ist die Eingabe |
| 422 | partial_shipment_unavailable | Teilweise versendete Bestellung, deren Positionen keine Kennung tragen — KYTH.Karl könnte in Shopify nur die ganze Bestellung als versendet markieren; das Label in der App erstellen (seit 1.3.77). Nichts gebucht, der Idempotency-Key bleibt frei (bei /orders/{order_id}/labels) |
| 429 | rate_limited | Rate-Limit überschritten (120/min pro Shop) |
| 500 | result_persist_failed | Label erstellt, aber Ergebnis nicht gespeichert — Bestellstatus prüfen, NICHT mit gleichem Key wiederholen |
| 502 | carrier_error | Carrier-API-Fehler |
| 503 | service_check_failed | Dienst gerade nicht prüfbar (Carrier nicht erreichbar) — nichts gebucht, später mit demselben Key erneut senden |
KEY="karl_live_xxx"
BASE="https://karl.kyth.systems"
# 1) Offene Bestellungen
curl -s "$BASE/api/v1/orders?status=unfulfilled&limit=50" \
-H "Authorization: Bearer $KEY"
# 2) Carrier-Optionen für Bestellung 1042
curl -s -X POST "$BASE/api/v1/orders/1042/rates" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"package":{"weight_g":2500,"length_cm":30,"width_cm":20,"height_cm":15}}'
# 3) Label erstellen + per Companion drucken — carrier + service UNVERÄNDERT aus Schritt 2
curl -s -X POST "$BASE/api/v1/orders/1042/labels" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: 7d3f-uuid" \
-d '{"carrier":"ups","service":"11","print":"companion",
"package":{"weight_g":2500,"length_cm":30,"width_cm":20,"height_cm":15}}'
/labels mit 422.print: "companion"). Ohne Companion print: "none" nutzen./orders/{order_id}/labels, auch im Trockenlauf) enthält nur noch die offenen Positionen und Mengen — Zollerklärung, Versicherung und Warenwert (bis 1.3.76 die ganze Bestellung, auch schon Versendetes). KYTH.Karl markiert in Shopify nur diese Positionen als versendet. package_used, package_resolved und das Gewicht in /rates rechnen ebenfalls nur mit offenen Mengen; das Gesamtgewicht der Bestellung gilt dann nicht als Quelle. Neu: open_quantity je Position in /api/v1/orders und /api/v1/orders/{order_id}, der Fehler 422 partial_shipment_unavailable (Positionen ohne Kennung) und im Trockenlauf checks.partial_shipment. Für Bestellungen, von denen noch nichts versendet ist, ändert sich nichts; eBay-Bestellungen mit einem aktiven Label von KYTH.Karl enden weiter mit 409. package_resolved.decided_by_line_item zählt die Stelle in line_items der Bestellung (alle Positionen). DHL Kleinpaket hat den Servicecode V62KP (Tarife, Probelauf, Label-Antwort). V62WP (DHL Warenpost) nimmt DHL nicht mehr an — ein Label oder Probelauf mit "service": "V62WP" bucht KYTH.Karl als V62KP, die Antwort nennt V62KP. GET /api/v1/shipments/{tracking_number} bei DHL Paket: KYTH.Karl fragt über den GKP-Login deines Shops (eigenes Kontingent, bis zu 3 Abfragen je Sekunde statt einer alle 5 Sekunden). Sendungen von vor dem Umstieg und Shops, deren Zugang DHL ablehnt, laufen wie bisher über den gemeinsamen Zugang. In events kommen text dann aus der deutschen Anfrage bei DHL und code als DHLs Standard-Ereignis-Code (z. B. ZU, ZN); bei der Zustellung steht im Text, wer das Paket angenommen hat (z. B. „Empfangen von: Nachbar“), nie ein Name. Zugestellte Sendungen fragt KYTH.Karl nicht erneut bei DHL ab. Alle übrigen Felder bleiben unverändert.GET /api/v1/shipments/{tracking_number} bei DHL Paket: Ein Abruf, den der Abstand von 5 Sekunden zwischen zwei DHL-Abfragen ausbremst (mehrere DHL-Sendungen gleichzeitig), kommt mit "stale": true und zählt nicht mehr als Abfrage — der nächste Abruf frischt auf (in 1.3.74 blieb die Sendung danach 15 Minuten stale). Hintergrundwege von KYTH.Karl lassen API-Abrufen bei DHL den Vortritt. Felder und Antwortformat unverändert.GET /api/v1/shipments/{tracking_number} fragt vor der Antwort beim Carrier nach, wenn der gespeicherte Stand älter als 15 Minuten ist (höchstens einmal je Sendung und 15 Minuten, Antwort nach spätestens etwa 20 Sekunden). Neue Felder im Einzelabruf: stale, exception, events. status_updated_at heißt jetzt genau „zuletzt erfolgreich abgefragt“. Die Doku nennt jetzt die Status-Werte, die die API wirklich liefert (transit statt des bisher dokumentierten in_transit; dazu label_created, pickup, unknown, not_supported) — an der Antwort selbst hat sich dabei nichts geändert. Alle übrigen Felder bleiben unverändert./orders/{order_id}/labels, /labels, Kauf eines Entwurfs), in der Wiederholung mit demselben Idempotency-Key und in GET /api/v1/labels/{idempotency_key}: invoice_source (dhl | karl | karl_druck | null) und plt_uploaded (true | false | null) — Herkunft der Handelsrechnung und ob sie elektronisch beim Carrier liegt. Heute nur DHL Express, bei anderen Carriern null. Die Hinweise in warnings (rechnung_zum_druck usw.) bleiben unverändert. Alle übrigen Felder bleiben unverändert.warnings der Label-Antwort bei UPS: rechnung_beilegen — die Sendung überquert eine Zollgrenze, UPS bekommt aber keine elektronische Handelsrechnung (Ziel in der EU); eine Handelsrechnung ausdrucken und dem Paket beilegen. UPS übermittelt über eine Zollgrenze jetzt auch bei Zielen in der EU die Warenbeschreibung der Positionen statt „goods“. Alle übrigen Felder bleiben unverändert./api/v1/labels eine description (auch aus variant_id/sku belegt) — sonst 422 customs_data_missing, im Trockenlauf in errors. Fehlt eine Zollangabe erst beim Kauf, antwortet die API mit 422 customs_validation_failed und details.code zollangaben_fehlen (nichts gebucht, der Idempotency-Key bleibt frei).warnings der Label-Antwort bei DHL Express: rechnung_zum_druck, rechnung_fehlt, rechnung_von_karl (Herkunft der Handelsrechnung) und cpsc_dhl_express_bemerkung_voll. Bis 1.3.22 meldete die API diese Hinweise nicht. Die Warenbeschreibung einer Position geht bei DHL Express vollständig an DHL (bis 1.3.22 nach 70 Zeichen gekürzt); mehr als 512 Zeichen lehnt die API wie bei jedem Carrier mit invalid_items ab, description_truncated kommt bei DHL Express deshalb nicht mehr vor. Alle übrigen Felder bleiben unverändert.address — was aufs Label geht
(Korrekturen, DHL-Abholstelle mit Postnummer, Eircode). Der Trockenlauf hat dazu checks.address und je
Korrektur adresse_korrigiert in warnings. Neue Fehler 422 eircode_fehlt und
422 eircode_ungueltig (FedEx nach Irland). ⚠ Geändert: Eine fehlende Postnummer an einer
DHL Packstation oder Postfiliale antwortet jetzt mit 422 packstation_postnummer_fehlt statt mit
400 carrier_error; der Trockenlauf meldet sie vorher mit demselben Code und Text. ⚠ Geändert:
Fehlt in der Lieferadresse einer Bestellung ein Pflichtfeld (z. B. die PLZ bei Irland ohne Eircode), antwortet
/orders/{order_id}/labels mit 400 invalid_recipient statt mit 400 carrier_error; der
Trockenlauf meldet es vorher in errors. Alle übrigen Felder bleiben unverändert.