Loading tutorials…
Loading tutorials…
Customer.io is only as smart as the events you send it. Sloppy instrumentation — events fired client-side, inconsistent naming, missing properties — silently sabotages every workflow you build on top. This is the schema that scales.
Who this is forEngineering-marketing teams instrumenting Customer.io for the first time, or teams who notice their workflows trigger inconsistently and suspect the data layer is the cause. If your Customer.io event volume seems much lower than your product analytics tool (Mixpanel, Amplitude), this tutorial is for you.
What you'll need
Step 1
In a Google Doc or Notion page, list every event you want to track. Use a consistent naming convention. Lock the schema before instrumenting.
Open a doc. Title it 'Customer.io Event Schema v1.'
List every event with format `noun_verb` past tense (e.g., `Account Created`, `Subscription Started`, `Invite Sent`, `Feature Used`). Customer.io's docs use Title Case with spaces; Mixpanel/Amplitude defaults to snake_case — pick one convention and lock it across your stack.
For each event, list 5-10 properties you want to capture. Example for `Subscription Started`: `plan_name`, `plan_price_cents`, `billing_period`, `trial_converted` (bool), `referral_source`, `team_size`.
Aim for 15-30 events for v1. Over-instrumentation (200+ events) is worse than under — noisy events make segmentation harder, not easier.
Map each event to a lifecycle stage: Acquisition, Activation, Engagement, Retention, Expansion, Churn. This makes downstream workflow design 10x easier.
Step 2
identify creates/updates the profile. Fire it once on signup with all known attributes, then on every login to keep stale data fresh.
On user signup (in your backend), call: `cio.identify(user.id, { email, name, plan, created_at, signup_source, ... })`. Pass `created_at` as a Unix timestamp so Customer.io knows when the profile was created.
On every login, call identify again with the same attributes. Customer.io upserts — no risk of duplicates.
On any attribute change (user updates email, upgrades plan, changes team), fire identify immediately with the new value. Don't batch attribute updates daily — workflows triggered on attribute changes need real-time data.
Add a `last_seen_at` timestamp on identify — useful for Win-Back segments later.
Be liberal with attributes: `team_size`, `industry`, `mrr_cents`, `arr_cents`, `customer_tier`, `is_paying`, `lifecycle_stage`, `support_tier`. Storage is cheap. Adding attributes later requires backfilling.
Step 3
Track events are how Customer.io knows what users do. Fire them from your backend, with consistent properties.
Backend event firing: `cio.track(user.id, "Subscription Started", { plan_name: "Pro", plan_price_cents: 19900, billing_period: "monthly", trial_converted: true })`.
Critical SaaS lifecycle events to instrument: `Account Created`, `Email Verified`, `Onboarding Completed`, `Activation Event Hit` (you define what activation means — first project, first invite, first AI completion, etc.), `Subscription Started`, `Subscription Upgraded`, `Subscription Downgraded`, `Subscription Cancelled`, `Payment Failed`, `Trial Started`, `Trial Ended`, `Invite Sent`, `Invite Accepted`.
For each event, send the timestamp the action happened — not the timestamp the event fired (these can differ if you're batching).
Use idempotent event IDs when possible. Customer.io accepts `_io_id` as a dedup key — if your event firing has retries, idempotent IDs prevent duplicate counts.
Validate in Customer.io UI: People → search by user_id → Activity Timeline. Every event you instrument should appear within 30 seconds. If it lags more, your queue is backing up.
Step 4
Page views are noisy. Send them only if you'll use them in workflows. Otherwise skip and use Mixpanel/Amplitude.
Page events are useful for: 'sent a page-specific drip if user visited pricing page 3+ times in 7 days,' or 'send a help nudge if user spent 5+ minutes on the integrations page.' Otherwise they're noise.
If you decide to track pages: only send the meaningful 5-15 pages, not every URL. Pricing, Integrations, Specific Feature Pages, Help Center articles — yes. Every dashboard sub-route — no.
Server-side from your web app: in your route handler, after authenticating the user, fire `cio.track(user.id, 'Page Viewed', { page_name: 'Pricing', referrer: req.headers.referer })`.
For mobile apps, use the Customer.io mobile SDK's `screen()` method on key screens. Same discipline — meaningful screens only.
Step 5
Pre-signup user behavior matters. Use `cio_id` to track anonymous visitors, then merge to the user_id on signup.
Install Customer.io's JavaScript snippet on marketing pages (not the product app — the product is server-side instrumented).
The JS snippet sets a `cio_id` cookie automatically. Anonymous events (page views, marketing event signups, demo bookings) attach to that cio_id.
When the visitor signs up, your backend fires identify with the user_id. In the identify call, pass `cio_id` (read from the cookie at signup) as an attribute. Customer.io merges the anonymous profile into the identified profile.
Result: a lifecycle attribution chain from first marketing page view → demo booked → signup → activation, all on one profile.
Verify: in Customer.io → People → find your test profile. The Activity Timeline should show pre-signup events (Page Viewed: Pricing) and post-signup events (Account Created, Activation Event Hit) on the same profile.
Step 6
Bad event data sneaks in slowly. Set up monitoring so you find regressions in days, not months.
Customer.io → Settings → Webhooks → + Create. Subscribe to all event ingestion failures. Forward to Slack or PagerDuty.
In your product analytics tool (Mixpanel/Amplitude), set up a daily diff: 'Count of `Account Created` events in Mixpanel vs Customer.io for the past 7 days.' If they diverge by >5%, something is broken.
In Customer.io → Activity → Logs, periodically (weekly) check the Events tab for malformed event names — typos like `Account Crated` happen and stick around for months if unmonitored.
Run a quarterly schema audit: scan the Events tab, identify low-volume events that no workflow uses, deprecate them. Schema clutter compounds.
Step 7
Lock the schema in version control. Anyone adding an event should PR a schema change first.
In your repo, create `docs/customer-io-schema.md` or a TypeScript types file (`events.ts`) defining every event name and its property shape.
Wrap the Customer.io SDK in a typed helper: `function trackEvent<T extends keyof EventMap>(userId: string, event: T, props: EventMap[T])`. This forces engineers to use the schema at compile time.
Code-review process: any PR adding a new event must update the schema doc. Don't let ad-hoc events sneak into production.
Backfill discipline: when you add a new attribute or event, document whether it's backfilled or new-only. Future workflow designers need to know.
Sample TypeScript pattern: `type EventMap = { 'Account Created': { plan: string; signup_source: string }; 'Subscription Started': { plan_name: string; plan_price_cents: number } };` — then `trackEvent(user.id, 'Account Created', { plan: 'free', signup_source: 'organic' })` is type-checked.
Common mistakes
Client-side identify in an authenticated SaaS product
What goes wrong: Ad-blockers strip 15-25% of identify calls. Race conditions between identify and track lose attribution. Workflows fire on partial data. Activation funnel reporting shows artificially low numbers — a SaaS doing $50K MRR sees only $35K-40K in attributed Customer.io activation revenue, leading to wrong product decisions.
How to avoid: Move identify to server-side. Client-side is acceptable only for unauthenticated marketing pages. After moving, expect 20-30% more events flowing — that gap was your real attribution.
Inconsistent event naming (`Account Created` vs `account_created` vs `Created Account`)
What goes wrong: Workflows triggered on 'Account Created' miss users who fired 'account_created.' Activation campaigns under-target by 30-60%. The SaaS team's first 'why aren't users getting our onboarding emails?' incident usually traces here.
How to avoid: Pick one casing convention (Title Case with spaces is Customer.io idiomatic). Audit existing events in Activity → Events. Deprecate duplicates, alias old names to new ones in your code, run the migration.
No timestamp discipline (events fire with current time, not action time)
What goes wrong: Workflow timing windows are off. 'Send email 24 hours after Subscription Started' actually sends 24 hours after the event was queued — which on a batched system might be 48-72 hours after the actual subscription. Retention email cadence is broken.
How to avoid: Always pass a `timestamp` parameter on track calls when the action time differs from now. Customer.io's SDK signature is `cio.track(userId, event, { ...props, timestamp: actionUnixTime })`.
Sending raw click/UI events to Customer.io
What goes wrong: Event volume balloons 10-50x. Customer.io billing rises (events count toward usage on some plans). Segment performance degrades. Workflows trigger on irrelevant events. The Activity Timeline becomes unreadable for support debugging.
How to avoid: Restrict Customer.io to lifecycle events (15-30 max). Send raw click/UI events to Mixpanel or Amplitude. Customer.io should know what users did at a business-meaningful level — not every click.
No anonymous-to-identified merge via cio_id
What goes wrong: Pre-signup attribution disappears. Marketing pages, demo bookings, lead-magnet downloads can't be tied to the resulting user. You can't measure marketing → activation conversion accurately. Decisions about marketing spend lose 30-50% of their signal.
How to avoid: Install the Customer.io JS snippet on marketing pages. On signup, read the cio_id cookie and pass it to identify. Customer.io will merge profiles. One-time 30-minute setup.
No event-quality monitoring
What goes wrong: A deploy breaks identify firing. Nobody notices for 3 weeks. 3 weeks of new users land in Customer.io with no profile attributes — workflows can't segment them. Re-onboarding 3 weeks of users is painful.
How to avoid: Set up a daily event-count diff between your product analytics (Mixpanel/Amplitude) and Customer.io. Alert if divergence >5%. Catches deploy regressions same-day.
Recap
Done — what's next
How to set up a Customer.io account the right way (workspace, sending domain, identifiers)
Read the next tutorial
Hand it off
Event instrumentation is the foundation every Customer.io workflow stands on. A specialist who's instrumented 30+ SaaS products will design the schema, wrap your SDK in typed helpers, and set up monitoring in 1 week — typically $1,200-2,400 at $14-16/hr. The alternative is a year of 'why isn't this workflow firing?' tickets.
See specialist rates
Yes, if you're on Premium+. Data Pipelines (Sources) lets you instrument once and send the same events to Customer.io + Mixpanel + Amplitude + your data warehouse, all from a single SDK call. It replaces Segment for most SaaS use cases. If you're on Essentials, use the Track API directly until you upgrade.
Plan-dependent. Essentials includes 5M events/mo, Premium scales to 50M+, Enterprise is unlimited. Most SaaS teams send 2-10M events/mo at 25K-100K active users. If you cross plan limits, the cheapest fix is usually deprecating noisy events (raw clicks) rather than upgrading.
Premium+ supports Segment-compatible Source API. Point your existing Segment instrumentation at the Customer.io Source endpoint (same `identify`, `track`, `page`, `screen`, `group` method names). Run dual-write for 2-4 weeks to verify parity. Cut over Segment when confident. Total migration: 1-3 weeks depending on Destination complexity.
Yes — Customer.io's REST API accepts events with historical timestamps. Bulk-import via the /events endpoint with `timestamp` set to the original action time. Cap historical imports at 13 months (anything older is rarely useful for workflow triggers anyway). Plan 4-8 hours for a 100K-event historical backfill.
Same way as password-auth users — the user_id you assign at SSO callback is the Customer.io primary identifier. Whether the user authenticated via Google, Okta, or email doesn't matter. Just make sure your SSO callback fires identify with the canonical user_id + email + name from the SSO provider.
Sensitive data: payment card numbers, government IDs, health data (HIPAA), passwords. Customer.io is a marketing platform with appropriate but not PCI/HIPAA-grade controls. Send `Subscription Started` with a `plan_name` — never with the credit card. Send `Email Verified` — never with the verification token.
Customer.io
Customer.io rewards careful account setup. Identifier strategy, sending domains, and workspace/environment splits decided in week one save SaaS teams months of cleanup later. This is the build that scales past 100K users.
Customer.io
Segments are how Customer.io decides who. Workflows are how it decides when and what. Most SaaS teams get one or the other right but rarely both — and the gap shows up as activation campaigns missing 40% of eligible users.
Customer.io
Customer.io Data Pipelines is the CDP layer that lets you instrument once and fan out to many tools. For SaaS teams paying Segment $1K-10K/mo, this typically consolidates the spend while keeping the same instrumentation pattern.
Mixpanel
Mixpanel doesn't fail because events break — it fails because event names drift. Three engineers, three opinions, three versions of 'signup' over a year. Here's how to ship instrumentation that holds up.
Amplitude
Bad event tracking is the most common reason Amplitude projects fail. Here is the naming convention, the SDK code, and the Data Guard rules that keep your taxonomy clean for years — not weeks.
Customer.io
When a Customer.io workflow isn't firing, 80% of the time it's an event-delivery problem upstream, not the workflow itself. This is the diagnostic path that finds the root cause in minutes instead of days.