Skip to main content
The Goal: Give your Agentic pipeline real-time user interaction context. Think of this like setting up a live camera feed so your AI knows exactly what your users are doing
If you already use PostHog to capture events, register using your PostHog project id as the product_id and skip to step 3You 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.

Step 1 — Go to the project page in Autoplay and press “create one with Autoplay”

First, you need to grab the unique IDs that tell our system who you are.
  1. Go to app.autoplay.ai and sign up.
  2. Inside your dashboard, find your Product ID and API Key. Keep this page open—you will need both of these in the next step!
Note: Save your product ID as this will be important for product registration with the SDK!

Step 2 — Add the front end snippet to capture events

Next, we are going to add a tiny piece of tracking code (powereed by PostHog) to your website. This acts as the “eyes” that see what users are clicking on. Copy the code snippet below into your website’s frontend. Make sure to replace YOUR_API_KEY and YOUR_PRODUCT_ID with the badges you found in Step 1. JavaScript
import posthog from 'posthog-js'

posthog.init('YOUR_AUTOPLAY_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) => {
        posthog.identify(posthog.get_distinct_id(), {
            // Tells Autoplay where to send your events:
            product_id: 'YOUR_PRODUCT_ID', 
        });
    },
})
Highly Recommended: Tag users when they log in If a user logs into your app, you can tell Autoplay who they are. This automatically links their web activity to their chatbot conversations, no extra work required. Just run this code right after your login process finishes:
posthog.identify(user.id, {
    product_id: 'YOUR_AUTOPLAY_PRODUCT_ID',
    email: user.email, // Passing the email is optional, but it makes tracking much smarter!
})
(Note: If you don’t provide an email,anonymous users will still be tracked.)
👋 Quick Tip: Once you add this code to your site, jump into our Slack workspace and drop a message in #just-integrated. We will check to make sure your data is flowing properly and help you get fully set up!

Step 3 — Registering 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. Registration requires a valid contact email (stored on your connector product row) in addition to your product id. Existing deployments may still have older product rows without email until you re-register.
If you already use PostHog, register using your PostHog project id as the product_idYou 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.
Run the following script: 1. Install the Toolkit (Requires Python 3.10+) Open your computer’s terminal and install our SDK using either command: Bash
pip install autoplay-sdk
# OR
uv add autoplay-sdk
2. Run the Registration Script Create a Python file with the code below. Paste your Product ID from Step 1 into the script and run it. Python
import asyncio
from autoplay_sdk.admin import onboard_product

async def main() -> None:
    result = await onboard_product(
        "YOUR_AUTOPLAY_PRODUCT_ID",
        contact_email="you@yourcompany.com",
        print_operator_summary=True,
    )
    # result still has the same full fields if you need them in code

asyncio.run(main())
This will print the following fields:
  • product_id: {product_id_entered}
  • webhook_url: https://event-connector-luda.onrender.com/webhook/{product_id}
    stream_url: https://event-connector-luda.onrender.com/stream/{product_id}
  • webhook_secret: {secret}
    unkey_key: {secret}
Important: onboard_product registers an event_stream product. result includes your webhook, stream, and auth values—save what prints in the terminal.
  • Step 4 (PostHog): result.webhook_url, result.webhook_secret
  • Step 5 (live stream): result.stream_url, result.unkey_key (use as Bearer)
Re-registering your product
  • A second onboard_product with the same product_id returns 409 until overwrite is allowed.
  • Pass force_reregister=True (same as CLI --force-reregister). You must still pass contact_email on every registration, including overwrites.
  • After a successful overwrite, the webhook secret rotates. Update PostHog (Step 4) so X-PostHog-Secret matches the new secret.

Step 4 — Set up your PostHog webhook

Now we must tell the website tracker (Step 2) to send its data to the secure address (webhook) you just generated (Step 3). You have two choices: Option A — Managed (default)
  • Join our Slack workspace and message #just-integrated.
  • We configure the PostHog webhook for you.
  • You receive a 1Password link with your Stream URL and API token for Step 5.
Option B — Self-serve (DIY)
  • In PostHog, add a Webhook destination.
  • Webhook URL: paste result.webhook_url from Step 3.
  • X-PostHog-Secret header: paste result.webhook_secret from Step 3. Do not create a new secret.
PostHog webhook setup walkthrough
Below is the code to add in the source code
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 ?? '',
    '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': 'dZG-Clgaeqa-fg-MLdHAJ3xKfYmcOdRRk7KadkEyIoo'
}

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

let url := 'https://event-connector-luda.onrender.com/webhook/{product_id}'

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 5 — Watch Your Data Arrive Live! (Receive your first event)

Everything is wired up! Let’s turn on the monitor to see a live feed of your users’ actions. Run this final script. If you used the DIY method in Step 4, use the Stream URL from Step 3 and your Unkey token. If we helped you in Slack, use the stream URL and API token from Autoplay’s 1Password handoff. Then run the script, it connects to the SSE endpoint and prints every event as it arrives. Python
import asyncio
from autoplay_sdk import AsyncConnectorClient

# Paste your Stream URL and Token here!
STREAM_URL = "https://your-connector.onrender.com/stream/YOUR_PRODUCT_ID"
API_TOKEN  = "unkey_xxxx..."

def on_actions(p):
    print("\n=== LIVE USER ACTIONS ===")
    print(f"  Session ID   : {p.session_id}")
    print(f"  User ID      : {p.user_id}")
    print(f"  Product ID   : {p.product_id}")
    print(f"  Total Clicks : {p.count}")
    print(f"  Time Received: {p.forwarded_at}")
    for i, action in enumerate(p.actions):
        print(f"  [{i}] Action: {action.title}")
        print(f"       Details: {action.description}")
        print(f"       Page:    {action.canonical_url}")

def on_summary(p):
    print("\n=== AI SUMMARY ===")
    print(f"  Session ID   : {p.session_id}")
    print(f"  Product ID   : {p.product_id}")
    print(f"  Summarizes   : {p.replaces} actions")
    print(f"  Summary text : {p.summary}")

async def main():
    async with AsyncConnectorClient(url=STREAM_URL, token=API_TOKEN) as client:
        client.on_actions(on_actions)
        client.on_summary(on_summary)
        print("Listening for live website clicks... (Press Ctrl+C to stop)")
        await client.run()

asyncio.run(main())
(Note: If your internet drops, the client will reconnect automatically. Press Ctrl+C to stop listening.) What you will see: When you click around your app, your terminal will instantly populate with an AI summary of exactly what you are doing, looking something like this: Plaintext
=== ACTIONS ===
  session_id   : ps_abc123
  user_id      : user_xyz
  product_id   : acme-corp
  count        : 3
  forwarded_at : 1736940685.103
  [0] title        : Page Load: Dashboard
       description  : User landed on the main Dashboard page
       canonical_url: <https://app.example.com/dashboard>
  [1] title        : Click Export CSV
       description  : User clicked the Export CSV button
       canonical_url: <https://app.example.com/dashboard>
  [2] title        : Click Settings
       description  : User clicked the Settings link in the sidebar
       canonical_url: <https://app.example.com/dashboard>

=== SUMMARY ===
  session_id   : ps_abc123
  product_id   : acme-corp
  replaces     : 12 actions
  forwarded_at : 1736940750.881
  summary      : User explored the Dashboard, exported a CSV, and navigated to billing settings.

Next Steps & Advanced Features

Now that your data is flowing, here’s how to put it to work:
  • Typed Payloads: Explore all available fields for Actions and Summaries.
  • Inject into your LLM Response: Pass real-time user events into your prompt so the LLM understands what a user just did—not just what they asked. Most users are in the wrong part of your product when they ask for help; this is what lets your copilot bridge that gap.
  • Agentic RAG: Build agents that track where a user is, where they need to be, and guide them there—“You’ve been on this page for a few minutes—here’s where you actually need to go.”
  • Async Client: Use Autoplay alongside LangChain or FastAPI.

Typed payloads

Explore all fields on ActionsPayload and SummaryPayload

RAG pipeline

Embed events into a vector store in real time

Async client

Use AsyncConnectorClient with LangChain or FastAPI

Proactive copilot

Combine real-time events, memory, and golden paths
For structured logging and extra field conventions used across the SDK, see Logging. Release history is on the Changelog.