# PostHog Webhooks

## When to Use This Skill

* How do I receive PostHog webhooks?
* How do I verify PostHog webhook signatures?
* What secret do I put in the PostHog Webhook destination "Signing secret" input?
* How do I handle `$pageview`, `$identify`, or custom events from PostHog?
* Why does PostHog signature verification fail behind Hookdeck or after a retry?

## How PostHog Signs Webhooks

The CDP **Webhook destination** has an optional **Signing secret**. When set, each request follows Standard Webhooks:

| Header | Value |
| --- | --- |
| `webhook-id` | Unique message ID (constant across retries of one message) |
| `webhook-timestamp` | Unix seconds when that attempt was signed |
| `webhook-signature` | `v1,<base64>` |

Signature: HMAC-SHA256 over `<webhook-id>.<webhook-timestamp>.<raw body>`, base64 encoded. Signing is documented only in PostHog's open-source code ([`standard-webhooks.ts`](https://github.com/PostHog/posthog/blob/master/nodejs/src/cdp/utils/standard-webhooks.ts)), not on the docs page. No signing secret means no signature headers.

### You choose the secret

PostHog does not issue one. It strips an optional `whsec_` prefix, base64-decodes the rest, and requires at least 24 decoded bytes. **Generate a `whsec_<base64>` secret and paste the same string into PostHog and your receiver (or Hookdeck):**

```bash
echo "whsec_$(openssl rand -base64 32)"

```

A bare base64 secret works in PostHog but fails Hookdeck, so always keep the `whsec_` prefix.

## Verification (core)

```javascript
const { Webhook } = require('standardwebhooks');

// rawBody MUST be the unparsed body (Express: express.raw({ type: '*/*' }))
const payload = new Webhook(process.env.POSTHOG_WEBHOOK_SECRET).verify(rawBody, {
  'webhook-id': req.headers['webhook-id'],
  'webhook-timestamp': req.headers['webhook-timestamp'],
  'webhook-signature': req.headers['webhook-signature'],
}); // throws on a bad signature or a timestamp older than 5 minutes

```

Python: `from standardwebhooks.webhooks import Webhook`, then `Webhook(secret).verify(raw_body, headers)`.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/posthog-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/posthog-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/posthog-webhooks/examples/fastapi/).

### Timestamp window and Hookdeck retries

The `standardwebhooks` and `svix` libraries **always enforce a fixed 5-minute timestamp window**. PostHog signs a fresh timestamp on each of its own attempts, but **Hookdeck retries and replays resend the original timestamp**, so the library rejects them once they are more than 5 minutes old. If you receive through Hookdeck and need late retries or replays to pass, use the manual verifier in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/posthog-webhooks/references/verification.md), where the window can be dropped or widened (deduplicate on `webhook-id` instead).

## Event Payload

The body is whatever the destination's template sends. The default is `{ "event": {...}, "person": {...} }`. Dispatch on `event.event`:

| `event.event` | Meaning |
| --- | --- |
| `$pageview` | Page viewed |
| `$identify` | User identified |
| `$autocapture` | Autocaptured click/input |
| `<custom name>` | Any custom event, e.g. `signed_up` |

See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/posthog-webhooks/references/overview.md).

## Environment Variables

```bash
POSTHOG_WEBHOOK_SECRET=whsec_your_base64_secret_here

```

## Local Development

```bash
npx hookdeck-cli listen 3000 posthog --path /webhooks/posthog

```

No account required. Use the printed Hookdeck URL as the PostHog destination URL. Use port `8000` for FastAPI.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/posthog-webhooks/references/overview.md) - What PostHog webhooks are, payload and events
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/posthog-webhooks/references/setup.md) - Create the destination, generate the secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/posthog-webhooks/references/verification.md) - Signature details, manual verifier, gotchas