# Verifizieren und testen (/de/webhooks/receiving)



Verifizieren Sie jeden Webhook, bevor Sie seine Payload verwenden, und senden Sie dann einen Test, um zu bestätigen, dass Ihr Empfänger durchgängig funktioniert.

## Vorbereitung [#before-you-start]

* Ein [gespeichertes Ziel](/webhooks/configuration#save-a-destination) und sein signing secret (Signaturschlüssel), `twhsec_...`. Verwenden Sie nicht Ihr Server Secret Token, um Webhooks zu verifizieren.
* Eine Empfängerroute, die den ursprünglichen Anfrageinhalt lesen kann, bevor ein JSON-Parser ihn verändert.
* Ein Speicher mit einer Eindeutigkeitsbedingung für die Ereignis-`id`, damit Duplikate unbedenklich sind.

## Webhook empfangen [#receive-a-webhook]

<div className="fd-steps [&_h3]:fd-step">
  ### Ursprünglichen Inhalt lesen [#read-the-raw-body]

  Tapaya sendet einen JSON-`POST` mit diesen Headern. Behalten Sie den Inhalt als ursprüngliche Bytes bei; ein geparster und erneut serialisierter Inhalt stimmt nicht mehr mit der Signatur überein.

  ```http
  Content-Type: application/json
  X-Tapaya-Event-Id: 019a0000-0000-7000-8000-000000000001
  X-Tapaya-Signature: t=1793361600,v1=<lowercase hexadecimal HMAC-SHA256>
  ```

  * Verwenden Sie mit Express für diese Route `express.raw({ type: "application/json" })` vor einem globalen JSON-Parser.

  ### Signatur prüfen [#check-the-signature]

  Berechnen Sie HMAC-SHA256 über den Zeitstempel, einen Punkt und den ursprünglichen Inhalt, und vergleichen Sie das Ergebnis dann mit konstanter Laufzeit mit `v1`.

  ```js title="verify-tapaya-webhook.mjs"
  import { createHmac, timingSafeEqual } from "node:crypto";

  export function verifyTapayaWebhook(rawBody, signatureHeader, secret, now = Date.now()) {
    if (!Buffer.isBuffer(rawBody) || typeof signatureHeader !== "string") {
      throw new Error("Missing raw body or signature");
    }
    if (typeof secret !== "string" || !secret.startsWith("twhsec_")) {
      throw new Error("Missing signing secret");
    }

    const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(signatureHeader);
    if (!match) throw new Error("Malformed signature");

    const timestamp = Number(match[1]);
    if (!Number.isSafeInteger(timestamp) || Math.abs(now / 1000 - timestamp) > 300) {
      throw new Error("Expired signature");
    }

    const expected = createHmac("sha256", Buffer.from(secret, "utf8"))
      .update(`${match[1]}.`, "utf8")
      .update(rawBody)
      .digest();
    const received = Buffer.from(match[2], "hex");
    if (!timingSafeEqual(expected, received)) throw new Error("Invalid signature");

    return JSON.parse(rawBody.toString("utf8"));
  }
  ```

  * Verwenden Sie das vollständige Geheimnis `twhsec_...` als UTF-8-Schlüssel. Decodieren Sie es nicht und entfernen Sie sein Präfix nicht.
  * Weisen Sie Zeitstempel zurück, die mehr als fünf Minuten von Ihrer Uhrzeit abweichen, und halten Sie die Uhr synchronisiert.
  * Während Sie [das signing secret rotieren](/webhooks/configuration#configuration-changes), prüfen Sie zuerst das neue Secret und dann das alte, bis die Warteschlange abgearbeitet ist.

  ### Speichern und bestätigen [#store-and-acknowledge]

  Nehmen Sie das Ereignis erst an, wenn es sicher gespeichert ist, damit ein Fehler auf Ihrer Seite eine Wiederholung auslöst.

  ```js title="server.mjs"
  import express from "express";
  import { verifyTapayaWebhook } from "./verify-tapaya-webhook.mjs";
  import { saveEventOnce } from "./events.mjs"; // your store, unique on event id

  const app = express();

  app.post(
    "/webhooks/tapaya",
    express.raw({ type: "application/json" }),
    async (req, res) => {
      let event;
      try {
        event = verifyTapayaWebhook(
          req.body,
          req.get("X-Tapaya-Signature"),
          process.env.TAPAYA_WEBHOOK_SECRET,
        );
      } catch {
        return res.sendStatus(400);
      }
      if (event.organizationId !== process.env.TAPAYA_ORGANIZATION_ID) {
        return res.sendStatus(400);
      }

      await saveEventOnce(event); // skips an id it already stored
      res.sendStatus(204);
    },
  );
  ```

  * Geben Sie für ein bereits gespeichertes Ereignis `2xx` zurück. Wiederholungen behalten dieselbe `id` und denselben Inhalt, mit neuem Zeitstempel und neuer Signatur.
  * Geben Sie eine Antwort außerhalb von `2xx` zurück, wenn Sie ein neues Ereignis nicht speichern können, damit die Zustellung wiederholt wird.
  * Verarbeiten Sie gespeicherte Ereignisse aus Ihrer eigenen Warteschlange. Die Zustellreihenfolge ist nicht garantiert; gleichen Sie ältere Snapshots daher mit dem aktuellen Zustand ab.
</div>

## Test-Webhook senden [#send-a-test-webhook]

Die Testaktion der Platform sendet eine signierte Anfrage `webhook.test` an die gespeicherte URL. Sie funktioniert auch, wenn Webhooks deaktiviert sind oder der Typ nicht in Ihrem Abonnement enthalten ist, und erstellt weder eine Zahlung noch eine Erstattung noch einen Händler.

1. Öffnen Sie **API Keys** für Ihre Organisation und speichern Sie das Ziel.
2. Verwenden Sie die Webhook-Testaktion.
3. Prüfen Sie das Ergebnis, die Dauer und das gesendete JSON, die in der Platform angezeigt werden.

```json
{
  "id": "019a0000-0000-7000-8000-000000000010",
  "type": "webhook.test",
  "apiVersion": "1",
  "organizationId": "019a0000-0000-7000-8000-000000000002",
  "merchantId": null,
  "createdAt": "2026-10-30T12:00:00.000000Z",
  "test": true,
  "data": {
    "message": "This is a test webhook from Tapaya. No payment was created."
  }
}
```

<Accordions type="multiple">
  <Accordion title="Was ein Test abdeckt" id="what-a-test-covers">
    * Ein Versuch mit einem Zeitlimit von 10 Sekunden und ohne automatische Wiederholungen.
    * Er prüft die Zustellung und Signaturverarbeitung, nicht Zahlungs- oder Onboarding-Payloads. Testen Sie diese mit Sandbox-Aktivitäten.
    * Eine fehlgeschlagene Zustellung wird als Testergebnis angezeigt, nicht als Fehler in der Platform.
  </Accordion>

  <Accordion title="Testlimits" id="test-limits">
    * Zustellungen aus der Warteschlange und manuelle Tests teilen sich 2 gleichzeitige Anfragen pro Organisation.
    * Manuelle Tests erlauben zusätzlich 10 Starts innerhalb der jeweils letzten 60 Sekunden pro Organisation, gemeinsam für alle Benutzer.
    * Beim Überschreiten eines der Limits gibt der Test `429` mit `Retry-After` in Sekunden zurück und sendet nichts.
    * Zugelassene Tests zählen auch bei Fehlern oder Abbruch. Das erneute Anlegen des Ziels setzt die Zählung nicht zurück.
  </Accordion>
</Accordions>

## Lokale und Automatisierungsempfänger [#local-and-automation-receivers]

Richten Sie einen Webhook auf einen Entwicklungsrechner oder ein No-Code-Tool, wenn Ihr Produktionsserver noch nicht bereit ist.

<Accordions type="multiple">
  <Accordion title="Lokalen Empfänger testen" id="test-a-local-receiver">
    * Speichern Sie die URL eines öffentlichen HTTPS-Tunnels als Ziel; Localhost-URLs und private IP-Adressen werden zurückgewiesen.
    * Lassen Sie die Signaturprüfung aktiviert und stellen Sie sicher, dass der Tunnel den ursprünglichen Inhalt und die Tapaya-Header unverändert weiterleitet.
    * Wenn sich die URL des Tunnels ändert, speichern Sie das Ziel erneut. Halten Sie den alten Tunnel verfügbar, bis die dafür eingereihten Ereignisse abgearbeitet sind.
  </Accordion>

  <Accordion title="Automatisierungstool verbinden" id="connect-an-automation-tool">
    * Der Einstiegspunkt darf keine benutzerdefinierten Authentifizierungsheader benötigen, muss den ursprünglichen Inhalt beibehalten und innerhalb von 10 Sekunden antworten.
    * Er muss den HMAC des ursprünglichen Inhalts selbst prüfen oder hinter einem Verifizierungsadapter geschaltet sein, der dies übernimmt.
    * Sobald das Tool `2xx` antwortet, gilt die Zustellung als erfolgreich. Spätere Workflow-Fehler und Duplikate muss das Tool selbst behandeln; sie lösen keine weitere Zustellung aus.
  </Accordion>
</Accordions>

## Fehler behandeln [#handle-errors]

<Accordions type="multiple">
  <Accordion title="Testergebnisse" id="test-results">
    | Ergebnis          | Bedeutung                                                                                                              |
    | ----------------- | ---------------------------------------------------------------------------------------------------------------------- |
    | `delivered: true` | Der Empfänger hat HTTP `2xx` zurückgegeben.                                                                            |
    | `http_error`      | Der Empfänger hat einen Status außerhalb von `2xx` zurückgegeben.                                                      |
    | `transport_error` | Das Ziel konnte nicht erreicht werden, einschließlich Fehlern bei TLS, DNS oder der Prüfung auf öffentliche Netzwerke. |
    | `timeout`         | Der Empfänger hat nicht innerhalb von 10 Sekunden bestätigt.                                                           |
  </Accordion>

  <Accordion title="Signaturprüfungen" id="signature-checks">
    | Symptom                       | Prüfung                                                                                                                                                                  |
    | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
    | Signatur stimmt nicht überein | Verwenden Sie das vollständige Geheimnis und die ursprünglichen Bytes. Neu formatiertes JSON oder das API-Token als Schlüssel führen zum Fehlschlagen der Verifizierung. |
    | Abgelaufene Signatur          | Synchronisieren Sie die Uhr des Empfängers und prüfen Sie anhand des Anfrageheaders, nicht anhand von `createdAt`.                                                       |
  </Accordion>

  <Accordion title="Zustellung" id="delivery">
    | Symptom                                                   | Prüfung                                                                                                                                                                       |
    | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | URL zurückgewiesen oder Verbindung blockiert              | Verwenden Sie öffentliches HTTPS auf Port 443 ohne Weiterleitungen, Zugangsdaten oder Fragment.                                                                               |
    | Kein Webhook-Ereignis                                     | Prüfen Sie die Organisation, den Aktivierungszustand und die ausgewählten Ereignistypen. Frühere Änderungen werden nicht nachträglich bereitgestellt.                         |
    | Doppeltes oder älteres Ereignis                           | Deduplizieren Sie anhand von `id` und gleichen Sie mit dem aktuellen Zustand ab, statt von einer Reihenfolge auszugehen. Siehe die [Zustellregeln](/webhooks#delivery-rules). |
    | Test funktioniert, aber Zahlungsverarbeitung schlägt fehl | `webhook.test` hat eine andere `data`-Struktur. Leiten Sie anhand von `type` weiter und testen Sie mit echten Sandbox-Ereignissen.                                            |
  </Accordion>
</Accordions>
