Tracking Events
An event is a record of something a person did—signed up, viewed a screen, completed a purchase, or abandoned a cart. Events are the behavioural layer of Conviso: they describe what people do, while attributes describe who people are.
Once events are flowing in, you can search them, inspect their properties, build segments, trigger campaigns, measure broadcast and campaign goals, and personalise messages with live event data.
This page is the working guide: how to send events, how to find them in Conviso, and how to use them.
Do this first: send and find one event
If you have never sent an event before, complete this sequence once. You will know the pipeline works when you can see the event on a person in the dashboard.
1. Get a workspace API key
- Open your Conviso workspace
- Go to Settings → Workspace → API keys
- Create a key (or copy an existing one)
- Keep it somewhere safe — you will send it as the
Authorizationheader
2. Identify a test person
curl -X POST "https://api.opencdp.io/gateway/data-gateway/v1/persons/identify" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"identifier": "user_123",
"properties": {
"email": "[email protected]",
"first_name": "Ada",
"last_name": "Lovelace",
"created_at": "2026-09-07T12:00:00.000Z",
"plan": "pro"
}
}'
Successful response:
{
"success": true,
"message": "Identification complete",
"data": {}
}
3. Track a test event for that same person
curl -X POST "https://api.opencdp.io/gateway/data-gateway/v1/persons/track" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"identifier": "user_123",
"eventName": "purchase_completed",
"properties": {
"order_id": "ord_789",
"amount": 99.99,
"currency": "USD",
"item_count": 1
}
}'
Successful response:
{
"success": true,
"message": "Track event recorded",
"data": {}
}
A 200 means the event was accepted. It is processed asynchronously, so it may take a few seconds to appear in the dashboard.
4. Find the event in Conviso
From Events (by event name):
- Go to Events in the left navigation
- Search for
purchase_completed - Click the event name
- In Activities, confirm
Ada Lovelace(oruser_123) appears - Click the row — the drawer shows the JSON properties (
order_id,amount,currency)
From People (by person):
- Go to People
- Search for
user_123or[email protected] - Open the profile
- Open Activity
- Confirm
purchase_completedis in the timeline
If both places show the event with the properties you sent, you are ready to instrument your product.
How events work
Every tracked event belongs to a person and has:
| Field | What it is | Example |
|---|---|---|
| Event name | Stable label for the action (case-sensitive) | purchase_completed |
| Identifier | The same person ID you used in identify | user_123 |
| Properties | Optional metadata about this occurrence | { "amount": 99.99 } |
| Timestamp | When the action happened | 2026-09-07T12:00:00.000Z |
You do not create event types in the dashboard. The first time Conviso receives a new eventName, it appears under Events. After that, every occurrence is stored against the person who performed it.
Two different records are involved:
- Event type — the named action in your workspace (
purchase_completed). One row in the Events table. Shows last tracked time, occurrence count, and how many people have done it. - Tracked event — one occurrence by one person at one time. These are the rows in Activities, and on the person’s Activity tab.
A typical flow:
- Your app or backend identifies a person
- Your app or backend tracks an event with that same identifier
- Conviso attaches the event to the person’s profile
- The event becomes available for segments, campaigns, goals, analytics, and personalisation
Common use cases:
- Triggering a campaign when someone abandons a cart
- Building a segment of people who purchased in the last 30 days
- Measuring whether a broadcast caused people to complete onboarding
- Personalising an email with the product they just viewed
- Analysing which events drive conversion in a journey
Identify, then track
Events are only useful when they attach to the right person. Always use a consistent identifier—typically your internal user ID—across identify, track, device registration, and messaging.
// 1. Create or update the person
await client.identify('user_123', {
email: '[email protected]',
first_name: 'Ada',
last_name: 'Lovelace',
created_at: new Date().toISOString(),
plan: 'pro',
});
// 2. Track behaviour with the same ID
await client.track('user_123', 'purchase_completed', {
order_id: 'ord_789',
amount: 99.99,
currency: 'USD',
});
⚠️ Important:
Do not mix identifiers. If you identify with user_123 and later track with the person’s email, Conviso treats those as two different people. Events will not appear on the profile you expect, campaigns will not fire, and segments will under-count.
Rules:
- Use your internal user ID (
user_123), not an email, asidentifier - Use that same string on every
identify,track,registerDevice, and transactional send - For mobile apps the identifier must not be an email address — see Identity and devices
- Include
email,first_name/firstName, andcreated_aton the firstidentifyso the person is searchable and usable in email
Learn more about creating people in Adding people and People and their profiles.
Sending events into Conviso
You can send events from any of the following sources. Choose the option that matches where the action happens.
| Source | Best for | Start here |
|---|---|---|
| Direct API | Any language, curl, ETL, first test | Direct API |
| Node.js SDK | Server-side apps, APIs, web backends | Node.js SDK |
| Python SDK | Python backends and workers | Python SDK |
| Flutter SDK | Cross-platform iOS and Android apps | Flutter SDK |
| Android SDK | Native Android (Kotlin/Java) | Android SDK |
| iOS SDK | Native iOS (Swift/Objective-C) | iOS SDK |
| React Native SDK | React Native / Expo development builds | React Native SDK |
| PostHog webhook | Product analytics already in PostHog | PostHog webhook |
All of these sources write to the same Data Gateway. Once an event lands, it behaves the same in Conviso regardless of how it was sent.
Base URL (production): https://api.opencdp.io/gateway/data-gateway
Browse the full catalogue on the Integrations page, or start from Get started with SDKs.
Authentication
Every source authenticates with a workspace API key:
- Go to Settings → Workspace → API keys
- Create or copy a key
- Direct API / PostHog: send it in the
Authorizationheader (raw key value — notBearerunless your key already includes that prefix) - SDKs: pass it as
cdpApiKey(orcdp_api_key/api_key, depending on the SDK)
Authorization: YOUR_API_KEY
Content-Type: application/json
Keep API keys out of git and out of public web pages. Mobile apps still need a key to talk to the Data Gateway; treat it like any other client-side secret and rotate it if it leaks.
Direct API (complete)
Use this when you do not have an SDK, or when you want to verify the pipeline with curl.
These are the same endpoints the SDKs call.
| Operation | Method | Path |
|---|---|---|
| Identify a person | POST | /v1/persons/identify |
| Track an event | POST | /v1/persons/track |
| Register a device | POST | /v1/persons/registerDevice |
Identify
URL: POST https://api.opencdp.io/gateway/data-gateway/v1/persons/identify
| Field | Type | Required | Description |
|---|---|---|---|
identifier | string | Yes | Unique person ID (your internal user ID). Cannot be empty. |
properties | object | Yes | Profile attributes. Use email, first_name / firstName, last_name / lastName, created_at, plus any custom keys. |
timestamp | string | No | ISO 8601 timestamp for the identify call. |
curl -X POST "https://api.opencdp.io/gateway/data-gateway/v1/persons/identify" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"identifier": "user_123",
"properties": {
"email": "[email protected]",
"firstName": "Ada",
"lastName": "Lovelace",
"created_at": "2026-09-07T12:00:00.000Z",
"plan": "pro"
}
}'
firstName / first_name and lastName / last_name are equivalent. Conviso maps both onto the person profile.
Track
URL: POST https://api.opencdp.io/gateway/data-gateway/v1/persons/track
| Field | Type | Required | Description |
|---|---|---|---|
identifier | string | Yes | Must match the identifier used in identify. |
eventName | string | Yes | Event name. Case-sensitive. Cannot be empty. |
properties | object | No | JSON object of event metadata. Nested objects and arrays are allowed. |
curl -X POST "https://api.opencdp.io/gateway/data-gateway/v1/persons/track" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"identifier": "user_123",
"eventName": "purchase_completed",
"properties": {
"order_id": "ord_789",
"amount": 99.99,
"currency": "USD",
"items": [
{ "product_id": "prod_123", "name": "Pro plan", "quantity": 1 }
],
"payment_method": "card"
}
}'
Properties are optional. This is also valid:
{
"identifier": "user_123",
"eventName": "app_opened"
}
What a successful track looks like
{
"success": true,
"message": "Track event recorded",
"data": {}
}
The body is empty on purpose. Tracking is fire-and-forget: Conviso queues the event and returns immediately. Open Events a few seconds later to confirm it landed.
Errors
| Status | When | What to do |
|---|---|---|
401 Missing Authorization Header | No Authorization header | Add the header |
401 Invalid Token | Key is wrong, revoked, or for another workspace | Copy a key from Settings → Workspace → API keys |
400 identifier is required / identifier cannot be empty | Missing or empty identifier | Send a non-empty string |
400 eventName is required / eventName cannot be empty | Missing or empty eventName | Send a non-empty string |
400 properties must be an object | properties is an array or a primitive | Send a JSON object |
200 Request payload discarded: exceeds 20kb limit | Body larger than 20 KB | Shrink properties. This still returns HTTP 200 — the event is not stored. |
500 Failed to process track request | Server error | Retry; check that identify/track still work with curl |
⚠️ Payload size:
Identify and track bodies are limited to 20 KB. Oversized requests are discarded. Check the message field even on HTTP 200.
Track without identify
If you track an identifier that has never been identified, Conviso still accepts the event. A person profile is created from that identifier, but it will have no email or name until you identify. Always identify first so the person is searchable and you can message them.
Full Direct API reference (including device registration and transactional send): Direct API.
Server SDKs
Use a server SDK when the event happens in your backend—checkouts, billing webhooks, account changes, or anything you already trust on the server.
Node.js
Install:
npm install @codematic.io/cdp-node
Identify and track:
import { CDPClient } from '@codematic.io/cdp-node';
const client = new CDPClient({
cdpApiKey: process.env.CDP_API_KEY,
});
await client.identify('user_123', {
email: '[email protected]',
firstName: 'Ada',
lastName: 'Lovelace',
created_at: new Date().toISOString(),
plan: 'pro',
});
await client.track('user_123', 'purchase_completed', {
order_id: 'ord_789',
amount: 99.99,
currency: 'USD',
items: [
{ product_id: 'prod_123', name: 'Pro plan', price: 99.99, quantity: 1 },
],
payment_method: 'card',
});
Signature:
async track(
identifier: string,
eventName: string,
properties?: Record<string, any>
): Promise<void>
identifierandeventNamecannot be empty — the SDK throws before it calls the APIpropertiesis optional- Wrap calls in try/catch. For page views and other non-critical tracking, do not block the response:
client
.track(user.id, 'page_viewed', { path: req.path })
.catch((err) => console.error('Track failed', err));
Checkout example:
app.post('/api/checkout', async (req, res) => {
const { userId, items, total } = req.body;
const order = await processCheckout(items);
await client.track(userId, 'purchase_completed', {
order_id: order.id,
amount: total,
currency: 'USD',
item_count: items.length,
payment_method: 'stripe',
});
res.json({ success: true, orderId: order.id });
});
Python
Install:
pip install cdp-python-sdk
from cdp_client.client import CDPClient
from cdp_client.models import CDPConfig
config = CDPConfig(cdp_api_key="YOUR_API_KEY")
client = CDPClient(config)
await client.identify("user_123", {
"email": "[email protected]",
"first_name": "Ada",
"last_name": "Lovelace",
"created_at": "2026-09-07T12:00:00.000Z",
"plan": "pro",
})
await client.track(
"user_123",
"purchase_completed",
{
"order_id": "ord_789",
"amount": 99.99,
"currency": "USD",
"item_count": 1,
},
)
await client.close()
| Parameter | Type | Description |
|---|---|---|
user_id | str | Same identifier as identify |
event_name | str | Event name |
properties | Dict[str, Any] | Event metadata |
Mobile SDKs
Use a mobile SDK when the event happens on a device—button taps, screen views, in-app purchases, or app lifecycle. Mobile SDKs also handle push, in-app messaging, offline queuing, and automatic screen tracking.
If you are integrating a mobile app for the first time, start with the Mobile E2E guide. Compare capabilities across platforms in the Mobile feature matrix.
On every mobile SDK:
- Initialise before any other call — if you skip this,
trackfails silently - Identify with a non-email user ID
- Track the product action
- Enable
debug: truewhile testing and watch identify/track POST logs
Flutter
await OpenCDPSDK.initialize(
config: OpenCDPConfig(
cdpApiKey: 'YOUR_API_KEY',
debug: true,
autoTrackScreens: true,
),
);
await OpenCDPSDK.instance.identify(
identifier: 'user_123',
properties: {
'email': '[email protected]',
'firstName': 'Ada',
'plan': 'pro',
'created_at': DateTime.now().toIso8601String(),
},
);
await OpenCDPSDK.instance.track(
eventName: 'purchase_completed',
properties: {
'order_id': 'ord_789',
'amount': 99.99,
'currency': 'USD',
},
);
Manual screen tracking (for in-app page targeting — this does not appear as a row in Events):
await OpenCDPSDK.instance.trackScreenView(
title: 'Product Details',
properties: {
'product_id': '123',
'category': 'Electronics',
},
);
- Introduction
- Quick start
- API methods (
track,trackScreenView,identify)
Android
val config = OpenCDPConfig(
cdpApiKey = "YOUR_API_KEY",
debug = true,
autoTrackScreens = true,
trackApplicationLifecycleEvents = true,
)
OpenCDP.initialize(this, config)
OpenCDP.identify(
identifier = "user_123",
properties = mapOf(
"email" to "[email protected]",
"plan" to "pro",
),
)
OpenCDP.track(
eventName = "purchase_completed",
properties = mapOf(
"order_id" to "ord_789",
"amount" to 99.99,
"currency" to "USD",
),
)
iOS
let config = OpenCDPConfig(
cdpApiKey: "YOUR_API_KEY",
debug: true,
autoTrackScreens: true,
trackApplicationLifecycleEvents: true
)
OpenCDP.shared.initialize(config: config)
OpenCDP.shared.identify(
identifier: "user_123",
properties: [
"email": "[email protected]",
"plan": "pro",
]
)
OpenCDP.shared.track(
eventName: "purchase_completed",
properties: [
"order_id": "ord_789",
"amount": 99.9,
"currency": "USD",
]
)
React Native
Expo Go is not supported. Use an Expo development build or a bare React Native app.
import { OpenCDPSDK } from '@codematic/opencdp-react-native';
await OpenCDPSDK.initialize({
config: {
cdpApiKey: 'YOUR_API_KEY',
debug: true,
autoTrackDeviceAttributes: true,
},
});
await OpenCDPSDK.instance.identify({
identifier: 'user_123',
properties: {
email: '[email protected]',
plan: 'pro',
},
});
await OpenCDPSDK.instance.track({
eventName: 'purchase_completed',
properties: {
order_id: 'ord_789',
amount: 99.99,
currency: 'USD',
},
});
PostHog webhook
If you already capture product analytics in PostHog, stream those events into Conviso without adding another client SDK.
Setup
- Log in to PostHog
- Go to Data pipeline → Destinations (or Webhooks)
- Create a Webhook destination
- Set the URL to
https://api.opencdp.io/gateway/data-gateway/v1/posthog - Add header
Authorization= your Conviso API key - Trigger on all events, or filter to the events you care about
- Save and enable
How PostHog events map
| PostHog event | What Conviso does |
|---|---|
$identify | Creates or updates the person (properties.id, or distinct_id if id is missing) |
$register_device | Registers a device for push. Requires device_id, platform, fcm_token |
| Any other event | Stored as a tracked event with that event name |
Minimum payload:
{
"event": {
"event": "purchase_completed",
"distinct_id": "user_123",
"uuid": "018f...",
"timestamp": "2026-09-07T12:00:00Z",
"properties": {
"id": "user_123",
"amount": 99.99,
"currency": "USD"
}
}
}
PostHog metadata is nested on the tracked event so you can still inspect it in the Activities drawer:
{
"amount": 99.99,
"currency": "USD",
"posthog": {
"uuid": "018f...",
"distinct_id": "user_123",
"url": "https://...",
"timestamp": "2026-09-07T12:00:00Z"
}
}
⚠️ Loop prevention:
Events whose identifier contains the word posthog (case-insensitive) are ignored. This stops internal PostHog system users from creating noise in your workspace.
If events do not appear: confirm the identifier is not posthog…, the event object is present, and the API key is in Authorization.
Full setup: PostHog webhook integration.
Event names and properties
Naming events
Event names are case-sensitive. Purchase_Completed and purchase_completed are two different event types, two different Events-table rows, and two different campaign triggers.
Use a consistent convention across every source:
- Prefer lowercase
snake_case:purchase_completed,cart_abandoned,onboarding_step_viewed - Name the action that happened, not the page or campaign
- Keep names stable — renaming creates a new event type; historical data stays on the old name
- Use the same name from web, iOS, Android, and your backend
Good examples:
user_signed_upcheckout_startedpurchase_completedsubscription_cancelledvideo_watched
Avoid:
- Generic names like
clickorevent - Names that include dates or campaign IDs (
purchase_summer_2026) - The same action tracked under different names from different platforms (
Purchaseon iOS andpurchase_completedon the server)
Event properties
Properties are the details attached to a single occurrence. They do not update the person’s profile.
- To change who someone is (
plan,email,name) →identify - To describe what they just did (
amount,order_id,page) →trackproperties
{
"identifier": "user_123",
"eventName": "purchase_completed",
"properties": {
"order_id": "ord_789",
"amount": 99.99,
"currency": "USD",
"items": [
{ "product_id": "prod_123", "name": "Pro plan", "quantity": 1 }
],
"payment_method": "card"
}
}
Best practices:
- Store numbers as numbers (
99.99, not"99.99") and booleans as booleans (true, not"true") - Use ISO 8601 timestamps for dates (
2026-09-07T12:00:00.000Z) - Nest objects when it helps — you can reference nested keys with dot notation in messages and segment filters
- Keep the payload under 20 KB
- Do not put passwords, tokens, payment card numbers, or other secrets in properties
Person attributes vs event properties:
| Person attributes | Event properties | |
|---|---|---|
| Sent with | identify | track |
| Stored on | The person profile | That one event occurrence |
| Available in | Every message ({{ person.* }}) | Event-triggered messages and event conditions ({{ event.* }}) |
| Example | plan, email, created_at | amount, order_id, page |
See Attributes for profile data.
Screen views and lifecycle events
Mobile SDKs can automatically record screen views and app lifecycle.
Screen views (screen_view / trackScreenView) catalogue screens for in-app messaging page targeting. They do not appear as standard events in the Events list, and they do not trigger campaigns.
A small set of mobile lifecycle names is ignored so they do not flood your workspace:
App_inactiveApp_backgroundedApp_hiddenApp_resumedApp_foregrounded
Track the product actions you care about explicitly (purchase_completed, onboarding_completed) rather than relying on lifecycle noise.
For automatic screen tracking:
- Flutter
trackScreenView()andscreenTracker - Android lifecycle tracking
- iOS quick start
- React Native quick start
Finding events in Conviso
This is how you get events after they have been sent. There is no public “list events” endpoint on the Data Gateway — you inspect them in the workspace.
Events list
- Open the workspace
- Click Events in the left navigation
Each row is an event type (not a single occurrence):
| Column | Meaning |
|---|---|
| Event name | The eventName you sent |
| Last tracked | When the most recent occurrence arrived (relative time if within 90 days) |
| Occurrences | How many times this event has been recorded in the workspace |
You can:
- Search by name (the filter is applied as you type)
- Sort by last tracked
- Click a name to open the event detail
If the list is empty, nothing has been tracked in this workspace yet. Complete Do this first with curl, then refresh.
Event detail
The header shows:
- The event name
- How many people have performed it (“12 people have performed this event”)
Below that:
- Used in N segments — click to see which segments reference this event, or Create a segment from this event
- Used in N campaigns — click to see which campaigns are triggered by this event, or Create a campaign from this event
Use those shortcuts when you are ready to automate. They pre-fill the event name so you do not retype it.
Activities (individual occurrences)
On the event detail page, Activities lists recent tracked events:
| Column | Meaning |
|---|---|
| Person | Name, email, or external ID. Click through to the profile |
| Timestamp | When the occurrence happened |
Click a row to open the Event Details drawer:
- Event name
- Person (with a link to their profile)
- Timestamp
- Full properties JSON — copy with the copy button
Lookback starts at 7 days. If the occurrence you just sent is older than that window, or you need more history, widen lookback to 30, 90, or 180 days. Pagination walks older pages within the selected window.
On a person’s profile
This is the fastest way to debug a single user.
- Go to People
- Search by external ID (
user_123), email, or name - Open the profile
- Open Activity
You should see:
- The
identify(profile created/updated) - Each
trackwith its event name - Messages sent, campaign interactions, and segment changes
If the person exists but the event does not, the track used a different identifier than identify.
See People and their profiles.
What “event not found” usually means
Work through this in order:
- Did
trackreturn200with"Track event recorded"? - Did you wait a few seconds and refresh Events?
- Are you in the same workspace as the API key?
- Is the name exactly what you sent (
purchase_completedvsPurchase Completed)? - Open People for
user_123— if the person is missing,identifydid not land - If the person exists but Activities is empty, the track identifier does not match
Using events in segments
Data-driven segments include or exclude people based on behaviour.
Create a segment from an event
- Open Events → purchase_completed (or your event)
- Click Create a segment from this event
- Name the segment (for example,
Purchased in last 30 days) - The event condition is pre-selected as Has been performed
- Expand the condition to set at least N times and a timeframe
- Optionally add event-attribute filters (
amountgreater than100) - Save
Event condition options
| Option | What it does |
|---|---|
| Has been performed | People who did this event |
| Has not been performed | People who did not do this event in the chosen timeframe |
| Count | At least n times (default 1) |
| Timeframe: ever | Any time in history |
| Timeframe: within the last | Last n days |
| Timeframe: between | Inclusive date range |
| Event attributes | Filter on properties of the event |
Event-attribute operators:
- Equals / not equals
- Contains
- Greater than / less than
- Exists / does not exist
You can require all or any of the attribute filters to match.
Examples
| Segment | Condition |
|---|---|
| Recent buyers | purchase_completed performed at least 1 time within the last 30 days |
| High-value buyers | purchase_completed performed where amount greater than 100 |
| At risk | logged_in has not been performed within the last 30 days |
| Power users | feature_used performed at least 10 times ever, and person plan equals pro |
You can combine event conditions with person attributes using AND / OR groups. See Segments for group logic.
Using events in campaigns
Campaigns can start when an event is received. Use this when you want to react immediately—abandoned carts, completed purchases, form submissions.
Create a campaign from an event
- Open Events → your event
- Click Create a campaign from this event
- Or go to Campaigns → Create campaign, then in the workflow click Start here → Event
- Select the event name
- Optionally add filters:
- Event attribute — e.g.
amountgreater than50 - Person attribute — e.g.
planequalspro - Segment — e.g. person is in
Newsletter subscribers
- Event attribute — e.g.
- Set frequency:
- On every occurrence — enter the campaign every time the event fires
- One time — enter only once, ever
- Once within a period — at most once per hour/day/week/month, etc.
- Build the rest of the workflow (delay, email, push, in-app, branch)
- Set a goal if you want to measure success (often “perform an event”)
Personalise with event data
Event-triggered messages can use both the person profile and the event that fired the campaign.
Hi {{ person.first_name }},
Your order {{ event.order_id }} for {{ event.amount }} {{ event.currency }} is confirmed.
Nested properties use dot notation. Arrays are zero-indexed:
{{ event.items.0.name }}
{{ event.address.city }}
Safe defaults (avoids blank output if a key is missing):
{{ event.amount | default: " " }}
{{ event.items | json }}
| Namespace | Available in | Example |
|---|---|---|
{{ person.* }} | Every message | {{ person.first_name }}, {{ person.plan }} |
{{ event.* }} | Event-triggered messages only | {{ event.order_id }}, {{ event.amount }} |
The message editor’s event-attribute picker is filled from recent occurrences of that event type. If a property does not appear in the picker, send a test event that includes the key, wait a minute, then reopen the editor.
Campaign goals
A campaign goal can be:
- Perform an event — e.g.
purchase_completedwithin 7 days of a delivery - Enter a segment
- Leave a segment
Pick the conversion window (value + unit) and whether the clock starts when the message is sent, opened, or clicked.
Learn more in Create a campaign and Overview & setup.
Using events in broadcasts and transactionals
Broadcast goals
A broadcast is a one-time send. You can still define success as:
- Performing a specific event
- Entering a segment
- Exiting a segment
Example: send a legal-update email, then measure success as terms_accepted.
Broadcasts are not triggered by events. They are sent by you, immediately or on a schedule. Events measure what happened afterwards.
Transactionals
Transactional messages are sent by API when something happens in your product. They are not the same as tracked events, but they often go together:
- Track
password_reset_requested - Send a transactional email with the reset link
- Track
password_reset_completedwhen they finish
Use events for analytics, segments, and campaign triggers. Use transactionals for the message that must go out immediately to one person.
Using events in journeys and analytics
Events also power measurement:
- Journey analysis — build funnels from event and segment steps to see where people drop off
- Event analytics cards — in Journeys analytics, pin an event to a card and watch volume, unique people, and period-over-period change
- Workspace dashboard — overall activity; see Workspace dashboard
To pin an event analytics card:
- Go to Journeys (analytics / event analytics)
- Create a card and choose the event type
- Open the card to see counts, unique people, and trends
Use analytics to decide which events are worth automating. A noisy event that fires on every page load is a poor campaign trigger; a clean checkout_started or trial_ended event is a strong one.
In-app messaging, push, and events
Tracked events and messaging delivery are related but separate:
- Tracked events (
track) describe product behaviour - Push / in-app delivery metrics (delivered, opened, impression, click, dismiss) are recorded by the mobile SDKs when messages are shown
For in-app messages, the SDK syncs eligible messages for the current screen, then reports impression, click, and dismiss. Screen names must match what you configure in the in-app editor.
You can still use a tracked event as the campaign trigger that queues an in-app or push message—for example, track cart_abandoned, then show an in-app banner the next time the SDK syncs on the cart screen.
See:
Recommended event catalogue
Start with a small, stable set. Identify the person before or with the first event so history is not orphaned.
| Event name | When to send | Useful properties |
|---|---|---|
user_signed_up | Account created | source, plan, method |
user_logged_in | Successful login | method |
onboarding_step_completed | A step in onboarding finished | step, step_index |
onboarding_completed | Onboarding finished | duration_seconds |
page_viewed | Meaningful web page (not every navigation) | page, referrer |
checkout_started | Checkout begun | cart_value, item_count |
cart_abandoned | Checkout started but not completed | cart_value, item_count |
purchase_completed | Payment succeeded | order_id, amount, currency, items |
subscription_started | Paid plan started | plan, amount, interval |
subscription_cancelled | Plan cancelled | plan, reason |
feature_used | A key feature was used | feature_name |
support_ticket_created | Customer contacted support | channel, topic |
Copy-paste purchase event (Node.js):
await client.track(user.id, 'purchase_completed', {
order_id: order.id,
amount: order.total,
currency: 'USD',
item_count: order.items.length,
items: order.items.map((item) => ({
product_id: item.id,
name: item.name,
price: item.price,
quantity: item.quantity,
})),
payment_method: order.paymentMethod,
});
Limits and behaviour
| Topic | Behaviour |
|---|---|
| Payload size | Identify and track bodies must be ≤ 20 KB. Larger payloads are discarded (HTTP 200 with a discard message). |
| Processing | Track is asynchronous. Dashboard appearance can lag by a few seconds. |
| Event names | Case-sensitive. New name = new event type. |
| Identifier | String; required; must be consistent. Mobile: do not use an email. |
screen_view | Catalogued for in-app targeting; not listed under Events; does not trigger campaigns. |
| Lifecycle names listed above | Accepted with 200, then dropped. They never appear in Events. |
PostHog identifiers containing posthog | Ignored. |
| Properties | JSON object. Nested objects/arrays allowed. Native types recommended. |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
401 on curl | Missing or wrong API key | Copy the key from Settings → Workspace → API keys. No Bearer prefix unless the key includes it. |
400 identifier / eventName | Empty or missing field | Send non-empty strings. |
| HTTP 200 but event never appears | Payload over 20 KB, ignored lifecycle name, screen_view, or PostHog loop filter | Read the response message. Shrink the body. Use a real product event name. |
| Event type appears, but Activities is empty | Lookback window is 7 days and you are looking at older data, or you are on the wrong event name | Widen lookback. Search Events for the exact name. |
| Person exists, event does not | track used a different identifier than identify | Log both values. They must be the identical string. |
| Person missing entirely | identify never ran, or ran with a different key/workspace | Identify first, same API key, then track. |
| Properties missing in the drawer | You did not send properties, or sent them as a string | Send a JSON object. Re-track and click the newest row. |
| Property missing in the campaign editor | No recent occurrence included that key | Send a test event with the key, wait, reopen the editor. |
| Campaign did not enter | Frequency is one-time and they already entered, or a filter did not match | Check frequency and event-attribute filters. Confirm the event is on the person’s Activity tab. |
| Two Events rows for one action | iOS sends Purchase and the server sends purchase_completed | Standardise the name in every SDK. |
| Mobile track does nothing | SDK not initialised, or debug off so failures are silent | Initialise before runApp / onCreate. Set debug: true. |
Still stuck? Re-run the first curl test with a unique identifier (user_debug_001). If curl works and the SDK does not, the bug is in initialisation or the identifier you pass in app code.
More: Direct API troubleshooting, PostHog troubleshooting, Node.js error handling.
Best practices
- Identify first, track second — events attach to people
- Use one identifier everywhere — the same ID for identify, track, register device, campaigns, and transactionals
- Keep event names stable and boring —
purchase_completedages better thanbought_pro_plan_v3 - Put state on the profile, actions on the event —
plan: "pro"belongs inidentify;amount: 99.99belongs intrack - Send native types — numbers, booleans, and ISO dates, not stringified values
- Track fewer, better events — a dozen meaningful actions beat hundreds of low-value clicks
- Do not send secrets — no passwords, tokens, card numbers, or government IDs
- Test with a known person — one test ID, from SDK logs → Events → person profile, before you automate
- Confirm properties in the drawer before you build a campaign that uses
{{ event.amount }}
Related documentation
Product
- People and their profiles
- Adding people
- Attributes
- Segments
- Create a campaign
- Campaign overview & setup
- Sending a broadcast
- Transactionals
- Push notifications
- In-app messaging
- Workspace dashboard
Integrations
- All integrations
- Get started with SDKs
- Direct API
- PostHog webhook
- Node.js SDK · Installation ·
track() - Python SDK ·
track - Flutter SDK · API methods
- Android SDK · Quick start
- iOS SDK · Quick start
- React Native SDK · Quick start
- Mobile E2E guide
- Identity and devices
- Mobile feature matrix
🎉 That’s it!
Send one identify and one track with curl, confirm they appear under Events and on the person’s Activity tab, then instrument the same calls in your app. After that, every new event name shows up automatically and is ready for segments, campaigns, and personalisation.