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, "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": ""
}
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", "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).
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": "STANDARD",
"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": "STANDARD",
"cost": 6.20, "currency": "EUR", "label_base64": "JVBERi0…",
"printed": true, "fulfilled": true }
Das Label wird automatisch im Shopify-Fulfillment hinterlegt (Tracking an den Kunden). Bei print: "none" druckst du das label_base64-PDF selbst.
Trockenlauf (dry_run): Setz "dry_run": true im Body, 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": "STANDARD", "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": "STANDARD",
"checks": { "package": true, "customs_item_data": true, "customs_validation": true, "service_available": true },
"errors": [], // bei valid:false z.B. {"code":"customs_validation_failed", …}
"package_used": { "weight_g": 700, "length_cm": 24, "width_cm": 18, "height_cm": 17, "source": "variant_metafield" },
"estimated_cost": 6.20, "currency": "EUR" }
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": "STANDARD",
"cost": 6.20, "currency": "EUR", "printed": true, "fulfilled": true }
Unbekannter Key → 404 idempotency_key_not_found.
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 |
| 400 | invalid_package | package (weight_g/length_cm/width_cm/height_cm > 0) fehlt |
| 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 |
| 409 | request_in_progress | gleicher Idempotency-Key wird gerade bearbeitet |
| 422 | idempotency_key_conflict | Idempotency-Key bereits für eine andere Bestellung genutzt — neuen Key verwenden |
| 422 | customs_validation_failed | Zoll-Pflichtdaten (HS-Code/MRN/ABD) fehlen — siehe details |
| 422 | customs_data_missing | Drittland: per-Artikel Gewicht/Warenwert fehlt in Shopify |
| 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 |
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 mit UPS erstellen + per Companion drucken
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":"STANDARD","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.