---
title: PostHog
---
# PostHog

Svix can deliver webhooks directly to [PostHog](https://posthog.com), without your customers having to set up any listener endpoint or write any glue code. Each batch of webhooks is sent to PostHog's [`/batch/` capture endpoint](https://posthog.com/docs/api/capture).

When **Advanced Destinations** are [enabled](/advanced-destinations#enabling-advanced-destinations), your customers will see the option to use a PostHog destination in the [App Portal](/app-portal).

![PostHog Destination Create](/img/advanced-destinations/posthog-create.png)

They will be able to configure the connection right in the App Portal:

- `domain` — the PostHog ingestion domain. This is `https://us.i.posthog.com` for US Cloud, `https://eu.i.posthog.com` for EU Cloud, or a self-hosted domain. Note the `.i.` in the Cloud hosts — `us.posthog.com` and `eu.posthog.com` are different, private endpoints and won't work here.
- `projectToken` — the PostHog project API key. Svix sends it as the `api_key` field in the body of each request.

<Callout type="info">
PostHog destinations require a transformation. The transformation is what builds the PostHog event batch, and in particular it's where you set each event's `distinctId`. PostHog uses `distinctId` to attribute events to a person and rejects events that don't have one, so it can't be left unset.
</Callout>

If every webhook in a batch is timestamped more than 48 hours in the past, Svix sends the batch with PostHog's `historical_migration` flag set. This keeps backfills (for example, after a destination has been disabled and re-enabled) from tripping PostHog's [spike detection](https://posthog.com/docs/billing/spike-detection). Nothing needs to be configured for this to happen.

# Transformations

Each webhook is mapped to a [PostHog event](https://posthog.com/docs/data/events). A transformation returns the `batch` of events to send.

```JavaScript
/**
 * @param input - The input object
 * @param input.events - The array of webhooks in the batch. The number of webhooks in the batch is capped by the destination's batch size.
 * @param input.events[].payload - The message payload (string or JSON).
 * @param input.events[].eventType - The message event type (string).
 * @param input.events[].createdAt - The ISO 8601 timestamp the webhook was received at (string).
 *
 * @returns Object containing the batch of events sent to PostHog's /batch/ endpoint.
 * @returns returns.batch - An array of PostHog events (https://posthog.com/docs/data/events).
 * @returns returns.batch[].event - The name of the event sent to PostHog.
 * @returns returns.batch[].distinctId - REQUIRED. The unique identifier of the person the event
 *                                       belongs to. It cannot be inferred from the event, so you
 *                                       must set it, usually from a field in the payload. PostHog
 *                                       rejects events that have no distinctId.
 * @returns returns.batch[].timestamp - An optional ISO 8601 timestamp for the event. If omitted, PostHog uses the time it receives the event.
 * @returns returns.batch[].properties - A map of properties associated with the event (https://posthog.com/docs/data/events#event-properties).
 */
function handler(input) {
  const batch = input.events.map((event) => ({
    event: event.eventType,
    // REQUIRED. PostHog uses distinctId to attribute events to a person and rejects events that
    // have none. Replace this with a value from your payload.
    distinctId: undefined,
    timestamp: event.createdAt,
    properties: event.payload,
  }));

  return { batch };
}
```

`input.events` is the list of webhooks received by the destination, processed in batches.

The transformation maps each webhook onto a [PostHog event](https://posthog.com/docs/data/events). Most of the fields map over cleanly — `event` from the event type, `timestamp` from when the webhook was received, and `properties` from the payload. The one field you have to supply yourself is `distinctId`, since it's specific to how you identify people in PostHog.

For example, if the destination receives the following messages:

```json
{
  "eventType": "user.signed_up",
  "payload": "{\"userId\": \"usr_123\", \"plan\": \"pro\"}"
}
```

```json
{
  "eventType": "user.signed_up",
  "payload": "{\"userId\": \"usr_456\", \"plan\": \"free\"}"
}
```

A transformation that sets `distinctId` from the payload's `userId`:

```JavaScript
function handler(input) {
  const batch = input.events.map((event) => ({
    event: event.eventType,
    distinctId: event.payload.userId,
    timestamp: event.createdAt,
    properties: event.payload,
  }));

  return { batch };
}
```

would result in the following request being made to PostHog's `/batch/` endpoint:

```json
{
  "api_key": "phc_*******",
  "batch": [
    {
      "event": "user.signed_up",
      "distinct_id": "usr_123",
      "timestamp": "2025-07-21T14:23:17.861Z",
      "properties": { "userId": "usr_123", "plan": "pro" }
    },
    {
      "event": "user.signed_up",
      "distinct_id": "usr_456",
      "timestamp": "2025-07-21T14:23:19.204Z",
      "properties": { "userId": "usr_456", "plan": "free" }
    }
  ]
}
```

These events would subsequently be visible in your PostHog dashboard.

![PostHog-Create](/img/stream/posthog-dashboard.png)
