Customer.io Known Pitfalls
Overview
The 12 most common Customer.io integration mistakes, with the wrong pattern, the correct pattern, and why it matters. Use this as a code review checklist and developer onboarding reference.
The Pitfall Catalog
Pitfall 1: Wrong API Key Type
// WRONG — using Track API key for transactional messages
const api = new APIClient(process.env.CUSTOMERIO_TRACK_API_KEY!);
// Gets 401 because App API uses a DIFFERENT bearer token
// CORRECT — use the App API key
const api = new APIClient(process.env.CUSTOMERIO_APP_API_KEY!);
Why: Customer.io has two separate authentication systems. Track API uses Basic Auth (Site ID + Track Key). App API uses Bearer Auth (App Key). They are not interchangeable.
Pitfall 2: Millisecond Timestamps
// WRONG — JavaScript Date.now() returns milliseconds
await cio.identify("user-1", {
created_at: Date.now(), // 1704067200000 → year 55976
});
// CORRECT — Customer.io expects Unix seconds
await cio.identify("user-1", {
created_at: Math.floor(Date.now() / 1000), // 1704067200
});
Why: Customer.io accepts millisecond values without error but interprets them as seconds, resulting in dates thousands of years in the future. Segments using date comparisons silently break.
Pitfall 3: Track Before Identify
// WRONG — tracking before identifying creates orphaned events
await cio.track("new-user", { name: "signed_up", data: {} });
// User profile doesn't exist yet — event may be lost
// CORRECT — always identify first
await cio.identify("new-user", { email: "user@example.com" });
await cio.track("new-user", { name: "signed_up", data: {} });
Why: Track calls on non-existent users may be silently dropped. Always identify() before track().
Pitfall 4: Using Email as User ID
// WRONG — email can change, creating duplicate profiles
await cio.identify("user@example.com", { email: "user@example.com" });
// When user changes email, old profile orphaned, new one created
// CORRECT — use immutable database ID
await cio.identify("usr_abc123", {
email: "user@example.com", // Email as attribute, not ID
});
Why: The first argument to identify() is the permanent user ID. If you use email and the user changes it, you get two profiles. Use your database primary key instead.
Pitfall 5: Missing Email Attribute
// WRONG — user can't receive email campaigns
await cio.identify("user-1", {
first_name: "Jane",
plan: "p