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

# PostHog — How to setup

> Learn how to connect existing PostHog live user activity to your support AI agent using the Autoplay SDK.

## ⚡ Add this skill

<CardGroup cols={2}>
  <Card title="One command" icon="terminal">
    Add the Autoplay PostHog session replay provider skill for an existing PostHog setup.

    <CodeGroup>
      ```bash CLI theme={null}
      uvx --from autoplay-sdk autoplay-install-skills --user-activity posthog
      ```
    </CodeGroup>

    <a className="skill-card-link" href="/recipes/posthog/how-to-setup">View the docs →</a>
  </Card>

  <Card title="Agent onboarding" icon="robot">
    Fetch this skill when a customer already uses PostHog as a session replay provider and wants Autoplay live user activity.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -s https://developers.autoplay.ai/activity-posthog/SKILL.md
      ```
    </CodeGroup>

    <a className="skill-card-link" href="https://developers.autoplay.ai/activity-posthog/SKILL.md" target="_blank">View the skill →</a>
  </Card>
</CardGroup>

**Prerequisite:** install the SDK first — see [Quickstart](/quickstart).

<Tip>
  If you **already use PostHog** to capture events, register using your PostHog project id as the **`product_id`** and skip to Step 2

  You can find your PostHog Project ID (often referred to as a Team ID) through the following methods:

  * **URL:** The easiest way to find it is to look at the URL while logged into your project. The numeric value following /project/ in the address bar is your Project ID.
  * **Project Settings:** Navigate to Project Settings in the PostHog sidebar. The ID is typically listed under the general project configuration or API sections.
</Tip>

### 🎯 Step 1 — Set up PostHog in your app

Go to [posthog.com](https://posthog.com) and sign up or log in. Create a new project if you don't have one. Your **Project ID** is the number in the URL once you're inside the project:

```
https://us.posthog.com/project/YOUR_POSTHOG_PROJECT_ID/...
```

**📺 How to find your PostHog Project ID and Project API Key**

<iframe src="https://app.arcade.software/share/uQizURhG2MTNIxQjiIA1" title="Find your PostHog Project ID and Project API Key" width="100%" height="500" allow="fullscreen" style={{ borderRadius: "8px", border: "none" }} />

**Save your Project ID** — you will use it in Steps 2 and 3.

Now install the PostHog JavaScript SDK:

```bash theme={null}
npm install posthog-js
```

<Tip>
  **Where to find your PostHog Project API Key:** In PostHog, go to **Settings → Projects → \[Your Project] → General**. Copy the **Project API Key** (also labelled **Project Token** in the PostHog UI — they are the same value) — it starts with `phc_`. Do not use a Personal API Key (starts with `phx_`) — `posthog.init()` rejects those.
</Tip>

Copy the snippet below into your app's entry point (`main.ts`, `_app.tsx`, or root layout). Replace `YOUR_POSTHOG_PROJECT_API_KEY` with your API key from the Tip above, and `YOUR_POSTHOG_PROJECT_ID` with the project ID you saved above:

```javascript theme={null}
import posthog from 'posthog-js'

posthog.init('YOUR_POSTHOG_PROJECT_API_KEY', {
    api_host: 'https://us.i.posthog.com',
    person_profiles: 'identified_only',
    // This stops recording if a user walks away for 2 minutes, keeping your data clean:
    session_idle_timeout_seconds: 120, 
    loaded: (posthog) => {
        // Attach product_id to every event as a super-property. This is NOT an
        // identify call, so it never creates an anonymous "identified" user.
        // Must match onboard_product(product_id=...).
        posthog.register({ product_id: 'YOUR_POSTHOG_PROJECT_ID' });
    },
})
```

**Required: identify users on login.** The moment your auth flow knows who the user is, pass **your own user id** (never the anonymous `posthog.get_distinct_id()`). This makes PostHog's `distinct_id` equal your app's user id — so the same id reaches Autoplay as `user_id` — and links earlier anonymous activity to the identified person:

```javascript theme={null}
// On login:
posthog.identify(user.id, {
    product_id: 'YOUR_POSTHOG_PROJECT_ID',
    email: user.email, // Added to the person profile and the $identify event.
})

// On logout — clears identity so the next user starts clean:
posthog.reset()
```

<Warning>
  Don't call `posthog.identify(posthog.get_distinct_id(), …)` — that re-stamps
  the anonymous id and never sets a real user id. Always pass your app's stable
  user id. Until a user logs in they stay anonymous (that's expected); the
  `session_id` still scopes everything.
</Warning>

> **👋 Quick Tip:** Once you add this code to your site, jump into our [Discord](https://discord.gg/jCbR2tQA5) and say hi — we will check your data is flowing and help you get fully set up!

<Note>
  **Identity plumbing for widget-based support AI agents:** make sure the same user identity flows across all three layers: PostHog `distinct_id` / `user_id`, your chat widget session metadata, and the support AI agent backend sender identifier. If those do not match, chat replies will look like "no recent activity" because events are stored under one key and fetched with another.
</Note>

***

### 📝 Step 2 — Register your product with Autoplay

Now that your website is tracking clicks, we need to create a secure "mailing address" (**Webhook URL**) and a shared **secret** (`X-PostHog-Secret`) so that data can be safely sent to Autoplay.

The `autoplay-sdk` was installed on the [Quickstart](/quickstart) page. Create a Python file with the script below, replace the placeholders with your values from Step 1, and run it once:

```python theme={null}
import asyncio
from autoplay_sdk.admin import onboard_product
from autoplay_sdk.providers import PostHogProvider

async def main() -> None:
    result = await onboard_product(
        "YOUR_POSTHOG_PROJECT_ID",    # your PostHog project ID from Step 1
        contact_email="you@yourcompany.com",  # replace with your actual email
        user_activity_provider=PostHogProvider(),
        print_operator_summary=True,
    )

asyncio.run(main())
```

This will print the following fields:

* **product\_id:** `YOUR_POSTHOG_PROJECT_ID`
* **provider:** `posthog`
* **ingest\_url:** `https://connector.autoplay.ai/ingest/YOUR_POSTHOG_PROJECT_ID`
* **ingest\_secret:** `{secret}` — PostHog sends this as the `X-PostHog-Secret` header
* **mcp\_url:** `https://mcp.autoplay.ai/mcp`
* **mcp\_key:** `{secret}` — your agent's Bearer token

**Save what prints in the terminal** — you will need these values in Step 3 and Step 4:

* **Step 3 (PostHog webhook):** use the `ingest_url` and `ingest_secret` printed above
* **Step 4 (read live activity):** use the `mcp_url` and `mcp_key` printed above (`mcp_key` is your Bearer token)

<Note>
  **Re-registering your product**

  * A second `onboard_product` with the same `product_id` returns **409** until overwrite is allowed.
  * Pass **`force=True`**. You must still pass **`contact_email`** on every registration, including overwrites.
  * After a successful overwrite, the **`ingest_secret` rotates**. Update PostHog (**Step 3**) so **`X-PostHog-Secret`** matches the new secret.
</Note>

***

### 🔗 Step 3 — Set up your PostHog webhook

Now we must tell the website tracker (Step 1) to send its data to the secure address (webhook) you just generated (Step 2).

**You have three choices:**

**Option A — Automated with the SDK (recommended)**

Let the SDK create and verify the destination for you — no clicking around in
PostHog, no pasting Hog code. Run this once with the values from Step 2:

<Tip>
  **Where to find your PostHog Personal API Key:** In PostHog, go to **Settings → \[Your name] → Personal API keys → Create personal API key**. Give it `project:read` and `hog_function:write` permissions. This is different from the Project API Key used in `posthog.init()`.
</Tip>

**📺 Generate your PostHog Personal API Key**

<iframe src="https://app.arcade.software/share/lOvmHy4FeRDtjD9xBTBb" title="Generate your PostHog Personal API Key" width="100%" height="500" allow="fullscreen" style={{ borderRadius: "8px", border: "none" }} />

```python theme={null}
import asyncio
from autoplay_sdk.providers import PostHogProvider

async def main() -> None:
    provider = PostHogProvider()
    dest = await provider.create_destination(
        host="https://us.posthog.com",             # your PostHog host (us.posthog.com or eu.posthog.com)
        project_id="YOUR_POSTHOG_PROJECT_ID",      # your project ID from Step 1
        personal_api_key="YOUR_PERSONAL_API_KEY",  # phx_... Personal API Key from the Tip above
        webhook_url="YOUR_INGEST_URL",             # ingest_url printed by Step 2
        webhook_secret="YOUR_INGEST_SECRET",       # ingest_secret printed by Step 2
    )
    status = await provider.verify(
        host="https://us.posthog.com",
        project_id="YOUR_POSTHOG_PROJECT_ID",
        personal_api_key="YOUR_PERSONAL_API_KEY",
        destination_id=dest.id,
    )
    print("destination", dest.id, "enabled:", status.ok)

asyncio.run(main())
```

It's **idempotent** — it creates the "Autoplay Event Stream" destination (or
updates it) and confirms it's enabled, so you can re-run it safely.

**Option B — Managed**

* Join our [Discord](https://discord.gg/jCbR2tQA5) and say hi.
* We configure the PostHog webhook for you.
* You receive a **1Password** link with your `ingest_url`, `ingest_secret`, and `mcp_key`.

**Option C — Fully manual (advanced)**

Add the destination by hand in PostHog — only needed if you can't run Option A:

* In PostHog, add a **Webhook** destination.
* **Webhook URL:** paste the `ingest_url` printed by Step 2.
* **`X-PostHog-Secret` header:** paste the `ingest_secret` printed by Step 2. Do **not** create a new secret.

PostHog still requires the form-level **Webhook URL** field even if your Hog source code also sets `let url := ...`.

**PostHog webhook setup walkthrough**

<div
  style={{
position: "relative",
paddingBottom: "calc(52.9514% + 41px)",
height: 0,
width: "100%",
}}
>
  <iframe
    src="https://demo.arcade.software/EJMhT0lTf5CmhObVB4BE?embed&embed_mobile=tab&embed_desktop=inline&show_copy_link=true"
    title="Set Up a Webhook as a Data Destination"
    frameBorder={0}
    loading="lazy"
    allow="clipboard-write; fullscreen"
    style={{
  position: "absolute",
  top: 0,
  left: 0,
  width: "100%",
  height: "100%",
  colorScheme: "light",
}}
  />
</div>

Paste this into the **Event Body / Source** field of your PostHog webhook destination:

```text theme={null}
fun extractFromElementsChain(str, pattern) {
    try {
        if (empty(str)) {
            return ''
        }
        let startIdx := position(str, pattern)
        if (startIdx <= 0) {
            return ''
        }
        let sub := substring(str, startIdx + length(pattern), length(str) - startIdx - length(pattern) + 1)
        let endIdx := position(sub, '"')
        if (endIdx > 0) {
            return substring(sub, 1, endIdx - 1)
        }
        return ''
    } catch (err) {
        print(f'extractFromElementsChain error for pattern {pattern}:', err)
        return ''
    }
}


let elements_chain := event.elements_chain ?? ''

let element_id := ''
let input_field_name := ''
let link_destination := ''
let button_or_link_text := ''

try {
    element_id := extractFromElementsChain(elements_chain, 'attr__id="')
} catch (err) {
    print('Error extracting element_id:', err)
    element_id := ''
}
try {
    input_field_name := extractFromElementsChain(elements_chain, 'attr__name="')
} catch (err) {
    print('Error extracting input_field_name:', err)
    input_field_name := ''
}
try {
    link_destination := extractFromElementsChain(elements_chain, 'attr__href="')
} catch (err) {
    print('Error extracting link_destination:', err)
    link_destination := ''
}

try {
    button_or_link_text := extractFromElementsChain(elements_chain, 'text="')
} catch (err) {
    print('Error extracting button_or_link_text:', err)
    button_or_link_text := ''
}

let payload := {
    'event': event.event,
    'referrer': event.properties?.$referrer ?? '',
    'email': event.properties?.email ?? event.person?.properties?.email ?? '',
    'timestamp': event.timestamp ?? '',
    'element_id': element_id,
    'event_type': event.properties?.$event_type ?? '',
    'session_id': event.properties?.$session_id ?? '',
    'current_url': event.properties?.$current_url ?? '',
    'distinct_id': event.distinct_id ?? '',
    'elements_chain': elements_chain,
    'input_field_name': input_field_name,
    'link_destination': link_destination,
    'button_or_link_text': button_or_link_text
}


let headers := {
    'Content-Type': 'application/json',
    'x-posthog-secret': inputs.headers['x-posthog-secret']
}

let req := {
    'headers': headers,
    'body': jsonStringify(payload),
    'method': 'POST'
}

let url := inputs.url

if (inputs.debug) {
    print('Request payload', payload)
    print('Request', url, req)
}

let res := fetch(url, req)


if (res.status >= 400) {
    print('Webhook error response', res.status, res.body)
    throw Error(f'Webhook returned {res.status}: {res.body}')
}
if (inputs.debug) {
    print('Response', res.status, res.body)
}
```

***

### 📡 Step 4 — See your activity land

Everything is wired up! The connector is **pull-based** — instead of streaming, you (or your agent) ask for a user's recent activity the moment you need it. Let's confirm your events are landing.

Click around your app while logged in as an identified user, then fetch that user's activity with the `mcp_key` you saved from Step 2:

```bash theme={null}
curl "https://mcp.autoplay.ai/users/YOUR_POSTHOG_PROJECT_ID/YOUR_USER_ID/live-activity?limit=10" \
  -H "Authorization: Bearer YOUR_MCP_KEY"
```

* `YOUR_USER_ID` — the stable id you pass to `posthog.identify(...)`.
* `YOUR_MCP_KEY` — the `mcp_key` printed by Step 2.

**What you'll get back** — the user's recent footsteps, ordered oldest → newest:

```json theme={null}
{
  "product_id": "YOUR_POSTHOG_PROJECT_ID",
  "user_id": "user_12345",
  "count": 3,
  "as_of": 1736940750.881,
  "actions": [
    { "type": "pageview", "title": "Page Load: Dashboard", "description": "User landed on the dashboard page", "canonical_url": "https://app.example.com/dashboard", "index": 0 },
    { "type": "click", "title": "Click Export Csv", "description": "User clicked the Export Csv button", "canonical_url": "https://app.example.com/dashboard", "index": 1 },
    { "type": "submit", "title": "Submit Payment Form", "description": "User submitted the Payment form", "canonical_url": "https://app.example.com/checkout", "index": 2 }
  ]
}
```

A **`200` with a populated `actions` array** means your events are flowing. An **empty array** means the user identified but hasn't browsed yet (activity is built from `$pageview` / `$autocapture`), or the events haven't landed yet — click around and give it a few seconds.

<Note>
  This REST call returns the **exact same data your agent reads** — the agent just pulls it over **MCP** (the `get_live_user_activity` tool) instead of curl. That's the next step.
</Note>

***

### 🔌 Next: connect your AI support agent

Your activity is now flowing into the connector. Head back to Quickstart to choose your existing AI support agent and connect it via MCP — it then pulls a user's live activity on demand, the moment it needs context to answer.

<Card title="Choose your AI support agent" icon="comments" href="/quickstart">
  Fin (Intercom), Maven, Ada, Botpress, and more — pick yours and connect via MCP.
</Card>

For structured logging and `extra` field conventions used across the SDK, see [Logging](/sdk/logging). Release history is on the [Changelog](/changelog).
