> ## 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

> Configure webhooks and receive notifications.

<Frame>
  <img src="https://mintcdn.com/fynnsubscriptionbilling/1WHsrw9xV98b8NDj/images/fynn-eu-settings-webhooks.png?fit=max&auto=format&n=1WHsrw9xV98b8NDj&q=85&s=c07445e877b05afd86b0391ef1765d20" alt="Fynn Webhooks Settings" width="1678" height="1167" data-path="images/fynn-eu-settings-webhooks.png" />
</Frame>

With webhooks, you can have external systems notified in real-time about events in Fynn. Webhooks are sent as `POST` requests to your registered URL and contain structured JSON data about the respective event.

Webhooks can be configured under [**Settings > Webhooks**](https://app.fynn.eu/settings/webhooks).

***

## Security: `X-Webhook-Signature`

Each webhook request contains an `X-Webhook-Signature` header, which is generated as follows:

```
hash_hmac('sha256', $body, $webhookSecret)
```

* `$body`: The **unchanged JSON body** of the webhook request (as a string)
* `$webhookSecret`: Your secret key for this webhook

You can find the secret under:
[**Settings > Webhooks > Details**](https://app.fynn.eu/settings/webhooks)

<Warning>
  We strongly recommend validating the signature to ensure the request actually comes from Fynn.
</Warning>

***

### Examples

<Tabs>
  <Tab title="PHP">
    ```php theme={null}
    $webhookSecret = 'your_webhook_secret';
    $body = file_get_contents('php://input');
    $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';

    $expectedSignature = hash_hmac('sha256', $body, $webhookSecret);

    if (!hash_equals($expectedSignature, $signature)) {
        http_response_code(401);
        exit('Invalid signature');
    }

    // Processing ...
    http_response_code(200);
    echo 'OK';
    ```
  </Tab>

  <Tab title=".NET (C#)">
    ```csharp theme={null}
    using System.Security.Cryptography;
    using System.Text;

    string body = await new StreamReader(Request.Body).ReadToEndAsync();
    string signature = Request.Headers["X-Webhook-Signature"];
    string secret = "your_webhook_secret";

    using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)))
    {
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(body));
        var expected = BitConverter.ToString(hash).Replace("-", "").ToLower();

        if (expected != signature)
        {
            return Results.Unauthorized();
        }
    }

    // Processing ...
    return Results.Ok();
    ```
  </Tab>

  <Tab title="Node.JS">
    ```js theme={null}
        const crypto = require('crypto');
        const express = require('express');
        const app = express();

        app.use(express.text({ type: 'application/json' }));

        app.post('/webhook', (req, res) => {
            const webhookSecret = 'your_webhook_secret';
            const signature = req.headers['x-webhook-signature'];
            const expected = crypto
            .createHmac('sha256', webhookSecret)
            .update(req.body)
            .digest('hex');

            if (signature !== expected) {
                return res.status(401).send('Invalid signature');
            }

            // Processing ...
            res.send('OK');
        });
    ```
  </Tab>
</Tabs>

## Supported Events

### Product

* `product.created`
* `product.updated`
* `product.deleted`
* `product.archived`

### Discount Codes

* `coupon.created`
* `coupon.updated`
* `coupon.deleted`
* `coupon.toggled`

### Customers

* `customer.created`
* `customer.updated`
* `customer.archived`
* `customer.unarchived`
* `customer.deleted`
* `customer.merged` — Triggered when multiple customers have been merged into one.
* `customer.parent.assigned` — Triggered when a parent customer was assigned in the [organization hierarchy](/en/guide/customers/organization-hierarchy).
* `customer.parent.removed` — Triggered when a parent customer was removed from the [organization hierarchy](/en/guide/customers/organization-hierarchy).
* `payment_method.created`
* `payment_method.updated`

### Invoices

* `invoice.created`
* `invoice.finalized` — Triggered after the invoice is finalized and payment processing has been initialized. The invoice will have status `STATUS_UNPAID` at this point.
* `invoice.credited`
* `invoice.updated`
* `invoice.refunded`
* `invoice.canceled`
* `invoice.paid`
* `invoice.closed`
* `invoice.unpaid`
* `invoice.remindered`
* `invoice.requires_approval`
* `invoice.payment_method.updated`
* `invoice.pdf.generation_requested`

### Price Plans

* `price_plan.archived`

### Subscriptions

* `subscription.activated`
* `subscription.billed`
* `subscription.created`
* `subscription.changed`
* `subscription.invoice_address.updated`
* `subscription.delivery_address.updated`
* `subscription.payment_method.updated`
* `subscription.item.instant_metered`
* `subscription.item.metered`
* `subscription.item.quantity_changed`
* `subscription.products.ordered`
* `subscription.trial_expired`
* `subscription.trial_extended`
* `subscription.updated`
* `subscription.paused`
* `subscription.voided`
* `subscription.resumed`
* `subscription.canceled`
* `subscription.cancellation_revoked`
* `subscription.terminated`
* `subscription.phase.created`
* `subscription.phase.completed`
* `subscription.transferred`
* `subscription_item.terminated`

### Dunning

* `dunning_document.created`
* `dunning_document.paid`
* `dunning_document.canceled`

### Cart

* `cart.completed`
* `cart.paid`

### Bank Accounts

* `bank_account.synced`

### Features & Entitlements

* `feature.created`
* `feature.activated`
* `feature.archived`
* `entitlement.state.updated`

***

## Notes

Events are retried **up to 3 times** in case of an error.
