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.
 
Danach kann man eben an jener Stelle eine "Neue private App erstellen".
 
Man gibt der freigeschalteten API einen Namen und wählt aus, welche Zugriffe möglich sein sollen ("Admin-API-Berechtigungen"). Natürlich sollte man den Zugriff nur auf jene Daten beschränken, die auch tatsächlich benötigt werden. Zum Beispiel nur die Produktinfos im ReadOnly-Modus und keine Zahlungs- oder Kundendaten.

Kein Plan, was die Storefront API ist.
Also halt ohne jene speichern und man erhält API-Schlüssel, Passwort und einen Geteilten geheimen Schlüssel.

 

Über die Adresse https://{apikey}:{passwort}@{shopname}.mycopiloo.com/admin/api/{version}/{resource}.json könnte ich nun von einem externen Server aus Daten abzapfen.
Alternativ (statt in der URL) kann ich apikey und passwort aber auch im Anfrage-Header mitgeben.

<?php
$curl = curl_init();
// Zugangsdaten in der URL
curl_setopt($curl, CURLOPT_URL, "https://$API_KEY:$CLIENTPASS@apfelzshop.myshopify.com/admin/api/2021-10/products/0815.json");
curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
// Zugangsdaten im Header
curl_setopt($curl, CURLOPT_URL, "https://apfelzshop.myshopify.com/admin/api/2021-10/products/0815.json");
curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: application/json',
                                            'Authorization: Basic '.base64_encode($API_KEY.":".$CLIENTPASS)));
//...
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($curl);
print_r(json_decode($result, true));
?>

Ich habe daraus eine kleine PHP-Klasse gebastelt:

shopify_api.php
<?php

class shopify {

    public  $data = false;
    private $unsecure     = false;
    private $clientid     = "*API-Schlüssel*";
    private $clientpass   = "*Passwort*";
    private $shopname    = "*Shopname aus xxx.myshopify.com*";
    private $url          = "";
    public  $token        = "";
    public  $refreshtoken = "";

    public function __construct() {
      $this->url = "https://".$this->shopname.".myshopify.com/admin/api/2021-10/";
      }
 
  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', 'Authorization: Basic '.base64_encode($this->clientid.":".$this->clientpass)));
    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);
  }
?>