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

# Webhooks

> Receive and verify signed terminal generation events.

Pass a public HTTPS `webhook_url` when you submit a generation. Porn Factory AI
delivers `generation.completed` or `generation.failed` after the request reaches
a terminal state.

```json theme={"system"}
{
  "webhook_url": "https://example.com/webhooks/pornfactoryai"
}
```

Webhook endpoints must resolve to a public network address. Redirects, private
IP ranges, loopback targets, and local hostnames are rejected.

## Delivery headers

* `X-PF-Event-Id`: stable event ID for deduplication
* `X-PF-Timestamp`: Unix timestamp used in the signature
* `X-PF-Signature`: `sha256=` followed by a hexadecimal HMAC digest

## Verify the signature

The signed value is `{timestamp}.{raw_body}`. Use the `whsec_` secret shown
when the API key was created.

```javascript theme={"system"}
import crypto from "node:crypto";

export function verifyWebhook({ body, signature, timestamp, secret }) {
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > 300) return false;

  const expected = `sha256=${crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${body}`)
    .digest("hex")}`;

  const left = Buffer.from(signature);
  const right = Buffer.from(expected);
  return left.length === right.length && crypto.timingSafeEqual(left, right);
}
```

Verify the raw body before parsing JSON. Reject timestamps older than five
minutes and deduplicate by `X-PF-Event-Id`.

## Retry behavior

Delivery is at least once. Failed deliveries are retried with exponential
backoff for up to eight attempts. Return a `2xx` response only after the event
has been durably recorded. Your handler must be idempotent.
