# Receive generation webhooks

> Register an HTTPS endpoint, verify signed generation events, and handle retries without duplicate work.

Canonical: https://entirefeed.com/docs/operate/webhooks

Webhooks notify your integration when a generation changes state. Register only the generation events you need and continue to treat the generation resource as the current source of truth.

## Register a public HTTPS endpoint

Create an endpoint with an organization API key. The URL must use public HTTPS and must not resolve to a private or local address. The response returns a `whsec_...` signing secret once; store it securely.

## Verify before processing

Verify `EntireFeed-Signature` against the exact raw request body before parsing JSON. The signed message is the timestamp, a period, and the raw body using HMAC-SHA256. Also record `EntireFeed-Event-ID` and ignore duplicate event IDs.

## Return success promptly

Return any `2xx` response after durable receipt. Failed deliveries are retried with the same event ID. Make your handler idempotent and fetch the generation when your workflow needs the latest state.

Only the generation events listed in the [webhook reference](/docs/reference/webhooks) are currently supported.

## Register an endpoint

The endpoint response returns its `whsec_...` signing secret once.

### cURL

```bash
curl https://api.entirefeed.com/v1/webhooks/endpoints \
  -X POST \
  -H "Authorization: Bearer $ENTIREFEED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://example.com/webhooks/entirefeed",
  "events": [
    "generation.succeeded",
    "generation.failed",
    "generation.manual_review"
  ]
}'
```

### TypeScript

```typescript
const response = await fetch("https://api.entirefeed.com/v1/webhooks/endpoints", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.ENTIREFEED_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "url": "https://example.com/webhooks/entirefeed",
    "events": [
      "generation.succeeded",
      "generation.failed",
      "generation.manual_review"
    ]
  }),
});

if (!response.ok) throw new Error(await response.text());
console.log(await response.json());
```

### Python

```python
import os
import requests

response = requests.request(
    "POST",
    "https://api.entirefeed.com/v1/webhooks/endpoints",
    headers={"Authorization": f"Bearer {os.environ['ENTIREFEED_API_KEY']}", "Content-Type": "application/json"},
    json={
      "url": "https://example.com/webhooks/entirefeed",
      "events": [
        "generation.succeeded",
        "generation.failed",
        "generation.manual_review",
      ],
    },
    timeout=30,
)
response.raise_for_status()
print(response.json())
```

```json
{
  "id": "f419d0cf-5d8e-4b4c-82bb-6042b17e68f8",
  "url": "https://example.com/webhooks/entirefeed",
  "events": [
    "generation.succeeded",
    "generation.failed",
    "generation.manual_review"
  ],
  "secret": "whsec_store_this_value_once"
}
```

## List or deactivate endpoints

List endpoints:

### cURL

```bash
curl https://api.entirefeed.com/v1/webhooks/endpoints \
  -X GET \
  -H "Authorization: Bearer $ENTIREFEED_API_KEY"
```

### TypeScript

```typescript
const response = await fetch("https://api.entirefeed.com/v1/webhooks/endpoints", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.ENTIREFEED_API_KEY}`,
  },
});

if (!response.ok) throw new Error(await response.text());
console.log(await response.json());
```

### Python

```python
import os
import requests

response = requests.request(
    "GET",
    "https://api.entirefeed.com/v1/webhooks/endpoints",
    headers={"Authorization": f"Bearer {os.environ['ENTIREFEED_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
print(response.json())
```

Deactivate an endpoint. A successful response has no body:

### cURL

```bash
curl https://api.entirefeed.com/v1/webhooks/endpoints/f419d0cf-5d8e-4b4c-82bb-6042b17e68f8 \
  -X DELETE \
  -H "Authorization: Bearer $ENTIREFEED_API_KEY"
```

### TypeScript

```typescript
const response = await fetch("https://api.entirefeed.com/v1/webhooks/endpoints/f419d0cf-5d8e-4b4c-82bb-6042b17e68f8", {
  method: "DELETE",
  headers: {
    "Authorization": `Bearer ${process.env.ENTIREFEED_API_KEY}`,
  },
});

if (!response.ok) throw new Error(await response.text());
console.log(response.status);
```

### Python

```python
import os
import requests

response = requests.request(
    "DELETE",
    "https://api.entirefeed.com/v1/webhooks/endpoints/f419d0cf-5d8e-4b4c-82bb-6042b17e68f8",
    headers={"Authorization": f"Bearer {os.environ['ENTIREFEED_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
print(response.status_code)
```

## Verify the signature

Verify `EntireFeed-Signature` against the exact raw body before parsing JSON:

### TypeScript

```typescript
import { createHmac, timingSafeEqual } from "node:crypto";

const parts = Object.fromEntries(
  signatureHeader.split(",").map((part) => part.split("=", 2)),
);
const timestamp = Number(parts.t);
const received = Buffer.from(parts.v1 ?? "", "hex");
const expected = Buffer.from(
  createHmac("sha256", process.env.ENTIREFEED_WEBHOOK_SECRET!)
    .update(Buffer.concat([Buffer.from(String(timestamp) + "."), rawBody]))
    .digest("hex"),
  "hex",
);

const fresh = Math.abs(Date.now() / 1000 - timestamp) <= 300;
const valid =
  fresh &&
  received.length === expected.length &&
  timingSafeEqual(received, expected);
```

### Python

```python
import hashlib
import hmac
import os
import time

parts = dict(part.split("=", 1) for part in signature_header.split(","))
timestamp = int(parts["t"])
signed = f"{timestamp}.".encode() + raw_body
expected = hmac.new(
    os.environ["ENTIREFEED_WEBHOOK_SECRET"].encode(),
    signed,
    hashlib.sha256,
).hexdigest()

fresh = abs(time.time() - timestamp) <= 300
valid = fresh and hmac.compare_digest(parts["v1"], expected)
```

## Process deliveries idempotently

```json
{
  "id": "evt_0123456789abcdef0123456789abcdef",
  "type": "generation.succeeded",
  "created_at": "2026-07-12T08:31:14Z",
  "data": {
    "object": {
      "id": "gen_0123456789abcdef0123456789abcdef",
      "object": "generation",
      "status": "succeeded",
      "model": "seedance-2",
      "task": "video.create",
      "created_at": "2026-07-12T08:30:00Z",
      "completed_at": "2026-07-12T08:31:14Z",
      "output": {
        "assets": [
          {
            "id": "asset_0123456789abcdef0123456789abcdef",
            "type": "video",
            "url": "https://cdn.entirefeed.com/examples/product-reveal.mp4",
            "mime_type": "video/mp4",
            "expires_at": null
          }
        ]
      },
      "usage": {
        "amount_usd": "1.20",
        "line_items": [],
        "billing_status": "settled"
      },
      "error": null,
      "metadata": null
    }
  }
}
```

Persist `EntireFeed-Event-ID` before returning `2xx`. Repeated delivery of that ID must not repeat customer-side effects.

Requests time out after 10 seconds. Failed deliveries retry after 1m, 5m, 30m, 2h, 12h.
