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

# Receiving webhooks

> Outbound HTTP POST notifications when something changes inside FlowEstate. Headers, signature verification, retry policy, loop prevention.

FlowEstate sends webhooks when leads, projects, or units change. Subscribers register a target URL and an event type; FlowEstate fans out signed `POST` requests with a JSON body.

To **manage** subscriptions programmatically (create, list, delete), see the [Webhooks API reference](/api-reference/webhooks). This page covers what your receiver needs to do once a subscription is in place.

## Headers

Every delivery includes:

| Header                   | Description                                                                         |
| ------------------------ | ----------------------------------------------------------------------------------- |
| `Content-Type`           | Always `application/json`.                                                          |
| `User-Agent`             | `FlowEstate-Webhook/1.0`                                                            |
| `X-FlowEstate-Event`     | Event type, e.g. `lead.created`.                                                    |
| `X-FlowEstate-Delivery`  | UUID of this delivery attempt. Use it for idempotent processing.                    |
| `X-FlowEstate-Signature` | `sha256=<hex>` HMAC-SHA256 of the **exact raw body** using the endpoint secret.     |
| `X-FlowEstate-Source`    | Origin of the event: `ui`, `api`, or `system`. Use this to break loops — see below. |

## Body

```json theme={null}
{
  "id": "evt_<uuid>",
  "type": "lead.created",
  "createdAt": "2026-04-29T18:04:15.000Z",
  "organization": {
    "id": "org_...",
    "name": "Acme Realty",
    "slug": "acme-realty"
  },
  "data": { /* event-specific payload */ }
}
```

`data` is event-specific:

* `lead.*` events carry `data.lead`.
* `project.*` events carry `data.project`.
* `unit.*` events carry `data.unit` and `data.project`.
* `lead.note_added` carries `data.leadId` and `data.note`.
* `lead.communication_logged` carries `data.leadId` and `data.communication`.

For the full list of events see [Enums](/enums#webhookeventtype).

## Verifying the signature

Always verify the signature before trusting a payload. Anyone who knows your URL can hit it; only FlowEstate has the secret.

<CodeGroup>
  ```js Node.js theme={null}
  import crypto from "node:crypto";

  function verify(rawBody, signatureHeader, secret) {
    const expected = "sha256=" + crypto
      .createHmac("sha256", secret)
      .update(rawBody)
      .digest("hex");
    const a = Buffer.from(expected);
    const b = Buffer.from(signatureHeader);
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  }

  // Express
  app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
    if (!verify(req.body, req.get("X-FlowEstate-Signature"), process.env.FLOWESTATE_SECRET)) {
      return res.status(401).end();
    }
    const event = JSON.parse(req.body.toString("utf8"));
    // ... handle event ...
    res.status(200).end();
  });
  ```

  ```python Python theme={null}
  import hmac
  import hashlib

  def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
      expected = "sha256=" + hmac.new(
          secret.encode(),
          raw_body,
          hashlib.sha256,
      ).hexdigest()
      return hmac.compare_digest(expected, signature_header or "")
  ```

  ```php PHP theme={null}
  function flowestate_verify(string $rawBody, string $signatureHeader, string $secret): bool {
      $expected = "sha256=" . hash_hmac("sha256", $rawBody, $secret);
      return hash_equals($expected, $signatureHeader);
  }
  ```
</CodeGroup>

<Warning>**Sign the raw bytes** of the request body. Re-serializing the parsed JSON will produce a different signature because key order, whitespace, and number formatting may differ.</Warning>

The secret comes from the response of [`POST /webhooks/subscriptions`](/api-reference/webhooks#create-a-subscription) and is shown **only once**. If you lose it, delete the subscription and create a new one.

## Acknowledging

Return any `2xx` HTTP status to acknowledge receipt. FlowEstate doesn't read the response body — empty `200` is fine.

If your handler does heavy work (calling other APIs, writing to a slow database), **acknowledge first, process later**:

```js theme={null}
app.post("/webhook", (req, res) => {
  if (!verify(...)) return res.status(401).end();
  res.status(200).end();          // ACK first
  enqueueForBackgroundProcessing(req.body);  // process out-of-band
});
```

The delivery timeout is a few seconds — slow handlers cause spurious retries.

## Retry policy

Failed deliveries (network error, timeout, or HTTP `≥ 400`) are retried with this backoff:

| Attempt | Delay before retry |
| ------- | ------------------ |
| 1       | — (initial)        |
| 2       | 1 minute           |
| 3       | 5 minutes          |
| 4       | 30 minutes         |
| 5       | 2 hours            |
| 6       | 12 hours           |

After five failed attempts the delivery is marked `failed` and won't be retried again. The `X-FlowEstate-Delivery` header stays the same across retries — use it as your idempotency key when processing.

## Preventing loops

When you write to FlowEstate via this REST API, the resulting webhook fans out with `X-FlowEstate-Source: api`. If your receiver also writes back into FlowEstate, **drop events with source `api`** to avoid an infinite loop.

```js theme={null}
if (req.headers["x-flowestate-source"] === "api") {
  return res.status(200).end();  // ack and ignore
}
```

The other source values:

* `ui` — change made by a user clicking in the FlowEstate dashboard.
* `system` — change made by an internal worker (assignments, automations).
* `api` — change made through this REST API (your own writes, partner platforms).
