Zum Inhalt springen

Shopify-REST-API-Adapter

Kompatibel zur Admin API

Zuletzt aktualisiert: 5. September 2026

Eine Drop-in-kompatible REST API für STOAR, die die Shopify Admin REST API 2024-01 nachbildet — gleiche URL-Pfade (/admin/api/2024-01/...), gleiche flache Query-Parameter-Syntax, gleiche JSON-Envelope-Struktur ({ "order": {…} }, { "orders": [...] }), gleicher Auth-Header, gleiche Link: <…>; rel="next"-Cursor-Pagination. Bestehende Shopify-Client-Bibliotheken (shopify_api Ruby gem, @shopify/shopify-api-node, ShopifySharp, python-shopify-api) sprechen ohne Änderung mit STOAR.

Der Adapter ist die dritte Implementierung des erweiterbaren API-Adapter-Frameworks von STOAR (nach Magento und WooCommerce). Er zeigt eine nochmals andere Envelope-Ausprägung — schlüsselbasierte Wrapper auf oberster Ebene plus Cursor-Pagination — direkt neben Magento und WC auf derselben herstellerneutralen Datenschicht.


Inhaltsverzeichnis #


Schnellstart #

TOKEN="4|abcdef…"     # Sanctum personal-access token with shopify:admin ability

# Discover shop currency / locale (real Shopify clients call this first)
curl -H "X-Shopify-Access-Token: $TOKEN" \
     "https://app.stoar.ai/admin/api/2024-01/shop.json" | jq '.shop | {name, currency}'

# List the most recent 5 orders
curl -H "X-Shopify-Access-Token: $TOKEN" \
     "https://app.stoar.ai/admin/api/2024-01/orders.json?limit=5" | jq '.orders[] | {id, financial_status, total_price}'

# Fetch a single order
curl -H "X-Shopify-Access-Token: $TOKEN" \
     "https://app.stoar.ai/admin/api/2024-01/orders/10126.json"

# Cancel a pending order
curl -X POST -H "X-Shopify-Access-Token: $TOKEN" \
     "https://app.stoar.ai/admin/api/2024-01/orders/123/cancel.json" \
     -H "Content-Type: application/json" -d '{"reason":"customer"}'

Bearer-Auth funktioniert ebenfalls — -H "Authorization: Bearer $TOKEN" ist mit dem Shopify-eigenen Header austauschbar.


Authentifizierung #

Shopifys kanonischer Auth-Header lautet X-Shopify-Access-Token: <token>. Der STOAR-Adapter akzeptiert ihn und zusätzlich einen Bearer-Fallback:

Methode Format Einsatzzweck
X-Shopify-Access-Token X-Shopify-Access-Token: <sanctum-token> Standard für alle Shopify-Client-Bibliotheken
Bearer (STOAR-Erweiterung) Authorization: Bearer <sanctum-token> Natives Sanctum — austauschbar mit der Auth des Magento-Adapters

Tokens werden auf der STOAR-Seite /manager/api-tokens ausgestellt, programmatisch über AdminUser::createToken('label', ['shopify:admin']) oder — für STOAR-interne Abläufe — über Magentos Endpunkt /rest/V1/integration/admin/token, wobei demselben Datensatz anschließend die Ability shopify:admin erteilt wird.

Abbildung auf Sanctum. Die AccessTokenMiddleware läuft vor auth:sanctum und überführt X-Shopify-Access-Token in einen Authorization: Bearer …-Header. Aus Sicht von STOAR ist jede Shopify-Anfrage eine ganz normale Sanctum-authentifizierte Anfrage, deren Token-Zeile die Ability shopify:admin trägt. Ein einzelnes Token kann die Abilities aller Adapter gleichzeitig führen (shopify:admin + magento:admin + woocommerce:admin + bigcommerce:admin).

OAuth und HMAC-Verifizierung (wie sie echte Shopify-Apps nutzen) werden nicht unterstützt.


Endpunkte #

Alle Pfade sind relativ zu /admin/api/2024-01 (wortgleicher Shopify-API-Namespace inklusive der Version 2024-01).

GET /admin/api/2024-01/shop.json

Liefert Shop-Metadaten, die aus STOARs Setting-Tabelle zusammengestellt werden. Echte Shopify-Clients rufen diesen Endpunkt zuerst auf, um Währung und Locale des Shops zu ermitteln, bevor sie etwas anderes tun.

Antwort:

{
  "shop": {
    "id":               1,
    "name":             "STOAR",
    "domain":           "app.stoar.ai",
    "myshopify_domain": "app.stoar.ai",
    "email":            "[email protected]",
    "currency":         "EUR",
    "country_code":     "DE",
    "country_name":     "Germany",
    "primary_locale":   "en",
    "iana_timezone":    "UTC",
    "weight_unit":      "kg",
    "plan_name":        "stoar",
    "plan_display_name":"STOAR",
    "shop_owner":       "STOAR",
    "money_format":     "€ {{amount}}",
    "checkout_api_supported": true,
    "has_storefront":   true,
    "..."
  }
}

GET /admin/api/2024-01/orders.json

Listet Bestellungen auf.

Authorization: shopify:admin. Query: siehe Query-Parameter.

Antwort (200):

HTTP/1.1 200 OK
Link: <…?page_info=eyJwIjoyLCJzIjo1LCJmIjoiZjE…&limit=5>; rel="next"
Content-Type: application/json

{
  "orders": [
    { /* order object — see Response shape */ }
  ]
}

Der Link-Header steuert die Pagination (kein ?page=-Zähler). Siehe Cursor-Pagination.


GET /admin/api/2024-01/orders/{id}.json

Einzelne Bestellung anhand der numerischen ID.

Antwort: Envelope { "order": {...} }. Siehe Antwortstruktur.

Fehler:

{ "errors": "Not Found" }

POST /admin/api/2024-01/orders/{id}/cancel.json

Storniert eine offene oder bezahlte Bestellung.

Authorization: shopify:admin. Body (optional):

{ "reason": "customer" }

Von Shopify akzeptierte Gründe: customer, inventory, fraud, declined, other. STOAR schreibt den Wert in den entstehenden OrderStatusLog, wertet ihn darüber hinaus aber nicht aus.

Antwort (200): die stornierte Bestellung im Standard-Envelope.

Fehler:

  • 404 — unbekannte ID
  • 422 — Bestellung befindet sich in einem Status, der keine Stornierung erlaubt (z. B. delivered, refunded)

GET /admin/api/2024-01/products.json

Listet Produkte auf. Gleicher Envelope und gleiche Link-Header-Pagination wie bei Bestellungen.


GET /admin/api/2024-01/products/{id}.json

Einzelnes Produkt. Varianten sind eingebettet (nicht nur IDs, sondern die vollständigen Variantenobjekte) — exakt wie im Shopify-Kontrakt.

Antwort: { "product": {...} }. Siehe Antwortstruktur.


Query-Parameter #

Shopifys REST-Listen-Endpunkte arbeiten mit flachen Query-Parametern — deutlich einfacher als Magentos verschachteltes searchCriteria[…]. Der Parser liegt in app/Api/Adapters/Shopify/Search/.

Gemeinsame Parameter (Bestellungen + Produkte)

Parameter Standard Zweck
limit 50 Einträge pro Seite (max. 250)
page_info undurchsichtiger Base64-Cursor — überschreibt jeden anderen Filter (siehe Cursor-Pagination)
since_id nur Einträge mit id > since_id (Alternative zum Cursor)
ids ID-Liste als CSV (?ids=1,2,3)
created_at_min / created_at_max ISO 8601
updated_at_min / updated_at_max ISO 8601
order created_at desc <field> <direction>. Richtung: asc / desc

Nur Bestellungen

Parameter Beispiel Wirkung
status open, closed, cancelled, any grober Lebenszyklus — wird auf mehrere Stoar-Status übersetzt
financial_status paid, pending, refunded, voided, authorized wird auf einen einzelnen Stoar-Status übersetzt
fulfillment_status fulfilled, partial, unfulfilled, any wird auf einen Stoar-Status übersetzt

Nur Produkte

Parameter Beispiel Wirkung
status active, archived, draft übersetzt auf Stoar active / inactive
title widget Teilstring (LIKE) auf dem Produkt-name
handle widget-pro exakte Übereinstimmung mit slug
vendor / product_type akzeptiert, aber ignoriert — Stoar kennt keine Entsprechung

Unbekannte Parameter werden ignoriert. Filterwerte ohne sinnvolle Entsprechung (z. B. ?financial_status=foo) werden stillschweigend verworfen.


Cursor-Pagination #

Shopify verwendet undurchsichtige Base64-Cursor statt Seitenzähler. STOAR bildet den Kontrakt nach:

  1. Der Client sendet ?limit=N (kein page-Parameter).
  2. Der Server liefert die ersten N Einträge plus einen Link:-Header:
    Link: <…/orders.json?page_info=eyJwIjoyLCJzIjo1LCJmIjoiYWJjMTIzIn0&limit=5>; rel="next"
  3. Der Client folgt der rel="next"-URL wortgetreu — er baut sie niemals selbst zusammen.
  4. Beim Folgen des Links dekodiert der Server page_info zu (page=2, page_size=5, filter_hash=abc123) und liefert die nächste Seite.

Der filter_hash ist ein stabiler MD5 über das ursprüngliche Filterset. Versucht ein Client, einen Cursor auf einer anderen Ergebnismenge wiederzuverwenden (etwa indem er ?status=open auf ?status=closed ändert), passt der Hash des Cursors nicht mehr und STOAR fällt auf eine frische Seite 1 zurück. Das entspricht dem Verhalten des echten Shopify, bei dem Parameteränderungen Cursor invalidieren.

Der Link-Header kann sowohl rel="next" als auch rel="previous" enthalten:

Link: <…?page_info=PREV>; rel="previous", <…?page_info=NEXT>; rel="next"

Feld-Mapping (Shopify ↔ STOAR) #

Bestellung

Shopify-Feld STOAR-Quelle Hinweise
id id numerisch
admin_graphql_api_id id (formatiert) gid://shopify/Order/{id}
name id #{id} (Shopify zeigt bei Bestellungen #1001)
number id numerisch
order_number id + 1000 Shopify beginnt standardmäßig bei 1000
email, contact_email customer_email
phone immer null (auf der Stoar-Order nicht gespeichert)
currency, presentment_currency currency (in Großbuchstaben) eurEUR
financial_status status (übersetzt) siehe Statusübersetzung
fulfillment_status status (übersetzt) null, partial oder fulfilled
status (Lebenszyklus) status (übersetzt) open, closed, cancelled
total_price, current_total_price total_amount String mit 2 Nachkommastellen
total_price_set money_set-Wrapper um total_amount {shop_money, presentment_money}
subtotal_price, total_line_items_price subtotal String mit 2 Nachkommastellen + money_set
total_tax, current_total_tax tax_amount String mit 2 Nachkommastellen + money_set
total_shipping_price_set money_set-Wrapper um shipping_amount
total_discounts discount_amount String mit 2 Nachkommastellen
total_outstanding total_amount - sum(succeeded payments) offener Betrag
total_paid nicht auf oberster Ebene enthalten (Shopify berechnet es aus Transaktionen) verfügbar über current_total_price - total_outstanding
gateway, payment_gateway_names[] erste nicht archivierte OrderPayment.gateway
created_at, updated_at, processed_at created_at, updated_at ISO 8601
cancelled_at updated_at, wenn status=cancelled sonst null
closed_at updated_at, wenn status=delivered/refunded sonst null
cancel_reason 'other', wenn storniert sonst null
customer eingebettetes Customer-Modell null bei Gästen
billing_address JSON-Spalte billing_info flache Shopify-Struktur
shipping_address JSON-Spalte shipping_info flache Shopify-Struktur
line_items[] Order.items (mit Variante + Produkt) siehe unten
discount_codes[] abgeleitet aus coupon_code + discount_amount leer, wenn kein Gutschein
tax_lines[] ein Element, wenn tax_amount > 0 mit Satz 0, da Stoar keinen Satz auf Bestellebene speichert
shipping_lines[] abgeleitet aus shipping_method + shipping_amount leer, wenn kein Versand
refunds[] ein synthetischer Eintrag, wenn refunded_amount > 0
token lookup_token das Magic-Link-Token der Bestellung — geheim halten
order_status_url /checkout/success?order_id=…&token=… Clients nutzen dies für die Sendungsverfolgung im Self-Service
tags "" nicht modelliert

Bestellposition (line_item)

Shopify-Feld STOAR-Quelle
id OrderItem.id
variant_id OrderItem.variant_id
product_id OrderItem.product_id
title OrderItem.name
variant_title Variant.name (sofern nicht „Default“)
name "{title} - {variant_title}", wenn die Variante einen Namen hat
sku zuerst Varianten-SKU, ersatzweise Produkt-SKU
quantity OrderItem.quantity
price OrderItem.price (String mit 2 Nachkommastellen)
price_set money_set-Wrapper
tax_lines[] ein Element, wenn tax_amount > 0
vendor, properties[] immer leer / null
fulfillment_service 'manual'
fulfillment_status null
gift_card, requires_shipping, taxable sinnvolle Standardwerte

Produkt

Shopify-Feld STOAR-Quelle Hinweise
id id
admin_graphql_api_id gid://shopify/Product/{id}
title name
handle slug
body_html description unverändert durchgereicht (HTML oder Klartext)
status status (übersetzt) activeactive; inactivearchived
vendor, product_type immer "" in Stoar nicht modelliert
published_at created_at, wenn aktiv null bei inaktiv/archiviert
tags "" nicht modelliert
variants[] eingebettet — vollständige Variantenobjekte immer mindestens eine (Default Title, wenn Stoar keine hat)
options[] abgeleitet aus den JSON-Schlüsseln der Varianten-attributes (max. 3) jede Option hat name, position, values[]
images[] image_path + Array gallery_paths position beginnt bei 1
image erstes Bild (oder null)
template_suffix, published_scope null, 'web' fest verdrahtet

Produktvariante

Shopify-Feld STOAR-Quelle
id Variant.id
product_id Variant.product_id
title Variant.name ("Default Title", wenn der Name "Default" lautet)
option1, option2, option3 Werte aus dem JSON attributes in stabiler Reihenfolge (max. 3 Achsen)
price Variant.price (String mit 2 Nachkommastellen)
sku Variant.sku
inventory_quantity Variant.stock
inventory_management 'shopify' (immer)
inventory_policy 'deny' (immer)
weight Variant.weight
weight_unit 'kg'
grams weight * 1000 (gerundet)
requires_shipping, taxable immer true
compare_at_price immer null
barcode immer null

Statusübersetzung #

Shopify verteilt den Bestellzustand auf drei Felder. Stoar führt sie in einem zusammen. Der Übersetzer beherrscht beide Richtungen.

Stoar → Shopify (Ausgabe)

Stoar status financial_status fulfillment_status status (Lebenszyklus)
pending pending null open
paid paid null open
processing paid partial open
shipped paid fulfilled open
delivered paid fulfilled closed
cancelled voided null cancelled
refunded refunded null closed

Shopify-Filter → Stoar (Parsing)

Shopify-Eingabe Stoar-Entsprechung
?status=open pending, paid, processing, shipped
?status=closed delivered, refunded
?status=cancelled cancelled
?status=any (kein Filter)
?financial_status=pending pending
?financial_status=paid / =authorized paid
?financial_status=voided cancelled
?financial_status=refunded / =partially_refunded refunded
?fulfillment_status=fulfilled shipped
?fulfillment_status=partial processing
?fulfillment_status=unfulfilled paid

Produktstatus

Shopify Stoar
active active
archived, draft inactive

Stoar kennt kein draft — archivierte und Draft-Produkte aus Shopify werden beide auf Stoar inactive abgebildet. Die Ausgabe verwendet archived.


Antwortstruktur #

Bestellung — kommentiertes Beispiel

{
  "order": {
    "id":                   12345,
    "admin_graphql_api_id": "gid://shopify/Order/12345",
    "name":                 "#12345",
    "number":               12345,
    "order_number":         13345,
    "token":                "abc123…",
    "email":                "[email protected]",
    "contact_email":        "[email protected]",
    "currency":             "EUR",
    "presentment_currency": "EUR",
    "financial_status":     "paid",
    "fulfillment_status":   null,
    "status":               "open",
    "gateway":              "stripe",
    "payment_gateway_names":["stripe"],
    "total_price":          "115.00",
    "total_price_set":      {
      "shop_money":        { "amount": "115.00", "currency_code": "EUR" },
      "presentment_money": { "amount": "115.00", "currency_code": "EUR" }
    },
    "subtotal_price":       "100.00",
    "total_tax":            "10.00",
    "total_shipping_price_set": {
      "shop_money":        { "amount": "5.00", "currency_code": "EUR" },
      "presentment_money": { "amount": "5.00", "currency_code": "EUR" }
    },
    "total_discounts":      "0.00",
    "total_outstanding":    "0.00",
    "total_tip_received":   "0.00",
    "created_at":           "2026-04-10T09:00:00+00:00",
    "updated_at":           "2026-04-10T09:30:00+00:00",
    "processed_at":         "2026-04-10T09:00:00+00:00",
    "cancelled_at":         null,
    "closed_at":            null,
    "cancel_reason":        null,
    "customer":             {
      "id":                 45,
      "email":              "[email protected]",
      "first_name":         "Jane",
      "last_name":          "Doe",
      "verified_email":     true,
      "state":              "enabled",
      "currency":           "EUR"
    },
    "billing_address":      {
      "first_name":         "Jane",
      "last_name":          "Doe",
      "name":               "Jane Doe",
      "address1":           "1 Test St",
      "address2":           null,
      "city":               "Berlin",
      "province":           null,
      "country":            null,
      "country_code":       "DE",
      "zip":                "10115",
      "phone":              null
    },
    "shipping_address":     { /* same shape as billing */ },
    "line_items": [
      {
        "id":           987,
        "variant_id":   12,
        "product_id":   456,
        "title":        "Widget",
        "variant_title":"Red",
        "name":         "Widget - Red",
        "sku":          "WID-RED",
        "quantity":     2,
        "price":        "50.00",
        "price_set":    {"shop_money": {"amount": "50.00", "currency_code": "EUR"}, "presentment_money": {"amount": "50.00", "currency_code": "EUR"}},
        "fulfillable_quantity": 2,
        "fulfillment_service":  "manual",
        "fulfillment_status":   null,
        "requires_shipping":    true,
        "taxable":              true,
        "tax_lines":            []
      }
    ],
    "shipping_lines": [
      {
        "id":           0,
        "title":        "Standard",
        "code":         "flat_rate",
        "source":       "shopify",
        "price":        "5.00",
        "price_set":    {"shop_money": {"amount": "5.00", "currency_code": "EUR"}, "presentment_money": {"amount": "5.00", "currency_code": "EUR"}},
        "tax_lines":    [],
        "discount_allocations": []
      }
    ],
    "tax_lines":            [],
    "discount_codes":       [],
    "discount_applications":[],
    "fulfillments":         [],
    "refunds":              []
  }
}

Produkt — gekürztes Beispiel

{
  "product": {
    "id":                  789,
    "admin_graphql_api_id":"gid://shopify/Product/789",
    "title":               "Widget",
    "body_html":           "<p>A great widget</p>",
    "vendor":              "",
    "product_type":        "",
    "handle":              "widget",
    "status":              "active",
    "published_at":        "2026-04-01T10:00:00+00:00",
    "published_scope":     "web",
    "tags":                "",
    "variants": [
      {
        "id":                 12,
        "admin_graphql_api_id":"gid://shopify/ProductVariant/12",
        "product_id":         789,
        "title":              "Red",
        "price":              "99.99",
        "sku":                "WID-RED",
        "position":           1,
        "inventory_policy":   "deny",
        "compare_at_price":   null,
        "fulfillment_service":"manual",
        "inventory_management":"shopify",
        "option1":            "Red",
        "option2":            null,
        "option3":            null,
        "weight":             0.5,
        "weight_unit":        "kg",
        "grams":              500,
        "inventory_quantity": 42
      }
    ],
    "options": [
      { "id": 0, "product_id": 789, "name": "Color", "position": 1, "values": ["Red", "Blue"] }
    ],
    "images": [
      {
        "id":         0,
        "admin_graphql_api_id": "gid://shopify/ProductImage/0",
        "product_id": 789,
        "position":   1,
        "alt":        null,
        "src":        "products/widget.webp",
        "variant_ids":[]
      }
    ],
    "image": { /* same shape as images[0] */ }
  }
}

Praxisbeispiel #

GET /admin/api/2024-01/orders/10126.json gegen die Produktion (dieselbe Bestellung wie in Magento und WooCommerce, live in die Shopify-Struktur überführt). Personenbezogene Daten anonymisiert. Die Antwort passt ohne Änderung in Shopifys shopify_api Ruby gem und @shopify/shopify-api-node.

{
  "order": {
    "id":                   10126,
    "admin_graphql_api_id": "gid://shopify/Order/10126",
    "name":                 "#10126",
    "number":               10126,
    "order_number":         11126,
    "token":                "REDACTED-FOR-DOCS",
    "email":                "[email protected]",
    "contact_email":        "[email protected]",
    "currency":             "USD",
    "presentment_currency": "USD",
    "financial_status":     "paid",
    "fulfillment_status":   null,
    "status":               "open",
    "gateway":              "payid",
    "payment_gateway_names":["payid"],
    "total_price":          "936.98",
    "subtotal_price":       "936.98",
    "total_tax":            "0.00",
    "total_outstanding":    "936.98",
    "total_price_set":      { "shop_money": { "amount": "936.98", "currency_code": "USD" }, "presentment_money": { "amount": "936.98", "currency_code": "USD" } },
    "created_at":           "2025-06-03T04:56:43+00:00",
    "updated_at":           "2025-06-03T04:56:43+00:00",
    "processed_at":         "2025-06-03T04:56:43+00:00",
    "cancelled_at":         null,
    "closed_at":            null,
    "billing_address": {
      "first_name":   "Jane",
      "last_name":    "Doe",
      "name":         "Jane Doe",
      "address1":     "1 Example Street",
      "address2":     null,
      "city":         "Phoenix",
      "province":     "AZ",
      "country":      null,
      "country_code": "US",
      "zip":          "85001",
      "phone":        "+1-555-0100"
    },
    "shipping_address":     { /* same shape */ },
    "customer":             {
      "id":             5794,
      "email":          "[email protected]",
      "first_name":     "Jane",
      "last_name":      "Doe",
      "state":          "enabled",
      "verified_email": true,
      "currency":       "USD"
    },
    "line_items": [
      {
        "id":           30219,
        "variant_id":   95589,
        "product_id":   112238,
        "title":        "Reloop Terminal Mix 8",
        "variant_title":null,
        "name":         "Reloop Terminal Mix 8",
        "sku":          "RELOOP_TERMINALMIX8_025-DEF",
        "quantity":     3,
        "price":        "299.00",
        "fulfillable_quantity": 3,
        "fulfillment_service":  "manual",
        "fulfillment_status":   null,
        "requires_shipping":    true,
        "taxable":              false,
        "tax_lines":            []
      },
      {
        "id":           30220,
        "variant_id":   33857,
        "product_id":   51706,
        "title":        "Premium Skateboard Socks",
        "variant_title":null,
        "name":         "Premium Skateboard Socks",
        "sku":          "SK8-SOCK-027-DEF",
        "quantity":     2,
        "price":        "19.99",
        "fulfillable_quantity": 2,
        "fulfillment_service":  "manual",
        "fulfillment_status":   null,
        "requires_shipping":    true,
        "taxable":              false,
        "tax_lines":            []
      }
    ],
    "shipping_lines": [{
      "id":     0,
      "title":  "Free Shipping",
      "code":   "flat_rate",
      "source": "shopify",
      "price":  "0.00"
    }],
    "tax_lines":            [],
    "discount_codes":       [],
    "discount_applications":[],
    "fulfillments":         [],
    "refunds":              []
  }
}

Was Integratoren wissen sollten

Feld Beobachtet Warum
total_outstanding "936.98" bei einer bezahlten Bestellung Berechnet aus dem OrderPayment-Audit-Log; Altbestellungen aus der Zeit vor dem Log weisen ihren vollen Betrag als offen aus.
gateway "payid" Das ist Stoars Gateway-Key — kein Shopify-kanonischer Name wie shopify_payments.
customer_locale, device_id, app_id immer null Auf der Stoar-Order nicht modelliert.
tax_lines [], selbst wenn total_tax > 0 Stoar erfasst Steuern nur auf Bestellebene, nicht je Steuergebiet; bei Bedarf würden wir eine einzelne tax_line synthetisieren.
fulfillments, discount_applications immer [] Keine Fulfilment-Verfolgung; Gutscheine werden nur als flache discount_codes abgebildet.
tags immer "" In Stoar gibt es kein Tag-/Label-System.
token Magic-Link-Token je Bestellung Niemals in Client-Code oder Logs preisgeben — der Besitz des Tokens genügt, um die Bestellung ohne Auth einzusehen.
order_number id + 1000 Kosmetisch — Shopify startet in allen Shops standardmäßig bei 1000.

Fehlerstruktur #

Shopify verwendet einen lockeren Envelope:

{ "errors": "Not Found" }                       // string
{ "errors": { "title": ["can't be blank"] } }   // map (validation)
HTTP Auslöser Body
401 fehlendes oder ungültiges Token { "errors": "[API] Invalid API key or access token …" }
403 Token mit falscher Ability { "errors": "Forbidden" }
404 unbekannte Ressourcen-ID { "errors": "Not Found" }
422 Vorbedingung verletzt (nicht stornierbare Stornierung usw.) { "errors": "Order N cannot be cancelled in status 'delivered'." }
429 Rate Limit überschritten { "errors": "Exceeded 2 calls per second for api client. …" } + Retry-After: 2
500 unerwarteter Serverfehler { "errors": "Internal Server Error" }

Validierungsfehler (nur cancel.json akzeptiert einen Body) verwenden die feldbasierte Map-Struktur.


Rate Limiting #

Dieselbe an das Sanctum-Token gebundene api-rest-Drosselung, die Magento und WooCommerce schützt, schützt auch die Shopify-Routen. Beides konfigurierst du global unter /manager/api-settings. Beim Auslösen nutzt die Antwort Shopifys Fehler-Envelope:

HTTP/1.1 429 Too Many Requests
Retry-After: 2
Content-Type: application/json

{ "errors": "Exceeded 2 calls per second for api client. Reduce request rates to resume uninterrupted service." }

Der Wert des Retry-After-Headers entspricht dem von Shopify dokumentierten Verhalten. Das echte Shopify verwendet einen Leaky-Bucket-Algorithmus; STOAR nutzt ein Sliding-Window-Minutenfenster — nah genug für die Kompatibilität mit Client-Bibliotheken.

Ein einzelnes Sanctum-Token, das über alle vier Adapter (Magento + WC + Shopify + BC) verwendet wird, teilt sich einen Bucket — die vollständige Beschreibung der Konfigurationsoberfläche findest du im Abschnitt Rate Limiting der Magento-Dokumentation.


Siehe auch #

  • app/Api/Adapters/Shopify/Support/StatusTranslator.php — bidirektionales Vokabular-Mapping
  • app/Api/Adapters/Shopify/Support/PageInfoCursor.php — Kodierung des undurchsichtigen Base64-Cursors
  • Shopify-Admin-REST-API-Referenz — die Upstream-Spezifikation, die dieser Adapter nachbildet