Skip to main content

Install the SDK

Run these commands once from your project root:
pip install autoplay-sdk
# OR
uv add autoplay-sdk
Install Agent skills for Cursor/Claude so your assistant follows the correct integration pattern for your stack. See Pricing for plans.

πŸŽ₯ Step-1 Choose your existing session replay provider

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 Posthog and create a project

Your project ID is stored in the url: https://us.posthog.com/project/{project_id}/onboarding?next=%2Fhome

πŸ’» Step 2 β€” Add the front end snippet to capture events

Next, we are going to add a tiny piece of tracking code (powered by PostHog) to your website. This acts as the β€œeyes” that see what users are clicking on.Use your PostHog Project API Key (starts with phc_) here. Personal keys (phx_...) are admin keys and posthog.init() rejects them with personal_api_key.Copy the code snippet below into your website’s frontend. Make sure to replace YOUR_POSTHOG_PROJECT_API_KEY and YOUR_PRODUCT_ID with your real values.JavaScript
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_PRODUCT_ID' });
    },
})
Required: identify users with your app’s stable id 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 their earlier anonymous activity to the identified person:
// On login:
posthog.identify(user.id, {
    product_id: 'YOUR_AUTOPLAY_PRODUCT_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()
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.
πŸ‘‹ Quick Tip: Once you add this code to your site, jump into our Discord and say hi β€” we will check your data is flowing and help you get fully set up!
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.

πŸ“ 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.Your stream consumer can pass the bearer token directly to AsyncConnectorClient(token=...) or via AUTOPLAY_APP_UNKEY_TOKEN.
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.
Create a Python file with the script 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 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 3:
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
        project_id="YOUR_POSTHOG_PROJECT_ID",
        personal_api_key="phx_…",                # needs project:read + hog_function:write
        webhook_url="…",                          # result.webhook_url from Step 3
        webhook_secret="…",                       # result.webhook_secret from Step 3
    )
    status = await provider.verify(
        host="https://us.posthog.com",
        project_id="YOUR_POSTHOG_PROJECT_ID",
        personal_api_key="phx_…",
        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 and say hi.
  • We configure the PostHog webhook for you.
  • You receive a 1Password link with your Stream URL and API token for Step 5.
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 result.webhook_url from Step 3.
  • X-PostHog-Secret header: paste result.webhook_secret from Step 3. 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
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 ?? '',
    '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 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 on Discord, 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..."
# You can also set AUTOPLAY_APP_UNKEY_TOKEN in your environment.

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.
This output comes from the example script’s print(...) statements. SDK callbacks still receive typed dataclasses (ActionsPayload / SummaryPayload).

πŸ”Œ Next: choose your AI support agent β†’ connect via MCP

Your activity is now flowing into the connector. The next step is 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.
Connect Intercom Fin to live user activity via the MCP server β€” add the MCP connector, the Get Live User Activity tool, and Messenger JWT identity.Connect Fin via MCP ->
Coming soon β€” Maven will connect the same way, via the MCP server.

πŸš€ Other next steps

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 onboarding agent 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 onboarding agent

Combine real-time events, memory, and golden paths

Support AI agent tutorials

Step-by-step guides for Intercom, Ada, Botpress, and more

AI agent skills

Browse, install, and download Cursor / Claude skills
For structured logging and extra field conventions used across the SDK, see Logging. Release history is on the Changelog.
If you already use Amplitude to capture events, skip to Step 4 to register your product with Autoplay, then continue from Step 5 to identify on login, and Step 7 to derive your ingest URL.

🎯 Step 1 β€” Install the Amplitude Unified SDK

The Unified SDK (@amplitude/unified) combines Analytics + Session Replay in one package β€” the same setup used in the Autoplay reference app.
npm install @amplitude/unified

Complete Amplitude setup before continuing. When you log in for the first time, Amplitude shows a β€œLet’s get set up!” screen with β€œWaiting for your events…” at the bottom. You cannot access the Amplitude dashboard or create destinations until this step is finished.To unblock yourself, choose one of:
  • Complete the setup β€” follow Steps 1–3 below, trigger a few events in your app. Once Amplitude receives them, β€œFinish Setup” activates and you reach the main dashboard.
  • Skip for now β€” click β€œSkip for now” in the bottom-right corner to go directly to the dashboard. You can finish the SDK installation afterward, but do not move on to the Amplitude destination steps without completing Steps 1–4 first.

πŸ“² Step 2 β€” Install and initialise Amplitude in your app

This step is required before you can create a destination in Step 8. Amplitude only shows the Data β†’ Destinations section and the destination catalog after it has received at least one live event from your app. If you skip or fail to complete this step, the catalog will be empty and you will not be able to add a destination.
The interactive walkthrough below gives you a visual overview of the Amplitude SDK setup. Steps 3–5 walk through all three parts with the exact code: Step 3 initialises Amplitude, Step 4 registers your product with Autoplay, and Step 5 identifies users on login.πŸ“Ί Full setup walkthrough
If you already have amplitude.initAll(...) running in your app and users are appearing in the Amplitude dashboard, skip to Step 4 to register with Autoplay.

πŸ’» Step 3 β€” Initialize on app load

Call amplitude.initAll() once at app startup β€” in main.ts, _app.tsx, or your root layout. Replace YOUR_AMPLITUDE_API_KEY with your project’s API key (see the Tip below for where to find it):
Where to find your Amplitude API key: In Amplitude, go to Settings β†’ Projects β†’ [Your Project] β†’ General. Copy the API Key shown there. It’s also displayed on the onboarding wizard screen under β€œYour API Key”.
πŸ“Ί Copy your Amplitude API key
import * as amplitude from '@amplitude/unified';

amplitude.initAll('YOUR_AMPLITUDE_API_KEY', {
  analytics: {
    autocapture: true, // auto-captures clicks, page views, form interactions
  },
  sessionReplay: {
    sampleRate: 1, // 1 = record 100% of sessions; lower in production if needed
  },
});
autocapture: true ensures that page views, clicks, and session lifecycle events flow to Autoplay automatically. Session Replay records what users actually do β€” Autoplay correlates those sessions to give your support AI agent real-time context.

πŸ“ Step 4 β€” Registering your product with Autoplay

Where to find your YOUR_AUTOPLAY_PRODUCT_ID β€” this is your Amplitude project ID, found in Amplitude under Settings β†’ Projects β†’ select your project β†’ Project ID.
πŸ“Ί How to find your Amplitude Project ID
Use the same value everywhere: in Step 5’s identifyObj.set('product_id', ...), here in onboard_product(), and it will appear automatically in your ingest URL in Step 7.Create a Python file with the script below and run it once to generate your connector credentials:
import asyncio
from autoplay_sdk.admin import onboard_product
from autoplay_sdk.providers import AmplitudeProvider

async def main() -> None:
    result = await onboard_product(
        "YOUR_AUTOPLAY_PRODUCT_ID",
        contact_email="you@yourcompany.com",
        user_activity_provider=AmplitudeProvider(),
        print_operator_summary=True,
    )

asyncio.run(main())
This will print the following fields:
  • product_id: {product_id_entered}
  • stream_url: https://event-connector-luda.onrender.com/stream/{product_id}
  • unkey_key: {secret}
Save what prints in the terminal β€” you will need these values in Step 5 and Step 7.
Re-registering your productA second onboard_product with the same product_id returns 409 until overwrite is allowed. Pass force_reregister=True to overwrite. You must still pass contact_email on every registration, including overwrites.

πŸ‘€ Step 5 β€” Identify on login (required for session scoping)

Run this immediately after your login flow completes. Use the values you just got from Step 4:
  • YOUR_AUTOPLAY_PRODUCT_ID β€” the Amplitude project ID you registered above
  • YOUR_CONNECTOR_URL β€” the domain part of stream_url (e.g. if stream_url is https://event-connector-luda.onrender.com/stream/828048, your connector URL is https://event-connector-luda.onrender.com)
import * as amplitude from '@amplitude/unified';

amplitude.setUserId(user.id);

const identifyObj = new amplitude.Identify();
identifyObj.set('product_id', 'YOUR_AUTOPLAY_PRODUCT_ID');
identifyObj.set('email', user.email);
identifyObj.set('name', user.name);
amplitude.identify(identifyObj);

// Link the Amplitude session to Autoplay's session store
const sessionId = amplitude.getSessionId();
if (sessionId) {
  fetch('YOUR_CONNECTOR_URL/identify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: user.email,
      session_id: String(sessionId),
      product_id: 'YOUR_AUTOPLAY_PRODUCT_ID',
    }),
  });
}
Why both setUserId and identify?setUserId tells Amplitude who the user is. The Identify call sets user_properties β€” including product_id and email β€” which Autoplay uses to map Amplitude events to the correct tenant and to link activity with your support AI agent’s incoming conversation threads. The /identify POST links the live Amplitude session ID to Autoplay so the support AI agent can fetch real-time context immediately when a user opens the chat widget.
Without setUserId, users are tracked by device_id only. The session is still scoped correctly for activity tracking, but user_id and email will be absent on incoming ActionsPayload events until identity is established.
On logout β€” reset so the next user starts clean:
amplitude.reset()

πŸ”’ Step 6 β€” Understanding Amplitude session IDs

Amplitude assigns each browser session a numeric Unix-millisecond timestamp as its ID (e.g. 1749012345678). Autoplay automatically converts it to a string, so your backend always receives payload.session_id as a string like "1749012345678".You don’t need to do anything extra β€” the /identify POST in Step 5 already links the session to Autoplay at login. Every subsequent event arrives with session_id filled in automatically.
Amplitude initialises session_id to -1 briefly on page load before the first session is established. Autoplay falls back to device_id for these early events, which is still a valid scoping key. This resolves within a few seconds and is completely normal.

πŸ”— Step 7 β€” Derive the Amplitude ingest URL

Your connector exposes a dedicated Amplitude ingest endpoint. Derive it from the stream_url you saved from Step 4:
# Paste in the values printed by Step 4:
stream_url = "https://event-connector-luda.onrender.com/stream/828048"  # your stream_url from Step 4
unkey_key  = "3abc123..."                                          # your unkey_key from Step 4

amplitude_ingest_url   = stream_url.replace("/stream/", "/ingest/amplitude/")
amplitude_bearer_token = unkey_key

print(f"Amplitude ingest URL : {amplitude_ingest_url}")
print(f"Bearer token         : {amplitude_bearer_token}")
Example output:
Amplitude ingest URL : https://your-connector.onrender.com/ingest/amplitude/your_product_id
Bearer token         : 3abc123...
The amplitude_ingest_url is different from result.webhook_url. The webhook_url is for a different integration and will return 401 for Amplitude events. Always use /ingest/amplitude/{product_id} for Amplitude.

βš™οΈ Step 8 β€” Create the event streaming destination in Amplitude

πŸ“Ί How to create the Amplitude event streaming destination
Amplitude uses a developer portal to create custom event streaming destinations.8a β€” Open the destination builder
  1. In Amplitude, click Data in the left sidebar
  2. In the left sub-navigation, click Destinations
  3. Click + Add Destination (top right)
  4. In the catalog that opens, search for β€œHTTP” or scroll to find it, then select it
  5. Choose β€œEvent Streaming” as the type
8b β€” Fill in the Configuration tabIntegration Name β€” give the destination a name, e.g. autoplay-connector.URL Endpoint
FieldValue
MethodPOST
URLYour amplitude_ingest_url from Step 7 (e.g. https://your-connector.onrender.com/ingest/amplitude/your_product_id)
Create Parameters β€” click β€œAdd New Parameter” and add:
Parameter nameValue
apiKeyBearer + your amplitude_bearer_token from Step 7 (e.g. Bearer 4cmp...)
REST API Headers β€” click β€œAdd New REST API Header” and set:
Header KeyHeader Value
Authorization${parameters.apiKey}
Event Body Editor β€” replace the default Freemarker template with:
<#setting number_format="0.####">
<#assign et = input.event_type!''>
<#assign ep = input.event_properties!{}>
<#assign up = input.user_properties!{}>

{
  "events": [
    {
      "event_type": "<#if et?starts_with('Viewed') || et == '[Amplitude] Page Viewed' || et == 'Page Viewed'>$pageview<#elseif et == '[Amplitude] Element Clicked' || et == '[Amplitude] Element Changed' || et?starts_with('Form')>$autocapture<#else>${et}</#if>",
      "user_id": "${input.user_id!''}",
      "device_id": "${input.device_id!''}",
      "session_id": ${input.session_id!0},
      "time": ${input.time!0},
      "user_properties": {
        <#list up?keys as k>"${k}": "${up[k]}"<#sep>, </#sep></#list>
      },
      "event_properties": {
        "[Amplitude] Page URL": "${ep['Page URL']!ep['Page Location']!ep['[Amplitude] Page URL']!ep['[Amplitude] Page Location']!''}",
        "[Amplitude] Page Title": "${ep['Page Title']!ep['[Amplitude] Page Title']!''}",
        "$event_type": "<#if et == '[Amplitude] Element Changed'>change<#elseif et?starts_with('Form Submitted')>submit<#elseif et?starts_with('Form Started')>focus<#else>click</#if>",
        "$current_url": "${ep['Page URL']!ep['Page Location']!ep['[Amplitude] Page URL']!ep['[Amplitude] Page Location']!''}",
        "$button_text": "${ep['[Amplitude] Element Text']!ep['Element Text']!ep['Page Title']!''}",
        "$elements_chain": "${ep['[Amplitude] Element Path']!ep['[Amplitude] Element Hierarchy']!ep['Element Path']!''}",
        "$element_id": "${ep['[Amplitude] Element ID']!ep['Element ID']!''}",
        "$element_tag": "${ep['[Amplitude] Element Tag']!ep['Element Tag']!''}"
      }
    }
  ]
}
This template does two things the default Amplitude template does not:1. Event type normalisation β€” Amplitude’s autocapture sends events with names like [Amplitude] Page Viewed and [Amplitude] Element Clicked. The Autoplay connector needs these mapped to standard names it recognises. The template does that automatically:
Amplitude eventNormalised to
[Amplitude] Page Viewed, Page Viewed, Viewed *$pageview
[Amplitude] Element Clicked, [Amplitude] Element Changed, Form *$autocapture
Everything elsepassed through unchanged
2. Field mapping β€” Amplitude stores click details (element text, URL, element path) under its own field names. The template copies them into the fields Autoplay expects so it can generate human-readable descriptions like β€œUser clicked Sign Up button on the dashboard page”.Without this template, only page views appear in the Autoplay stream β€” clicks and form events are silently ignored.
Click β€œNext” to proceed to the Testing tab.8c β€” Configure and release on the Testing tab
  1. Enable the β€œSend Events” toggle
  2. Under β€œSelect & filter events”, keep it set to β€œAll Events”
  3. Click β€œTest Connection” to send a test event and confirm you get a 200 OK
  4. Click β€œRelease” (top right) β€” this publishes the destination to the catalog
8d β€” Create a sync from the published destination
  1. Go back to Data β†’ Destinations
  2. Under β€œNew Destinations”, find your destination
  3. Click it β†’ β€œAdd New Sync” page opens
  4. Enter a Sync Name (e.g., production-stream) and click β€œCreate Sync”
The sync is now active. Amplitude will stream all events to your connector endpoint in real time.

πŸ“‘ Step 9 β€” Watch Your Data Arrive Live! (Receive your first event)

Everything is wired up! Open a terminal and run:
curl -N "https://your-connector.onrender.com/stream/your_product_id" \
  -H "Authorization: Bearer YOUR_UNKEY_KEY"
Or use the Python SDK:
import asyncio
from autoplay_sdk import AsyncConnectorClient

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}")
    for i, action in enumerate(p.actions):
        print(f"  [{i}] Action: {action.title}")
        print(f"       Details: {action.description}")
        print(f"       Page:    {action.canonical_url}")

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

asyncio.run(main())
After clicking around your app, you should see data: lines arrive with "type": "actions" payloads. (If your internet drops, the client reconnects automatically.)Common issues:
  • 401 on the connector: Confirm the Authorization header is Bearer result.unkey_key, not the key alone. Confirm the URL ends with /ingest/amplitude/your_product_id.
  • 404: Confirm your_product_id in the URL matches the product_id used in onboard_product.
  • Events in Amplitude Live Events but not in SSE stream: Confirm the sync is active and the Event Body template uses event_type (not event_name) inside {"events": [...]}.
  • No events in Amplitude Live Events: Confirm the sync β€œSend Events” toggle is on and that you are calling amplitude.setUserId() before tracking events.
πŸ‘‹ Quick Tip: Once you add this code to your site, jump into our Discord and say hi β€” we will check your data is flowing and help you get fully set up!

πŸ“¦ What the payload looks like

A sample Amplitude event that Autoplay receives:
{
  "event_type": "$pageview",
  "session_id": 1749012345678,
  "user_id": "user-462718483",
  "device_id": "abc123def456",
  "user_properties": {
    "product_id": "your_product_id",
    "email": "alice@example.com"
  },
  "event_properties": {
    "[Amplitude] Page URL": "https://app.example.com/dashboard",
    "[Amplitude] Page Title": "Dashboard"
  },
  "time": 1749012345678
}
After normalization, ActionsPayload surfaces:
  • session_id β€” "1749012345678" (stringified)
  • user_id / email β€” from user_properties set via amplitude.identify()
  • actions β€” extracted page view actions with canonical_url, title, and description

⚠️ Common mistakes

Using result.webhook_url instead of the Amplitude ingest URL. Always use /ingest/amplitude/`.Calling amplitude.setUserId() but not amplitude.identify() with product_id. setUserId alone does not write user_properties. Without an explicit Identify call, product_id and email will be absent from every event β€” Autoplay will not be able to map the event to your tenant or to an active chat conversation.Session ID is βˆ’1 in early events. Amplitude initializes session_id to βˆ’1 briefly before the first session is established. Autoplay falls back to device_id for these events, which is still a valid scoping key. This is normal and resolves within a few seconds of page load.Not calling amplitude.init() before the chat widget opens. If the chat widget fires GET /sessions/{product_id}/{session_id} before Amplitude’s session is established, there will be no activity to return. Initialize Amplitude at the earliest possible point in your app lifecycle.

πŸ”Œ Next: choose your AI support agent β†’ connect via MCP

Your activity is now flowing into the connector. The next step is 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.
Connect Intercom Fin to live user activity via the MCP server β€” add the MCP connector, the Get Live User Activity tool, and Messenger JWT identity.Connect Fin via MCP ->
Coming soon β€” Maven will connect the same way, via the MCP server.
Coming soon. Join our Discord for notifications.
Coming soon. Join our Discord for notifications.
Coming soon. Join our Discord for notifications.

πŸ€– Step-2 choose your existing AI support agent

Coming soon.
Coming soon.
Coming soon.

πŸ—ΊοΈ Step-3 Choose your existing visual guidance

Pop-up tours

Browser agents

Coming soon. Join our Discord for notifications.