# Webhooks

Empfangen Sie Zahlungsaktualisierungen, prüfen Sie Signaturen und behandeln Sie Wiederholungen.

Source: https://docs.invoise.me/de/integration/webhooks/

Ein Webhook ist eine HTTP-Anfrage, die Invoise an Ihren Server sendet, wenn sich eine Zahlung ändert. Sie können mit [GET-Statusprüfungen](https://docs.invoise.me/de/payments/status/) beginnen und Webhooks hinzufügen, wenn Sie automatische Updates benötigen.

## 1. Ihren Endpunkt registrieren

```bash
curl -X POST 'https://platform.invoise.me/api/v1/shops/{shop_id}/webhooks' \
  -H 'Authorization: Bearer ivk_...' \
  -H 'Idempotency-Key: <saved-unique-key>' \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com/invoise","filters":["transfer","invoice","payout"]}'
```

Verwenden Sie Ihre eigene öffentliche HTTPS-URL. Lokale und private Adressen werden abgelehnt. Speichern Sie das beim Erstellen des Endpunkts zurückgegebene `secret`: Sie benötigen es, um Anfragen zu verifizieren.

Ein Shop kann bis zu 10 Endpunkte haben. Die URL eines Endpunkts kann geändert werden, verwenden Sie also einen weiter, statt einen weiteren hinzuzufügen. Siehe [Limits](https://docs.invoise.me/de/integration/idempotency-and-errors/#limits).

## 2. Die benötigten Ereignisse auswählen

| Ereignis | Bedeutung |
| --- | --- |
| `transfer.observed` | Ein Transfer wurde gesehen; er ist noch nicht bestätigt. |
| `transfer.confirmed` | Ein eingehender Transfer hat genügend Bestätigungen. Es kann nur eine teilweise Rechnungszahlung sein. |
| `transfer.reverted` | Ein vorheriges Transfer-Ereignis wurde zurückgenommen. |
| `invoice.closed` | Die Rechnung wurde on-chain abgeschlossen. |
| `invoice.cancelled` | Die Zahlungsseite wurde storniert oder die Rechnung ist abgelaufen; dies ist keine Rückerstattung. |
| `payout.sent` | Die Auszahlung wurde gesendet oder ihr ausgehender Transfer wurde gesehen. |
| `payout.confirmed` | Eine vollständige Abrechnungsbuchung ist verfügbar. |
| `payout.reverted` | Eine vorherige Auszahlungsbestätigung wurde zurückgenommen. |
| `payout.failed` | Ein endgültiger Auszahlungsfehler erfordert ein Eingreifen. |

Bei `invoice.cancelled` gibt `data.reason` den Grund an: `expired`, wenn die [Rechnungslaufzeit](https://docs.invoise.me/de/payments/deposits-and-invoices/#rechnungslaufzeit) abgelaufen ist, `merchant_blocked`, wenn das Invoise-Team den Händler gesperrt hat. Eine Stornierung durch den Händler hat keinen `reason`.

Abonnieren Sie nach Gruppe: `transfer`, `invoice`, `payout`. Eingehende Transfers und ausgehende Auszahlungen sind unterschiedliche Ereignisse. Eine vorübergehende RPC- oder Gas-Verzögerung ist kein `payout.failed`.

`gas.wait` und `sweep.failed` werden als Filter akzeptiert, aber Invoise sendet sie nicht an Ihren Endpunkt. Es sind interne Signale: `gas.wait` bedeutet, dass eine Auszahlung auf Mittel für die Netzwerkgebühr wartet und von selbst fortfährt; `sweep.failed` bedeutet, dass ein Versuch, die Mittel zu Ihrem Empfänger zu bewegen, fehlgeschlagen ist. Für keines von beidem ist eine Aktion Ihrerseits nötig. Eine Auszahlung, die nicht abgeschlossen werden kann, erreicht Sie als `payout.failed`.

In einem Sandbox-Shop können Sie beides über `POST /shops/{shop_id}/sandbox/simulate` simulieren, um zu prüfen, dass eine verzögerte Auszahlung Ihren Ablauf nicht stört. Sie ändern nur den Zustand der Auszahlung und senden keinen Webhook.

## 3. Vor der Verarbeitung überprüfen

`Invoise-Signature` hat die Form `t=<timestamp>,v1=<hex>`. Die Signatur ist HMAC-SHA256 von `timestamp + "." + raw_body`, unter Verwendung Ihres Webhook-Secrets.

Das folgende Node.js-Beispiel lehnt außerdem Zeitstempel ab, die mehr als fünf Minuten von der Serveruhr abweichen. Halten Sie diese Uhr synchronisiert und wählen Sie Ihre Toleranz bewusst.

```js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(rawBody, header, secret) {
  const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(header ?? '');
  if (!match) return false;
  const [, timestamp, signature] = match;
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > 300) return false;
  const expected = createHmac('sha256', secret)
    .update(timestamp + '.')
    .update(rawBody)
    .digest();
  return timingSafeEqual(Buffer.from(signature, 'hex'), expected);
}
```

Übergeben Sie die ursprünglichen Anfrage-Bytes als `rawBody`. Parsen Sie das JSON nicht und wandeln Sie es nicht wieder in einen String um, bevor Sie die Signatur prüfen. Lehnen Sie ungültige Signaturen ab, bevor Sie ihren Inhalt speichern oder darauf reagieren.

## 4. Einmal verarbeiten

Eine gekürzte Payload sieht so aus:

```json
{
  "id": "<event-id>",
  "type": "transfer.confirmed",
  "created_at": "2026-09-19T10:00:00Z",
  "data": {
    "issuance_id": "<issuance-id>",
    "shop_id": "<shop-id>",
    "external_id": "order-123",
    "type": "invoice",
    "invoice_cancelled": false
  }
}
```

| Fall | Behandlung |
| --- | --- |
| Duplikate | Deduplizieren Sie anhand von `Invoise-Event-ID`, die der `id` der Envelope entspricht. Speichern Sie die ID und das Geschäfts-Update atomar. |
| Ein Ereignis annehmen | Speichern Sie angenommene Ereignisse dauerhaft, geben Sie umgehend 2xx zurück und verarbeiten Sie anschließend die Warteschlange. |
| Wiederholungen und Replays | Wiederholungen und manuelle Replays verwenden dieselbe Ereignis-ID erneut. `Invoise-Attempt-ID` ändert sich und ist kein Deduplizierungsschlüssel. |
| Ereignisreihenfolge | Ereignisse können in falscher Reihenfolge eintreffen. Verwenden Sie deren Inhalt und gleichen Sie bei Bedarf mit dem [aktuellen Status](https://docs.invoise.me/de/payments/status/) ab. |

Bei Rückabwicklungen verweist `chain_event.reference_event_id` auf das frühere Chain-Ereignis. Auszahlungs-Payloads enthalten eine stabile `payout_id`, einen Transaktions-Hash, Beträge und Gebührendetails; unbekannte Beträge sind `null`. Ihre Liste eingehender Transfer-IDs ist auf 100 begrenzt und markiert eine Kürzung.

Ältere gespeicherte Ereignisse können `project_id` verwenden; lesen Sie zuerst `shop_id` und greifen Sie bei Bedarf darauf zurück. Replays behalten die ursprünglichen Bytes.

## Fehlgeschlagene Zustellung prüfen

Listen Sie mit `GET /api/v1/shops/{shop_id}/deliveries` auf. Wiederholen Sie eine Zustellung mit `POST /api/v1/shops/{shop_id}/deliveries/{id}/replay` und einem gespeicherten Idempotenzschlüssel. Das Wiederholen erstellt kein neues Geschäftsereignis.
