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
- Authentifizierung
- Endpunkte
- Query-Parameter
- Cursor-Pagination
- Feld-Mapping (Shopify ↔ STOAR)
- Statusübersetzung
- Antwortstruktur
- Praxisbeispiel
- Fehlerstruktur
- Rate Limiting
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 ID422— 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:
- Der Client sendet
?limit=N(keinpage-Parameter). - Der Server liefert die ersten N Einträge plus einen
Link:-Header:Link: <…/orders.json?page_info=eyJwIjoyLCJzIjo1LCJmIjoiYWJjMTIzIn0&limit=5>; rel="next" - Der Client folgt der
rel="next"-URL wortgetreu — er baut sie niemals selbst zusammen. - Beim Folgen des Links dekodiert der Server
page_infozu(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) |
eur → EUR |
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) |
active ↔ active; inactive ↔ archived |
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-Mappingapp/Api/Adapters/Shopify/Support/PageInfoCursor.php— Kodierung des undurchsichtigen Base64-Cursors- Shopify-Admin-REST-API-Referenz — die Upstream-Spezifikation, die dieser Adapter nachbildet