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

# PostHog

> Stream PostHog events into Greenflash with one webhook destination — no app code, no SDKs, no extra instrumentation.

If you already track `signup`, `purchase`, `subscribe`, `churn`, or any other product event in PostHog, you can pipe those events into Greenflash in real time. Your existing PostHog instrumentation becomes the outcome layer for your AI conversations — Greenflash uses the events to figure out which conversations correlate with revenue, retention, and churn.

## How it works

PostHog has a built-in **HTTP Webhook** destination that POSTs a JSON-templated request for every event you forward. Greenflash gives you a per-product URL to paste into that destination. From there:

1. A user action fires an event in PostHog (`$identify`, `purchase`, your own custom event, etc.).
2. PostHog's destination filters decide whether to forward it.
3. PostHog POSTs the event to your Greenflash URL.
4. Greenflash authenticates the URL token, maps the event to a Greenflash business event, infers whether it's a positive / negative / neutral outcome, and attaches it to the product you connected.
5. The next time Greenflash analyzes a conversation, it has the downstream events as context — so "did this conversation lead to anything?" becomes answerable.

No code in your app changes. PostHog stays the source of truth for events; Greenflash just consumes a copy of the ones you choose to forward.

## Before you start

* **A Greenflash product** you want to enrich with PostHog events. Create one under **Settings → Products** if you don't have one yet.
* **A PostHog project with admin access** so you can create a destination in the data pipeline.
* (Optional) A list of the events you want to forward. Forwarding everything works but burns destination volume on noise like `$pageview` and `$autocapture`.

## Setup

### 1. Create the connection in Greenflash

1. Open **Settings → Connect → Events** in the Greenflash web app.
2. Click **Connect** on the PostHog card.
3. Pick the product the stream should feed and click **Create source**.
4. Copy the destination URL. Greenflash shows it once. If you lose it, rotate the token from the connection row and update PostHog.

<Tip>
  The URL is the secret. Anyone with the URL can write events into the connected product — treat it like an API key, store it in PostHog and a secrets manager only.
</Tip>

### 2. Add the destination in PostHog

1. In PostHog, open **Data pipeline → Destinations**.
2. Search for **HTTP Webhook** (described as *"Sends a webhook templated by the incoming event data"*) and click **Create**.
3. Paste the Greenflash URL into the destination URL field. Leave the HTTP method as `POST`.
4. (Optional but recommended) Under **Filters**, exclude `$pageview`, `$pageleave`, `$autocapture`, and `$feature_flag_called`, and allow only the business events you care about — typically signup, activation, purchase, upgrade, cancel, refund. PostHog forwards everything by default, so this is where you control volume.
5. Save and enable the destination.

The connection row in Greenflash flips from **Awaiting first event** to **Active** the moment the first event lands.

### 3. Send a test event

Fire the event from PostHog or post directly to the URL to confirm the round trip:

```bash theme={"theme":{"light":"github-light","dark":"vesper"}}
curl -X POST "<YOUR_GREENFLASH_URL>" \
  -H "Content-Type: application/json" \
  -d '{
    "event": {
      "event": "purchase",
      "distinct_id": "user_test_123",
      "timestamp": "2026-05-18T12:00:00Z",
      "uuid": "00000000-0000-0000-0000-000000000001",
      "properties": {
        "$revenue": 49.0,
        "plan": "pro"
      }
    }
  }'
```

A successful request returns:

```json theme={"theme":{"light":"github-light","dark":"vesper"}}
{ "received": 1, "accepted": 1, "dropped": 0 }
```

If `accepted` is `0` and `dropped` is `1`, the event was filtered by your connection's allowlist or denylist. If you get `401`, the URL token is wrong (or has been rotated). See [Troubleshooting](#troubleshooting).

## How events are mapped

Greenflash applies these defaults to every PostHog event:

| PostHog field                      | Greenflash field                                        |
| ---------------------------------- | ------------------------------------------------------- |
| `event.event`                      | `eventType`                                             |
| `event.distinct_id`                | `externalUserId`                                        |
| `event.timestamp`                  | `eventAt`                                               |
| `event.uuid`                       | `insertId` (prefixed `posthog:`) — used for idempotency |
| `event.properties.$revenue`        | `value` (with `valueType: currency`)                    |
| `event.properties` (entire object) | `properties` (preserved verbatim on the event row)      |

### Identity hints (used for user & organization attribution)

Greenflash inspects a handful of `$set` fields to attach the event to the right user and organization. Sending these from PostHog means Greenflash can auto-create the user / org if it hasn't seen them yet, instead of leaving the event unattached.

| PostHog field                                                                            | Greenflash field         |
| ---------------------------------------------------------------------------------------- | ------------------------ |
| `event.properties.$set.email` (or top-level `email`)                                     | `userHints.email`        |
| `event.properties.$set.first_name` + `$set.last_name` (or `$set.name`)                   | `userHints.name`         |
| `event.properties.$groups.organization` or `$groups.company` (or `$set.organization_id`) | `externalOrganizationId` |
| `event.properties.$set.organization_name` (or `$set.company`)                            | `organizationHints.name` |

> Greenflash never reads `clerk_user_id` — Clerk IDs are a Greenflash-internal identifier, not a trustworthy field to receive from an analytics provider.

## Attribution: how events tie to users, orgs, and conversations

**Users.** Events match an existing Greenflash user in this order: by `externalUserId` (= PostHog `distinct_id`), then by `email`. If no match exists *and* the event carries an `email`, Greenflash **upserts** a new user automatically using the `$set` fields above — anonymous events (no email) keep `userId = null` instead of bloating the user table.

**Organizations.** Same idea, scoped to `externalOrganizationId`. We never match orgs by name (collisions across customers make it unsafe), so the org upsert path only runs when `$groups.organization` (or equivalent) is present.

**Conversations.** Attribution falls through three layers:

1. **Explicit.** Add `gf_conversation_id` to the PostHog event's properties to attach it to a specific Greenflash conversation (matched by `externalConversationId`). Always wins when set.
2. **Implicit recency fallback.** If no `gf_conversation_id` is supplied and we resolved the user, the event attaches to the user's most-recent conversation in this product within the last **15 minutes**.
3. **Unattached.** If neither holds, the event is stored against the product (and user, when known) but `conversationId` stays `null`.

> Users and orgs auto-created from analytics carry `properties.created_from = 'analytics'` and `properties.source = 'posthog'` so they're filterable downstream. When you later identify them via the Greenflash SDK using the same `externalUserId`, the existing row is reused (no duplicates).

### Influence (positive / negative / neutral)

Greenflash classifies the *kind* of outcome each event represents so analyses can tell wins from losses:

* **Positive** when `$revenue > 0`, *or* the event name matches `signup`, `signed_up`, `upgrade`, `purchase`, `subscribe`, `convert`, `activate`, `complete`, or `order_completed`.
* **Negative** when `$revenue < 0`, *or* the event name matches `churn`, `cancel`, `unsubscribe`, `refund`, `downgrade`, `delete`, `failed`, or `error`.
* **Neutral** otherwise.

Matching is case-insensitive and works across `snake_case`, `kebab-case`, `PascalCase`, and `Title Case With Spaces`.

## Override the defaults

Need to ship a non-standard event name or pin a custom influence? Add any of these *magic properties* on the PostHog side and Greenflash will respect them — no Greenflash-side configuration required.

| Property                  | Effect                                                                               |
| ------------------------- | ------------------------------------------------------------------------------------ |
| `gf_event_type`           | Override the Greenflash event name (the original PostHog name stays in `properties`) |
| `gf_influence`            | Force `positive`, `negative`, or `neutral`                                           |
| `gf_quality_impact_score` | Numeric override from `-1.0` to `1.0` — fine-grained weight                          |
| `gf_value`                | Override the event value (string or number)                                          |
| `gf_value_type`           | One of `currency`, `numeric`, `text`, `boolean`                                      |
| `gf_conversation_id`      | Attach this event to a specific Greenflash conversation (`externalConversationId`)   |

Example: forward a custom retention event but tell Greenflash to treat it as a positive outcome with a tighter weight.

```js theme={"theme":{"light":"github-light","dark":"vesper"}}
posthog.capture('weekly_active_milestone', {
  weeks: 4,
  gf_influence: 'positive',
  gf_quality_impact_score: 0.6,
});
```

## Filtering: what to forward

**Filter on the PostHog side, not inside Greenflash.** PostHog ships a rich filter builder with property matching, regex, and per-event triggers — anything we'd build into Greenflash would be a strict downgrade. We deliberately don't expose a denylist UI: the only Greenflash-side knob is the magic-property overrides above, applied per event in the payload.

A good rule of thumb: forward business events, drop telemetry. Out of the box PostHog sends everything including `$pageview`, `$pageleave`, `$autocapture`, and `$feature_flag_called` — most of that is noise for outcome analysis and costs you destination volume. Configure the HTTP Webhook destination's **Filters** to allow only the events that signal real outcomes.

<AccordionGroup>
  <Accordion title="SaaS conversion funnel" icon="dollar-sign">
    Forward `signup`, `trial_started`, `activated`, `subscription_started`, `subscription_upgraded`, `subscription_cancelled`. Greenflash will infer influence from the names — no magic properties needed. Use this to answer "which AI conversations led to a paid conversion vs. a cancel?"
  </Accordion>

  <Accordion title="E-commerce" icon="shopping-cart">
    Forward `order_completed`, `refund_issued`, `cart_abandoned`. Include `$revenue` on each so Greenflash can dollar-weight outcomes per conversation. Refunds should arrive with negative `$revenue`.
  </Accordion>

  <Accordion title="Product-led growth" icon="rocket">
    Forward feature-adoption milestones (`workspace_created`, `integration_connected`, `first_value_reached`). Tag them with `gf_influence: 'positive'` if the event name doesn't match Greenflash's defaults.
  </Accordion>

  <Accordion title="Hard-mute a stream temporarily" icon="circle-pause">
    Don't want to touch PostHog's filter config for a short outage or experiment? Pause the connection from its row in **Settings → Connect → Events**. The Greenflash receiver returns `204` while paused so PostHog stops retrying. Re-enable to resume — no token rotation, no PostHog change.
  </Accordion>
</AccordionGroup>

## Response codes & limits

| Code  | Meaning                                                                                                       |
| ----- | ------------------------------------------------------------------------------------------------------------- |
| `200` | Event received and processed. Body includes `received`, `accepted`, `dropped`.                                |
| `204` | Connection is paused or disconnected in Greenflash. Returned deliberately so PostHog doesn't retry.           |
| `400` | Payload didn't match the PostHog webhook schema (missing `event`, missing `distinct_id`, malformed JSON).     |
| `401` | URL token is invalid, the connection has been rotated, or the optional shared secret header is missing/wrong. |
| `404` | Provider segment of the URL is wrong (e.g. you pointed PostHog at the `/segment/` URL by mistake).            |
| `500` | Greenflash couldn't look up the connection. Safe to retry.                                                    |

PostHog's HTTP Webhook destination POSTs **one event per request**, so there's no batch size limit on this provider. Compare with Amplitude / Segment, which support up to 1,000 events per batch.

## Security

The URL is path-authenticated: the token segment is generated per connection, stored only as a `scrypt` hash, and verified in constant time on every request.

For defense in depth, you can configure a **shared secret** alongside the URL token. The secret is communicated to Greenflash on every request via a custom HTTP header — there's no body field or query parameter.

1. In PostHog, open your HTTP Webhook destination and go to **Headers**.
2. Add a header with name `x-greenflash-secret` and value `<your-secret>` (any string you pick, 8+ characters).
3. Provide the same secret to Greenflash on the connection so we can match it.

Once configured, Greenflash rejects any request whose `x-greenflash-secret` header is missing or doesn't match with `401`.

**Rotating the URL** invalidates the previous token immediately. Events sent to the old URL return `401`. Update the PostHog destination URL to resume delivery.

## Troubleshooting

<AccordionGroup>
  <Accordion title="Connection still says ''Awaiting first event''" icon="hourglass">
    Most often this is a destination filter dropping everything. Check:

    * The destination is **enabled** in PostHog.
    * The destination's filters aren't excluding all your events.
    * The URL has **no trailing slash** and matches exactly what Greenflash showed you.
    * PostHog's destination is firing — check the **Activity** tab on the destination in PostHog.
  </Accordion>

  <Accordion title="Events arrive but influence is wrong" icon="thumbs-down">
    Either rename the event to match one of the Greenflash defaults, or add `gf_influence` and `gf_quality_impact_score` to the event properties. You can do this in your `posthog.capture(...)` call or with a PostHog destination transformation.
  </Accordion>

  <Accordion title="Refunds aren't showing up as negative" icon="rotate-ccw">
    PostHog refund events need a negative `$revenue` value (e.g. `$revenue: -49.0`) for Greenflash to infer negative influence from the dollar amount. Alternatively set `gf_influence: 'negative'` directly.
  </Accordion>

  <Accordion title="Lost the URL" icon="key">
    Open the PostHog row under **Settings → Connect → Events** and click **Rotate URL**. The old token is invalidated immediately. Paste the new URL into the PostHog destination configuration.
  </Accordion>

  <Accordion title="Duplicate events" icon="copy">
    Greenflash deduplicates on the `posthog:<uuid>` insert id. As long as PostHog sends a consistent `uuid` per event, retries from PostHog won't create duplicates. If you're posting to the URL yourself, include a stable `uuid` per event.
  </Accordion>
</AccordionGroup>

## Next

<CardGroup cols={2}>
  <Card title="How Greenflash uses events" icon="activity" href="/features/events">
    What changes in your analyses once events start flowing.
  </Card>

  <Card title="Public Events API" icon="bolt" href="/features/public-api">
    Skip the webhook and post events directly if you want full control.
  </Card>
</CardGroup>
