Vielleicht bin ich zu dumm dazu, aber nun stehe ich schon das zweite Mal vor dem Problem, wie ich von extern die Daten von Shopify abzapfen kann, bzw bei einer Bestellung im Shop die Daten der Bestellung (Produkte, Kunde) an eine externe Datenbank weiterleiten kann.

Ich bin zwar mal über "Shopify Flow" gestolpert, aber dies gibt es wohl nur in Verbindung mit "Shopify Plus". Ansonsten müsste es irgendwie eine Möglichkeit mit Hilfe der "Apps" geben (heutzutage sind drei Zeilen Code ja eine Ähpp)

Mal schnell zusammengefasst, was Shopify alles an Schnittstellen bietet:

  • Öffentliche Apps (das, was man so in Shopifys "App Store" findet)
  • Benutzerdefinierte Apps (Da bin ich noch nicht so wirklich hinter Sinn, Zweck und HowTo gekommen)
  • Private Apps (damit kann man schön was für sich selbst basteln, darauf gehe ich noch ein)
  • App-Entwürfe (keine Ahnung. Gibt's halt...steht in der Doku)
  • Webhooks (was ich suche, aber in den Dokus schwer zu finden: Shopify sendet aktiv bei bestimmten Ereignissen Daten an ein externes Skript)

Ãœber Payments-API, Partner-API, Messaging-API, Storefront-API, AJAX-API, Liquid & Co will ich gar nicht erst reden...

Im Folgenden will ich mal schauen, was man mit ner "Privaten App" (ich nenne lieber "Simplen API-Zugriff") und den Webhooks so anstellen kann.

Private App (einfacher API-Zugriff)

Die "Private App" hat weder mit der öffentlichen oder benutzerdefinierten App viel gemein, noch ist es eigentlich eine "Application". Aber "App" klingt halt hip.

An sich ist die "Private App" einfach eine API, um von außen auf seine Shop-Daten zugreifen zu können, ohne sich damit herumschlagen zu müssen, wie man eine Shopify-App erstellt und wie man jene in den App-Store von Shopify rein bekommt.
Ich werde im Folgenden nun auch nur noch von "der API" schreiben.

Um so einen API-Zugriff erhalten zu können, muss der Shop-Besitzer zuerst an dieser Stelle die "Privaten Apps" (jetzt habe ich es doch wieder gesagt!) freischalten.
 

Vor 2022 sah alles, was nun kommt, etwas anders aus und man benötigte einen Schlüssel und ein Passwort für den API-Zugriff.
Nun wird ein Token generiert.

Sollte sich jemand für die alte Variante mit Schlüssel und Passwort interessieren, lassen sich meine alten Informationen hier einblenden.

Nachdem man unter den benutzerdefinierten Apps auf "Apps für Deinen Shop entwickeln", danach auf "Eine App erstellen" geklickt hat und einen Namen für die App vergeben hat, konfiguriert man erst die Admin-API-Bereiche und wählt alle APIs aus, auf die man gerne Schreib- und/oder Lesezugriff haben möchte.
Achtung: Darauf generiert man einen Token. Möchte man später an den Zugriffen Änderungen durchführen, zum Beispiel noch Zugriffe hinzufügen, so erhält man darauf einen neuen Token!

Danach auf "App installiere" klicken und den Token einmalig einblenden und kopieren. Es gibt tatsächlich keine zweite Möglichkeit, diesen Token zu Gesicht zu bekommen!
Sollte man ihn verlieren, so muss die App deinstalliert und nochmals installiert werden (oder technisch korrekt: Die API deaktiviert und wieder neu aktiviert werden), worauf man einen neuen Token erhält.

Meine PHP-Klasse, die ich für die vorherige API mit Schlüssel und Passwort erstellt hatte, habe ich entsprechend geändert:

shopify_api.php - v2 mit Token
<?php

class shopify {

    public  $data = false;
    private $unsecure     = false;
    private $apitoken     = "*Admin-API-Zugriffstoken*";
    private $shopname     = "*Shopname aus xxx.myshopify.com*";
    private $url          = "";

    public function __construct() {
      $this->url = "https://".$this->shopname.".myshopify.com/admin/api/2022-04/";
      }
 
  public function unsecure() {
    $this->unsecure = true;
    }
 
  private function _api($mode, $path, $data = false) {
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, $this->url . $path);
    curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: application/json', 'X-Shopify-Access-Token: '.$this->apitoken));
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    if ($mode == "DELETE")   curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "DELETE");
    elseif ($mode == "PUT")  curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "PUT");
    elseif ($mode == "POST") curl_setopt($curl, CURLOPT_POST, true);
    if ($this->unsecure) {
      curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
      curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
      }
    if ($data !== false)
      curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
    $result = curl_exec($curl);
    $headers = curl_getinfo($curl);
    curl_close($curl);
    return json_decode($result, true);
    }
 
  public function get($path, $data = false) {
    return $this->_api("GET", $path, $data);
    }

  public function post($path, $data = false) {
    return $this->_api("POST", $path, $data);
    }

  public function put($path, $data = false) {
    return $this->_api("PUT", $path, $data);
    }

  public function del($path, $data = false) {
    return $this->_api("DELETE", $path, $data);
    }
  }
?>

Lässt sich wie folgt verwenden:

Beispiel.php
include 'shopify_api.php';
$api = new shopify();
$produkt = @$api->get("products/632910392.json");
echo($produkt['product']['title']);

Eine API-Ãœbersicht findet sich auf dieser Seite.

Webhooks

Doch ich will nicht im Minutentakt Daten abzapfen, sondern ich will ganz bequem die Daten von Shopify auf dem Silbertablett präsentiert bekommen.
Webhooks sind die Lösung!
Die Webhook-API ist allerdings nur sehr versteckt in der GUI zu finden:
Unter Einstellungen > Benachrichtigungen ganz unten (macht Sinn, oder?).
Dort lässt sich ein neuer Webhook erstellen, der Trigger auswählen, bei welchem Daten versendet werden sollen (z.B. jemand hat im Shop etwas bestellt) und eine URL angeben, an welche die Daten versendet werden sollen.

Achtung: Die Adresse, an welche Shopify Daten senden soll, muss per HTTPS erreichbar sein und ein gültiges Zertifikat vorweisen!

Webhooks per curl erstellen/löschen/modifizieren

Alternativ zum versteckten GUI hat man sehr viele Möglichkeiten, per Kommandozeile mit curl Befehle an den shopify-Server zu senden, um Webhooks zu erstellen/löschen/modifizieren.
Da dies nicht all zu intuitiv ist und man sich die Dokumentation zusammensuchen muss, möchte ich dies im Folgenden kurz zusammenfassen:
Will ich Webhooks erstellen, ändern oder abfragen, muss ich laut Shopify-Dokumentation immer einen {access_token} mitgeben.
Dieser Accesstoken ist allerdings weder der API-Schlüssel, den ich für die "Private App" generiert hatte, noch der geheime geteilte Schlüssel...der {access_token} ist schlichtweg das Passwort, welches unter Admin-API bei der Privaten App angezeigt wird (im Bild rechts markiert).
Folglich muss ich dazu auch vorher eine "Private App" angelegt haben.

Webhooks abfragen
Die einfachste Kür: Nehmen wir an, ich betreibe einen Shop namens apfelz, habe eine "Private App" generiert und dort das Passwort shppa_abc123 erhalten, so kann ich alle bereits bestehenden Webhooks mit diesem CURL-Befehl auf der Kommandozeile abfragen:

curl -X GET "https://apfelz.myshopify.com/admin/api/2021-04/webhooks.json" -H "X-Shopify-Access-Token:shppa_abc123"

Webhook einrichten
Kommen wir nun zum eigenlichen interessanten Part: Ich möchte einen Webhook einrichten, so dass bei einer neuen Bestellung die Daten zur Bestellung an meinen eigenen Server gesendet werden.

Auf dieser Seite ist eine Liste aller verfügbaren Webhooks zu sehen.
Mich interessieren neu erstellte Bestellungen, da hört sich orders/create ganz interessant an. Ich möchte bei einer neuen Bestellung Daten der Bestellungen an ein PHP-Skript auf meinem eigenen Webserver senden. Die Eckdaten, dass es sich um orders/create handelt und wie die Adresse meines PHP-Skriptes, an welches ich die Daten senden möchte, lautet, gebe ich als JSON-Objekt mit.
Der Curl-Befehl sieht wie folgt aus:

curl -d '{"webhook":{"topic":"orders\/create","address":"https:\/\/test.apfelz.net\/shopify\/input.php","format":"json"} }' -X POST "https://apfelz.myshopify.com/admin/api/2021-04/webhook.json" -H "X-Shopify-Access-Token:shppa_abc123"

Die Felder, die mir als Antwort an https://test.apfelz.net/shopify/input.php gesendet werden, lassen sich mit dem fields-Objekt einschränken:

... ,"format":"json", "fields": ["id", "note"] ...

Achtung: Endpunkt ist hier .../api/<version>/webhook.json und nicht webhooks.json
Verwendet man wie im vorhergehenden Beispiel webhooks.json, so bekommt man die Fehlermeldung "Required parameter missing or invalid" zurückgemeldet.

Webhook testen

Hat man einen Webhook eingerichtet, möchte man diesen vielleicht testen, ohne dafür extra eine neue Fake-Bestellung im Shop aufgeben zu müssen.
Sofern man seinen Shop als Developer-Shop eingerichtet hat, kann man unter Einstellungen > Benachrichtigungen > Webhooks (ganz unten) Testdaten an alle eingerichteten Webhooks senden.

An dieser Stelle sieht man auch gelb hinterlegt einen Schlüssel, der neben den eigentlichen Daten bei jeder Anfrage mitgegeben wird. Prüft man die eingegangenen Daten auf jenen Schlüssel, kann man sicher gehen, dass die Anfrage auch wirklich vom offiziellen Shopify-Webhook stammt.

Skript, welches die Daten entgegennimmt

im Folgenen ein einfach aufgebautes Skript, welches den eben erwähnten Schlüssel prüft und die rohen Daten in eine Datenbank speichert. Dazu in das Feld verify_webhook eine 1 oder 0, je nachdem, ob der Schlüssel verifiziert werden konnte.

Wichtig: Das Skript sollte einen HTTP-Status 200 zurückgeben.

<?php
$hmac_header = $_SERVER['HTTP_X_SHOPIFY_HMAC_SHA256'];
define('SHOPIFY_APP_SECRET', '/* Der unter Einstellungen > Benachrichtigungen > Webhooks angezeigte Schlüssel */');

$headers = getallheaders();
$request_body = file_get_contents('php://input', 'rb');
$json = json_decode($request_body, true);

// Und in Datenbank schreiben...
$stmt= $mysqli->prepare("insert into test (payload, verify_webhook) values (?, ?)");
$stmt->bind_param("si", $request_body, verify_webhook($request_body,$hmac_header)?1:0);
$stmt->execute();

header("HTTP/1.1 200 OK"); // <-- wichtig!

function verify_webhook($data, $hmac_header) {
  $calculated_hmac = base64_encode(hash_hmac('sha256', $data, SHOPIFY_APP_SECRET, true));
  return hash_equals($hmac_header, $calculated_hmac);
  }
?>