Zum Inhalt springen

Technik

Wie aus einem angenommenen Angebot genau ein Shopify-Bestellentwurf wird

Von Jahangir Alam · 30. September 2026 · 13 Min. Lesezeit

Zuletzt geprüft
Shopify-API
2026-07
Zielgruppe
Entwickler und Agenturen, die eine Integration vom Angebot zum Bestellentwurf auf Shopify bauen oder prüfen
Umfang
draftOrderCreate idempotent machen, wenn Shopify bei API 2026-07 keinen Idempotenzschlüssel bietet: ein Datensatz pro Umwandlung, gespeicherte Eingabe, ein Datenbank-Claim mit Ablauffenster, eine Erfolgstransaktion mit transaktionaler Outbox, idempotente Verarbeitung von orders/create, Abgleich und warum ein Anlegen mit unbekanntem Ergebnis nie automatisch wiederholt wird

In der Shopify Admin API Version 2026-07 nimmt draftOrderCreate keinen Idempotenzschlüssel entgegen. Manche Mutations tun das – inventoryAdjustQuantities hat in 2026-01 einen optionalen @idempotent-Schlüssel bekommen und ihn in 2026-04 zur Pflicht gemacht, und seine Referenzseite sagt das auch –, aber die Mutation für Bestellentwürfe dokumentiert nichts dergleichen. Rufen Sie sie zweimal mit derselben Eingabe auf, haben Sie zwei Bestellentwürfe, zwei Rechnungen und einen Käufer, der die eine bezahlt und die andere reklamiert.

Eine App, die angenommene Angebote in Bestellentwürfe umwandelt, muss die Umwandlung also selbst idempotent machen. Dieser Beitrag zeigt, wie wir das in QuotWay gebaut haben: ein Datensatz pro Umwandlung, die exakt kalkulierte Eingabe gespeichert und erneut gesendet, ein Claim (Reservierung) in der Datenbank, damit immer nur ein Worker Shopify aufruft, das Ergebnis in einer Transaktion zusammen mit seinem ausgehenden Event festgehalten, ein idempotenter Webhook-Handler für die Bestellung, die zurückkommt, und ein Sweep, der auffängt, was die Webhooks verpassen – der aber bewusst den einen Fall nicht wiederholt, in den er nicht hineinsehen kann.

Die Fehlerfälle rund um die Umwandlung – abweichende Steuer, abweichende Preise, Bestand – behandelt was zwischen Angebot und Umwandlung kaputtgeht. Hier geht es nur um einen davon: sicherzustellen, dass sie genau einmal passiert.

Die Shopify-Fakten wurden am 30. September 2026 bei API-Version 2026-07 gegen Shopifys Referenzseiten geprüft. Der Code ist aus QuotWays Umwandlungsservices vereinfacht: Namen gekürzt, interne Bezeichner entfernt.

Woher Duplikate kommen

Alles, was draftOrderCreate zweimal aufrufen kann, wird es irgendwann tun:

  • Die Job-Queue wiederholt. Ein Anlegen, das aus Sicht des Aufrufers in ein Timeout läuft, kann auf Shopifys Seite gelungen sein. Die Queue sieht einen Fehler und versucht es erneut.
  • Zwei Worker übernehmen denselben Job. At-least-once-Queues stellen einen Job unter Last oder während eines Deployments doppelt zu.
  • Jemand klickt zweimal. „In Bestellentwurf umwandeln“ bei langsamer Verbindung.
  • Automatisierung und ein Mensch handeln gleichzeitig. Eine Regel zur automatischen Umwandlung feuert, während jemand aus dem Team von Hand umwandelt.
  • Der Prozess stirbt zwischen Shopifys Antwort und Ihrem Datenbank-Schreibvorgang. Der Entwurf existiert; Ihr Datensatz sagt, er existiere nicht.

Die ersten vier sind Races zwischen Versuchen. Der fünfte ist etwas anderes: ein Versuch, dessen Ergebnis Sie nicht kennen. Das Design muss beides abdecken, und beides braucht unterschiedliche Antworten.

Ein Datensatz pro Umwandlung

Jede Umwandlung ist eine Zeile, angelegt bevor Shopify aufgerufen wird, eindeutig pro Umwandlungsgruppe – der Menge angenommener Positionen, die zu einem Bestellentwurf werden. Ein vollständig angenommenes Angebot hat eine Gruppe; ein teilweise angenommenes und in zwei Schritten umgewandeltes Angebot hat zwei, und jede wird ihr eigener Bestellentwurf. Die Zeile enthält:

Feld Warum es da ist
Ein Schlüssel aus Angebot, Gruppe und Version Eindeutig; ein zweiter Versuch findet den ersten
Der exakt kalkulierte DraftOrderInput Wird bei jedem Versuch unverändert gesendet
Ein Claim-Zeitstempel Wer gerade Shopify aufruft, und seit wann
Die ID des Shopify-Bestellentwurfs Sobald gesetzt, ist die Umwandlung erledigt
Die ID der Shopify-Bestellung Wird gesetzt, wenn der Käufer bezahlt
Fehlerzähler und letzter Fehler Was ein Mensch sieht, wenn es hängt
Der Lebenslauf eines Umwandlungsdatensatzes Ein Zustandsdiagramm. Eine Umwandlung beginnt als Entwurf, wird zu Kalkuliert, sobald die Eingabe für den Bestellentwurf berechnet und gespeichert ist, und dann reserviert ein Worker sie per Claim. Von Reserviert führt ein erfolgreiches Anlegen zu Umgewandelt, mit gespeicherter ID des Shopify-Bestellentwurfs; ein fehlgeschlagenes Anlegen führt zu Fehlgeschlagen, gibt den Claim frei, und die Queue wiederholt von dort aus. Wenn der Käufer bezahlt, verknüpft der Webhook orders/create die Bestellung, und die Umwandlung ist Bestellung verknüpft. Ein gestrichelter Pfad zeigt den unbekannten Fall: Veraltet ein Claim ohne ID des Bestellentwurfs, kann der Prozess gestorben sein, nachdem Shopify den Entwurf angelegt hat, deshalb markiert der Sweep ihn für einen Menschen, statt ihn zu wiederholen. Entwurf KalkuliertEingabe gespeichert Claim Reserviertein Worker, ein Aufruf angelegt UmgewandeltEntwurfs-ID gespeichert orders/create Bestellung verknüpftKäufer hat bezahlt Fehler FehlgeschlagenClaim freigegeben Queue wiederholt Ergebnis unbekanntalter Claim ohne ID: ein Mensch entscheidet
Jeder Pfad ist idempotent außer dem gestrichelten – dem Fall, in den eine Maschine nicht hineinsehen kann.

Schritt 1: einmal kalkulieren, dieselbe Eingabe senden

Bevor irgendetwas angelegt wird, wird die Umwandlung kalkuliert: Positionen, Preise, Rabatt, Versand, Zahlungsziele und der Käufer (als purchasingEntity bei einem unternehmensbezogenen Angebot) werden zu einem DraftOrderInput zusammengesetzt, mit draftOrderCalculate gegen Shopify geprüft, und der Händler sieht sich jede Abweichung an. Diese Eingabe wird in der Umwandlungszeile gespeichert – das exakte Objekt, nicht die Zutaten, um es neu zu bauen.

Jeder Versuch des Anlegens sendet dieses gespeicherte Objekt. Eine Wiederholung, die die Eingabe neu berechnet, könnte einen Entwurf erzeugen, den der Händler nie freigegeben hat – ein Katalogpreis, der sich über Nacht bewegt hat, ein Steuerbetrag, der sich verschoben hat –, und das ist ein Korrektheitsfehler, auch wenn es kein Duplikat ist.

Schritt 2: abkürzen, dann Claim setzen

Das Anlegen läuft als Job in der Queue. Seine ersten beiden Schritte entscheiden, ob es Shopify überhaupt aufruft:

// 1. Already done? A retry or a double-click lands here.
if (conversion.draftOrderId) return { conversion, isExisting: true };

// 2. Claim it. Exactly one worker's conditional update matches.
const CLAIM_STALE_MS = 2 * 60 * 1000;
const claimedAt = new Date();
const claim = await db.conversion.updateMany({
  where: {
    id: conversion.id,
    draftOrderId: null,
    OR: [
      { claimedAt: null },
      { claimedAt: { lt: new Date(claimedAt.getTime() - CLAIM_STALE_MS) } },
    ],
  },
  data: { claimedAt },
});

if (claim.count === 0) {
  // Lost the race. If the winner already stored the draft, that's our answer;
  // otherwise it's still mid-call - throw, and the queue retries in a moment.
  const fresh = await db.conversion.findUnique({ where: { id: conversion.id } });
  if (fresh?.draftOrderId) return { conversion: fresh, isExisting: true };
  throw new ConversionInProgressError(conversion.id);
}

Zwei Dinge machen das sicher. Die Entscheidung trifft die Datenbank, in einer einzigen bedingten Anweisung, sodass nicht zwei Worker beide „nicht reserviert“ lesen und beide weitermachen können – ein Update trifft, das andere trifft nichts. Und der Claim ist ein Zeitstempel, kein Lock: Stirbt der Worker, der ihn hält, veraltet der Claim nach zwei Minuten, statt die Umwandlung für immer zu blockieren.

Der Verlierer scheitert nicht. Hat der Gewinner die ID des Bestellentwurfs bereits gespeichert, gibt der Verlierer sie als Erfolg zurück; wenn nicht, wirft er einen eigenen „läuft noch“-Fehler, und die Queue versucht es kurz darauf erneut – bis dahin findet sie meist das fertige Ergebnis.

Schritt 3: Shopify aufrufen, das Ergebnis in einer Transaktion festhalten

Der Worker, der den Claim hält, sendet die gespeicherte Eingabe an draftOrderCreate. Liefert Shopify User Errors oder schlägt der Aufruf fehl, hält der Worker den Fehler fest, erhöht den Fehlerzähler, gibt den Claim frei, damit die Wiederholung nicht zwei Minuten wartet, und markiert die Umwandlung als fehlgeschlagen. Die Queue wiederholt einige wenige Male mit Backoff; danach sieht ein Mensch den Fehler.

Liefert Shopify einen Bestellentwurf, passiert alles Weitere in einer einzigen Datenbanktransaktion:

await db.$transaction(async (tx) => {
  await tx.conversion.update({
    where: { id: conversion.id },
    data: { draftOrderId: draft.id, draftOrderName: draft.name, invoiceUrl: draft.invoiceUrl, lastError: null },
  });
  await tx.conversionGroup.update({ where: { id: group.id }, data: { state: "CONVERTED" } });
  await advanceQuoteState(tx, quote);            // partially -> fully converted when the last group lands

  // The outbound event is written in the SAME transaction (a transactional outbox).
  await tx.integrationEvent.create({
    data: { quoteId: quote.id, type: "quote.converted", data: { draftOrderId: draft.id } },
  });
});
await dispatchIntegrationEvents(); // after commit, best effort; a 15-minute cron sends anything still pending

Die Outbox ist so wichtig wie die ID. Das Event quote.converted, das Webhooks und Shopify Flow erreicht, wird in derselben Transaktion wie die Zustandsänderung festgehalten und nach dem Commit versendet. Ein Event, das vor dem Commit rausgeht, könnte eine Umwandlung ankündigen, die zurückgerollt wurde; ein Event, das nach dem Commit aus dem Speicher gesendet wird, geht verloren, wenn der Prozess dazwischen stirbt. In der Transaktion geschrieben, existiert es genau dann, wenn die Umwandlung existiert – das ist die transaktionale Outbox.

Schritt 4: Die Bestellung kommt über einen Webhook zurück

Der Bestellentwurf ist nicht das Ende. Wenn der Käufer bezahlt, legt Shopify eine Bestellung an und sendet orders/create – mindestens einmal, manchmal mehrfach, gelegentlich gar nicht, und ohne Garantie zum Zeitpunkt. Die Bestellung trägt ein Notiz-Attribut, das die Umwandlungsgruppe nennt; darüber findet der Handler zurück.

Drei Ebenen halten das idempotent:

  1. Jede Webhook-Zustellung wird festgehalten, eindeutig pro Shop und Shopifys Webhook-ID. Eine erneute Zustellung ist ein doppelter Insert, und der Unique Constraint macht aus einem gleichzeitigen Paar einen Gewinner und ein „schon gesehen“ – kein Race zwischen Prüfen und Einfügen.
  2. Der Handler selbst ist idempotent. Hat die Umwandlung bereits eine Bestell-ID, ist ein zweiter Empfang ein No-op.
  3. Der Webhook kann schneller sein als unser eigener Schreibvorgang. Ein Käufer, der Sekunden nach Erhalt der Rechnung bezahlt, kann orders/create auslösen, bevor die Umwandlungszeile committet ist. Der Handler wertet eine fehlende Zeile nicht als „nicht unsere“ – das Notiz-Attribut sagt, dass sie es ist –, also plant er eine verzögerte Wiederholung und versucht es erneut.
const groupId = readNoteAttribute(order.note_attributes, "conversion_group_id");
if (!groupId) return { kind: "not-ours" };

const conversion = await db.conversion.findUnique({ where: { groupId } });
if (!conversion) {
  await enqueueDelayedRetry({ order, shopId });   // our own write hasn't landed yet
  return { kind: "retry-later" };
}
if (conversion.orderId) return { kind: "already-linked" }; // Shopify redelivered

await linkOrderAndAdvanceQuote(db, conversion, order);

Schritt 5: der Sweep

Ein geplanter Job läuft alle 15 Minuten und schließt die Lücken, die Events nicht schließen können:

Bedingung Was der Sweep tut
Rechnung vor mehr als 7 Tagen gesendet, Entwurfs-ID gespeichert, keine Bestell-ID Fragt Shopify nach dem Bestellentwurf. Hat er eine Bestellung, hat der Käufer bezahlt und der Webhook wurde verpasst: Die Bestellung wird durch denselben Handler nachgespielt, den der Webhook nutzt, sodass der Nachholpfad der getestete Pfad ist
Vor mehr als 24 Stunden kalkuliert, nie angelegt Meldet es; der Händler entscheidet – es kann einfach noch warten
Kalkuliert, keine Entwurfs-ID, seit mehr als 5 Minuten unverändert Markiert es für einen Menschen. Wiederholt nicht.

Jeder Zweig ist pro Lauf gedeckelt, und die Abfragen bei Shopify laufen nur wenige gleichzeitig, sodass ein Rückstau auf den nächsten Lauf verschoben wird, statt das Zeitbudget des Jobs zu sprengen.

Warum der Sweep das Unbekannte nicht wiederholt

Die letzte Zeile ist die, die Eigenentwicklungen überspringen. Eine Umwandlung, die kalkuliert und reserviert wurde und dann ohne Entwurfs-ID verstummt ist, hat zwei mögliche Geschichten: Der Aufruf hat Shopify nie erreicht, oder Shopify hat den Entwurf angelegt und der Prozess ist gestorben, bevor er ihn festhalten konnte. Aus Sicht der Datenbank sehen beide gleich aus. Ohne Idempotenzschlüssel für draftOrderCreate erzeugt eine Wiederholung der zweiten Geschichte einen zweiten Bestellentwurf – und eine zweite Rechnung, die der Käufer bezahlen kann.

Deshalb macht der Sweep den Fall sichtbar, statt ihn zu wiederholen. Die Lösung ist eine Suche, keine Wiederholung: die Bestellentwürfe des Shops nach der Referenz der Umwandlung durchsuchen (QuotWays umgewandelte Entwürfe tragen quotway und einen Tag mit der Angebotsreferenz sowie das Notiz-Attribut der Umwandlungsgruppe), den Entwurf übernehmen, wenn er existiert, und erst dann einen anlegen, wenn nicht. Dieser Schritt braucht heute einen Menschen, weil er selten ist und ein Fehler Geld kostet; fügt Shopify @idempotent zu draftOrderCreate hinzu, kommt der Schlüssel an den Aufruf und dieser ganze Zweig wird zu einer normalen Wiederholung.

Was passiert, wenn der Prozess bei jedem Schritt stirbt

Der Prozess stirbt … Hinterlassener Zustand Was als Nächstes passiert
Vor dem Claim Kalkuliert, nicht reserviert Die Queue wiederholt; der nächste Versuch setzt den Claim normal
Nach dem Claim, vor dem Aufruf von Shopify Reserviert, keine Entwurfs-ID Der Claim veraltet nach 2 Minuten; die Wiederholung der Queue reserviert neu und ruft Shopify einmal auf
Während des Aufrufs, Shopify hat ihn nie erhalten Reserviert, keine Entwurfs-ID Wie oben – aber nicht unterscheidbar von der nächsten Zeile
Nachdem Shopify den Entwurf angelegt hat, vor der Transaktion Reserviert, keine Entwurfs-ID, Entwurf existiert in Shopify Der Sweep markiert es nach 5 Minuten; ein Mensch sucht den Entwurf über seine Referenz und übernimmt ihn
Nach der Transaktion, vor dem Versand aus der Outbox Umgewandelt, Event ausstehend Der nächste Lauf des Workers versendet es – ein Cron alle 15 Minuten ist das Sicherheitsnetz, ein verpasster Anstoß verzögert das Event also, verliert es aber nie
Während der Bestell-Webhook unterwegs ist Umgewandelt, keine Bestell-ID Shopify stellt erneut zu; oder der 7-Tage-Sweep fragt Shopify und spielt die Bestellung nach

Eine Checkliste für Ihren eigenen Aufbau

  • Ein Umwandlungsdatensatz pro künftigem Bestellentwurf, eindeutig, angelegt vor jedem Aufruf von Shopify.
  • Den exakt kalkulierten DraftOrderInput speichern; erneut senden, nie neu berechnen.
  • Den Claim mit einem bedingten Update und einem Ablauffenster setzen – kein Lock im Speicher.
  • Den Claim bei einem sauberen Fehler freigeben, damit Wiederholungen nicht warten.
  • Entwurfs-ID, Zustandsänderungen und ausgehende Events in einer Transaktion festhalten; Events nach dem Commit versenden.
  • Webhooks über Shopifys Webhook-ID mit einem Unique Constraint deduplizieren und zusätzlich den Handler idempotent machen.
  • „Webhook vor meinem eigenen Schreibvorgang“ als Wiederholung behandeln, nicht als „nicht meiner“.
  • Abgleichen, indem Sie Shopify fragen, und durch denselben Handler nachspielen.
  • Ein Anlegen, dessen Ergebnis Sie nicht sehen können, nie automatisch wiederholen; zuerst den Entwurf suchen.
  • Die Referenzseite von draftOrderCreate bei jedem API-Release prüfen – taucht @idempotent auf, nutzen Sie es.

Dieselben Fragen, an einen Anbieter gerichtet statt an Ihren eigenen Code, stehen in wie Sie eine Shopify-B2B-App bewerten, und die Felder, die ein umgewandelter Bestellentwurf trägt – Tags, die Notiz, die Attribute, an denen ein ERP festmachen kann –, stehen in Integrationsmuster für ERP, CRM und PIM.

Wo das in QuotWay steckt

Alles oben ist QuotWays Umwandlungspfad, wie er heute läuft, in jedem Tarif: Ein angenommenes Angebot, oder jeder angenommene Teil davon, wird zu einem Shopify-Bestellentwurf, und das Event quote.converted feuert einmal pro Bestellentwurf – an Shopify Flow ab Professional und an Webhooks und die API im Tarif Enterprise. Die Tarife stehen auf der Preisseite.

Häufige Fragen

Unterstützt Shopifys draftOrderCreate Idempotenzschlüssel?

Nicht bei API-Version 2026-07: Seine Referenzseite dokumentiert weder eine @idempotent-Direktive noch ein Idempotenz-Argument. Mutations, die das unterstützen, sagen es auf ihrer Referenzseite – inventoryAdjustQuantities etwa hat seinen Schlüssel in 2026-01 optional und in 2026-04 zur Pflicht gemacht. Prüfen Sie die Seite für Bestellentwürfe bei jedem vierteljährlichen Release.

Wie verhindere ich doppelte Bestellentwürfe, wenn ein Job wiederholt wird?

Führen Sie einen Datensatz pro Umwandlung, angelegt vor dem Aufruf, und lassen Sie genau einen Worker ihn per bedingtem Datenbank-Update reservieren. Eine Wiederholung, die eine gespeicherte Entwurfs-ID findet, gibt sie zurück; eine Wiederholung, die den Claim verliert, wartet und versucht es erneut. Senden Sie die exakt kalkulierte Eingabe erneut, damit eine Wiederholung auch keinen anderen Entwurf erzeugen kann.

Was, wenn mein Prozess abstürzt, nachdem Shopify den Bestellentwurf angelegt hat?

Ihr Datensatz zeigt einen Claim, aber keine Entwurfs-ID, und Sie können nicht erkennen, ob Shopify den Aufruf erhalten hat. Wiederholen Sie nicht blind – das ist der eine Weg zu einem echten Duplikat. Durchsuchen Sie die Bestellentwürfe des Shops nach der Referenz, die Sie angehängt haben (ein Tag oder ein Notiz-Attribut), übernehmen Sie den Entwurf, wenn er da ist, und legen Sie nur dann einen an, wenn nicht.

Wie gehe ich mit doppelten orders/create-Webhooks um?

Halten Sie jede Zustellung mit Shopifys Webhook-ID unter einem Unique Constraint fest, sodass eine erneute Zustellung als Duplikat abgewiesen wird, selbst wenn zwei Kopien gleichzeitig ankommen. Machen Sie dann den Handler selbst idempotent: Ist die Bestellung bereits verknüpft, tun Sie nichts.

Was, wenn der orders/create-Webhook vor meinem eigenen Datenbank-Schreibvorgang ankommt?

Das kann passieren, wenn ein Käufer Sekunden nach der Rechnung bezahlt. Trägt die Bestellung Ihre Referenz, Ihr Datensatz existiert aber noch nicht, planen Sie eine verzögerte Wiederholung, statt sie als „nicht unsere“ zu verwerfen.

Was, wenn der orders/create-Webhook nie ankommt?

Webhooks sind nicht garantiert. Gleichen Sie nach Zeitplan ab: Fragen Sie bei Entwürfen, deren Rechnung vor einer Weile rausging und zu denen keine Bestellung festgehalten ist, Shopify, ob der Entwurf eine Bestellung hat, und wenn ja, geben Sie sie durch denselben Handler, den der Webhook nutzt.

Quellen

Verwandte Artikel

So funktioniert QuotWay in Ihrem Shop.

Wir möchten Analyse-Cookies setzen, um zu verstehen, wie die Website genutzt wird. Sie sind nicht erforderlich – eine Ablehnung ändert nichts an der Funktion der Website, und Sie können Ihre Entscheidung jederzeit ändern auf unserer Datenschutzseite.