Einen Qvard-Webhook verifizieren
Ein kurzer Leitfaden zum Header X-Qvard-Signature: was er ist und wie man ihn mit ein paar Zeilen Code verifiziert.
Was ist eine Signatur
Wenn sich der Status einer Zahlung ändert, sendet Qvard eine Benachrichtigung (Webhook) an die Adresse in Ihren Shop-Einstellungen. Jeder kann eine Anfrage an diese Adresse senden, daher benötigen Sie eine Möglichkeit, eine echte Benachrichtigung von einer gefälschten zu unterscheiden.
Die Signatur ist dieser Beweis: ein kurzer Code, den Qvard aus der Nachricht und Ihrem geheimen Schlüssel berechnet. Nur Qvard und Sie kennen den Schlüssel, daher kann nur Qvard den richtigen Code erzeugen.
Was Sie erhalten
Jeder Webhook trägt einen zusätzlichen Header und einen kleinen JSON-Body:
X-Qvard-Signature: t=1760950000,v1=3f2a…c91e
{"id":"order_1024","status":"paid"}t ist die Zeit, in der die Nachricht gesendet wurde (Unix, Sekunden). v1 ist der Code. Für 24 Stunden nach der Rotation Ihres geheimen Schlüssels trägt der Header zwei v1-Werte: einen für den neuen Schlüssel und einen für den alten.
Die Felder id und status bleiben wie bisher. Dem gleichen signierten Body werden vier Betragsfelder hinzugefügt:
- amount und currency: der Betrag und seine Währung, so wie Sie sie in der Anfrage gesendet haben.
- amountUsd: der Buchungsbetrag in USD.
- rate: Währungseinheiten für 1 USD zum Zeitpunkt der Zahlungserstellung.
- Bestehende Integrationen funktionieren weiter: Sie lesen id und status und ignorieren die neuen Felder. Die Signatur wird wie bisher über denselben Body geprüft.
Wie man es überprüft
- 1Nehmen Sie den Request-Body genau wie erhalten: Rohtext, vor jeglichem JSON-Parsing.
- 2Lesen Sie t und jeden v1 aus dem Header X-Qvard-Signature.
- 3Berechnen Sie HMAC-SHA256 des Textes t + "." + Body mit Ihrem geheimen Schlüssel (qvard_sk_…) und schreiben Sie ihn als Kleinbuchstaben-Hexadezimal.
- 4Wenn Ihr Ergebnis einem beliebigen v1 entspricht (vergleichen Sie in konstanter Zeit), ist die Nachricht echt.
- 5Überprüfen Sie, dass t nicht älter als 5 Minuten ist, damit eine alte Nachricht nicht erneut gespielt werden kann.
- 6Verwenden Sie erst dann die Daten und bestätigen Sie den gezahlten Betrag für diese Bestellung in Ihrer eigenen Datenbank.
Fertiger Code
const crypto = require('crypto'); function verifyQvardSignature(rawBody, header, secret, toleranceSec = 300) { const parts = header.split(',').map((p) => p.trim()); const t = Number((parts.find((p) => p.startsWith('t=')) || '').slice(2)); const sigs = parts.filter((p) => p.startsWith('v1=')).map((p) => p.slice(3)); if (!t || sigs.length === 0) return false; if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest(); return sigs.some((s) => { const got = Buffer.from(s, 'hex'); return got.length === expected.length && crypto.timingSafeEqual(got, expected); }); } // Use the RAW body: express.raw, not express.json app.post('/qvard-webhook', express.raw({ type: 'application/json' }), (req, res) => { const raw = req.body.toString('utf8'); if (!verifyQvardSignature(raw, req.get('X-Qvard-Signature') || '', process.env.QVARD_SECRET_KEY)) { return res.sendStatus(400); } const { id, status } = JSON.parse(raw); // Confirm the paid amount for order `id` in your own database before fulfilling it. res.sendStatus(200); });
<?php function verifyQvardSignature(string $rawBody, string $header, string $secret, int $tolerance = 300): bool { $t = 0; $sigs = []; foreach (explode(',', $header) as $part) { $part = trim($part); if (strpos($part, 't=') === 0) { $t = (int) substr($part, 2); } elseif (strpos($part, 'v1=') === 0) { $sigs[] = substr($part, 3); } } if (!$t || !$sigs || abs(time() - $t) > $tolerance) { return false; } $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret); foreach ($sigs as $s) { if (hash_equals($expected, $s)) { return true; } } return false; } $raw = file_get_contents('php://input'); if (!verifyQvardSignature($raw, $_SERVER['HTTP_X_QVARD_SIGNATURE'] ?? '', getenv('QVARD_SECRET_KEY'))) { http_response_code(400); exit; } $event = json_decode($raw, true); // ['id' => ..., 'status' => ...] http_response_code(200); ?>
Für 24 Stunden nach der Rotation Ihres geheimen Schlüssels trägt der Header zwei v1-Werte: akzeptieren Sie den Webhook, wenn einer von ihnen übereinstimmt.
Häufige Fehler
- Das Parsing von JSON und dessen erneute Serialisierung ändert den Text. Verwenden Sie immer den Rohtext.
- Verwendung des öffentlichen Schlüssels (qvard_pk_…) statt des geheimen Schlüssels.
- Eine desynchronisierte Server-Uhr führt dazu, dass die 5-Minuten-Prüfung fehlschlägt. Halten Sie sie synchronisiert.
- Vergleich mit == statt mit konstantem Zeit-Vergleich.
Wo Sie Ihren geheimen Schlüssel finden
Im Dashboard: Ihr Shop, Einstellungen, API-Schlüssel. Speichern Sie ihn nur auf Ihrem Server. Falls er auslaufen sollte, rotieren Sie ihn dort: Der alte Schlüssel läuft 24 Stunden lang für Zahlungen (nicht für Auszahlungen).
Ältere Schlüssel
Wenn Ihr Shop noch einen Schlüssel im alten Format hat (ohne qvard_sk_-Präfix), enthalten Webhooks auch einen x-api-key-Header mit diesem Schlüssel. Nach der Rotation verschwindet dieser Header. Wechseln Sie zur Signaturprüfung.