> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fynn.eu/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Lass dein System in Echtzeit benachrichtigen, wenn ein Angebot abläuft. Aufbau des Envelopes, verfügbare Events und Beispiel-Payloads.

Webhooks für Angebote benachrichtigen dein System automatisch über Ereignisse im Lebenszyklus eines Angebots. Sobald ein Ereignis eintritt, sendet Fynn einen `POST`-Request mit strukturierten JSON-Daten an deine hinterlegte URL.

<Info>
  Die Einrichtung, die Zustellung und die Signaturprüfung sind für alle Webhooks gleich. Wie du Webhooks anlegst und absicherst, liest du unter [Webhooks einrichten](/guide/webhooks/introduction). Auf dieser Seite findest du nur, was für Angebote gilt: die verfügbaren Events und ihre Payloads.
</Info>

## Der Envelope

Jeder Angebots-Webhook hat denselben Aufbau. Das eigentliche Objekt steckt unter `data`, daneben liegen Metadaten zum Ereignis und der Vertriebskanal als Kontext.

<ResponseField name="event" type="object" required>
  Metadaten zum Ereignis.

  <Expandable title="event">
    <ResponseField name="id" type="string">
      Eindeutige Kennung dieser Zustellung (ULID), identisch mit dem Header `X-Webhook-Id`. Nutze sie, um Wiederholungen idempotent zu verarbeiten.
    </ResponseField>

    <ResponseField name="type" type="string">
      Der Ereignistyp, zum Beispiel `offer.expired`.
    </ResponseField>

    <ResponseField name="version" type="string">
      Schema-Version des Ereignisses, aktuell `v1`.
    </ResponseField>

    <ResponseField name="createdAt" type="string">
      Zeitpunkt der Auslösung im ISO-8601-Format.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="salesChannel" type="object | null">
  Der Vertriebskanal, in dessen Kontext das Ereignis entstand. `null`, wenn sich kein Kanal auflösen ließ.

  <Expandable title="salesChannel">
    <ResponseField name="id" type="string">
      Kennung des Vertriebskanals.
    </ResponseField>

    <ResponseField name="name" type="string">
      Technischer Name des Kanals, zum Beispiel `default`.
    </ResponseField>

    <ResponseField name="brandName" type="string">
      Anzeigename der Marke.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="data" type="object" required>
  Die Nutzdaten des Ereignisses. Der Schlüssel darin benennt das betroffene Objekt, bei Angebots-Events also `offer`.
</ResponseField>

## HTTP-Header

Jede Zustellung bringt diese Header mit:

| Header                    | Inhalt                                                       |
| ------------------------- | ------------------------------------------------------------ |
| `X-Webhook-Event`         | Der Ereignistyp, zum Beispiel `offer.expired`.               |
| `X-Webhook-Event-Version` | Schema-Version, aktuell `v1`.                                |
| `X-Webhook-Id`            | Eindeutige Kennung der Zustellung, identisch mit `event.id`. |
| `X-Webhook-Signature`     | Signatur des Bodys zur Echtheitsprüfung.                     |
| `X-Sales-Channel`         | Technischer Name des Vertriebskanals, oder `default`.        |
| `X-Tenant-Id`             | Kennung deiner Organisation.                                 |

<Warning>
  Prüfe immer die Signatur aus `X-Webhook-Signature`, bevor du eine Zustellung verarbeitest. So stellst du sicher, dass die Anfrage wirklich von Fynn stammt. Den Ablauf und Beispielcode findest du unter [Webhooks einrichten](/guide/webhooks/introduction#sicherheit-x-webhook-signature).
</Warning>

## Verfügbare Events

| Event           | Wird ausgelöst, wenn                                                             |
| --------------- | -------------------------------------------------------------------------------- |
| `offer.expired` | Ein Angebot seinen Gültigkeitszeitraum überschreitet, bevor es angenommen wurde. |

<Note>
  Aktuell ist `offer.expired` das einzige Angebots-Event mit Webhook. Diese Liste wächst, wenn weitere Ereignisse hinzukommen.
</Note>

## `offer.expired`

Das Ereignis feuert, sobald die Gültigkeit eines Angebots verstreicht, solange es noch offen ist. Ein bereits angenommenes oder archiviertes Angebot läuft nie ab.

```mermaid theme={null}
flowchart LR
  Offen -->|Gültigkeit überschritten| Ablauf{Ablauf erkannt}
  Ablauf -->|regelmäßige Prüfung| Webhook[offer.expired]
  Ablauf -->|erster Aufruf des Links| Webhook
```

Fynn erkennt den Ablauf auf zwei Wegen: über eine regelmäßige Prüfung im Hintergrund und beim ersten Aufruf des abgelaufenen Links durch einen Empfänger. Egal welcher Weg zuerst greift, das Ereignis wird je Ablauf genau einmal gesendet. Verlängerst du ein abgelaufenes Angebot und es läuft erneut ab, gilt das als neues Ereignis.

### Das `offer`-Objekt

Unter `data.offer` erhältst du das Angebot in seiner Lese-Darstellung. Die wichtigsten Felder:

<ResponseField name="id" type="string">
  Eindeutige Kennung des Angebots. Nutze sie, um über die [Angebote-API](/v2-offers-api/offers/get-offer) den vollständigen Stand inklusive Kunde, Abonnement und Empfänger zu laden.
</ResponseField>

<ResponseField name="number" type="string">
  Fortlaufende Angebotsnummer, zum Beispiel `AN-2026-000042`.
</ResponseField>

<ResponseField name="name" type="string | null">
  Anzeigename des Angebots.
</ResponseField>

<ResponseField name="status" type="string">
  Der zuletzt gespeicherte Status: `open`, `signing`, `awaiting_invoice_details`, `signed` oder `archived`. Ein abgelaufenes Angebot behält seinen Status (meist `open`), der Ablauf selbst ergibt sich aus `validUntil`.
</ResponseField>

<ResponseField name="dealType" type="string">
  Art des Geschäfts: `new_business`, `expansion`, `renewal` oder `one_off`.
</ResponseField>

<ResponseField name="acceptanceMode" type="string">
  Wie der Empfänger zusagt: `click`, `esignature` oder `print`.
</ResponseField>

<ResponseField name="locale" type="string | null">
  Sprache des Angebots, zum Beispiel `de`.
</ResponseField>

<ResponseField name="validUntil" type="string | null">
  Datum, bis zu dem das Angebot angenommen werden konnte (ISO 8601). Bei `offer.expired` liegt dieser Zeitpunkt in der Vergangenheit.
</ResponseField>

<ResponseField name="issuedAt" type="string">
  Zeitpunkt der ersten Veröffentlichung (ISO 8601).
</ResponseField>

<ResponseField name="signed" type="boolean">
  Ob das Angebot vollständig unterschrieben ist.
</ResponseField>

<ResponseField name="signedAt" type="string | null">
  Zeitpunkt der vollständigen Unterschrift (ISO 8601), sonst `null`.
</ResponseField>

<ResponseField name="autoActivateSubscription" type="boolean">
  Ob aus dem angenommenen Angebot automatisch ein aktives Abonnement entsteht.
</ResponseField>

<ResponseField name="crmDealId" type="string | null">
  Verknüpfter Deal im CRM, sofern vorhanden.
</ResponseField>

<ResponseField name="customVariables" type="object | null">
  Eigene Variablen aus dem Editor als flaches Name-zu-Wert-Mapping.
</ResponseField>

<ResponseField name="capturedInvoiceDetails" type="object | null">
  Die vom Empfänger erfassten Rechnungsdaten, sofern bereits hinterlegt.
</ResponseField>

<ResponseField name="sections" type="array">
  Die Dokument-Blöcke des Angebots (Briefkopf, Parteien, Abonnement, Texte und mehr). Den Aufbau der Blöcke findest du unter [Kernkonzepte](/v2-offers-api/concepts).
</ResponseField>

<ResponseField name="publishedVersionHash" type="string | null">
  Prüfsumme der veröffentlichten Dokumentversion.
</ResponseField>

<ResponseField name="currentVersionHash" type="string | null">
  Prüfsumme der aktuellen Dokumentversion.
</ResponseField>

<ResponseField name="hasUnpublishedChanges" type="boolean">
  Ob seit der letzten Veröffentlichung Änderungen im Entwurf liegen.
</ResponseField>

<Note>
  Die Payload ist bewusst schlank. Verschachtelte Objekte wie `customer`, `subscription`, `recipients` und die Dokument-Referenzen sind im Webhook nicht ausgefüllt und kommen als leeres Objekt, leere Liste oder `null`. Brauchst du diese Details, lade das Angebot mit der `id` aus der Payload über [`GET /offers/{id}`](/v2-offers-api/offers/get-offer) nach.
</Note>

### Beispiel-Payload

```json theme={null}
{
  "event": {
    "id": "01JZ8F3KQ9X7N2VYB4C6D8E0FG",
    "type": "offer.expired",
    "version": "v1",
    "createdAt": "2026-06-29T08:00:01+00:00"
  },
  "salesChannel": {
    "id": "01JH2K8M4P6R8T0V2X4Z6B8D0F",
    "name": "default",
    "brandName": "Deine Marke"
  },
  "data": {
    "offer": {
      "id": "01JZ7A2B3C4D5E6F7G8H9J0K1L",
      "number": "AN-2026-000042",
      "name": "Angebot Enterprise-Plan 2026",
      "status": "open",
      "dealType": "new_business",
      "acceptanceMode": "esignature",
      "locale": "de",
      "validUntil": "2026-06-28T23:59:59+00:00",
      "issuedAt": "2026-06-01T10:00:00+00:00",
      "signed": false,
      "signedAt": null,
      "autoActivateSubscription": true,
      "crmDealId": null,
      "customVariables": null,
      "capturedInvoiceDetails": null,
      "publishedVersionHash": "8f14e45fceea167a5a36dedd4bea2543",
      "currentVersionHash": "8f14e45fceea167a5a36dedd4bea2543",
      "hasUnpublishedChanges": false,
      "sections": [
        { "type": "letterhead", "...": "..." },
        { "type": "parties", "...": "..." },
        { "type": "subscription", "...": "..." }
      ],
      "customer": {},
      "subscription": null,
      "recipients": [
        {}
      ],
      "purchaseOrderDocument": null,
      "auditLogDocument": null,
      "signedDocument": null,
      "contactPerson": {}
    }
  }
}
```

<Tip>
  Antworte mit einem `2xx`-Statuscode, sobald du die Zustellung angenommen hast. Schlägt die Zustellung fehl, wiederholt Fynn sie bis zu dreimal.
</Tip>
