Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
Designed for Claude Code, also compatible with Codex and OpenClaw
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 messagesconst api = newAPIClient(process.env.CUSTOMERIO_TRACK_API_KEY!);
// Gets 401 because App API uses a DIFFERENT bearer token// CORRECT — use the App API keyconst api = newAPIClient(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.
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.
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 profilesawait cio.identify("user@example.com", { email: "user@example.com" });
// When user changes email, old profile orphaned, new one created// CORRECT — use immutable database IDawait 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 campaignsawait cio.identify("user-1", {
first_name: "Jane",
plan: "pro",
// No email attribute!
});
// CORRECT — always include email for email campaignsawait cio.identify("user-1", {
email: "jane@example.com",
first_name: "Jane",
plan: "pro",
});
Why: Without an email attribute, the user profile exists but can't receive any email campaigns or transactional messages.
Pitfall 6: Dynamic Event Names
// WRONG — creates hundreds of unique event namesawait cio.track("user-1", {
name: `viewed_${productId}`, // "viewed_SKU-12345"data: {},
});
// CORRECT — use a static name with data propertiesawait cio.track("user-1", {
name: "product_viewed", // Consistent, filterabledata: { product_id: productId }, // Dynamic data in properties
});
Why: Dynamic event names pollute your event catalog and make it impossible to create campaign triggers. Use a fixed set of event names and pass variations as data properties.
Why: Event names are case-sensitive. Inconsistent naming means campaign triggers only match one variant, and your event catalog becomes a mess.
Pitfall 11: No Rate Limiting
// WRONG — blasting API at full speed during importfor (const user of allUsers) {
await cio.identify(user.id, user.attrs); // 1000+ req/sec → 429 errors
}
// CORRECT — rate-limited processingimportBottleneckfrom"bottleneck";
const limiter = newBottleneck({ maxConcurrent: 10, minTime: 15 });
for (const user of allUsers) {
await limiter.schedule(() => cio.identify(user.id, user.attrs));
}
Why: Customer.io rate limits at ~100 req/sec. Without throttling, bulk operations trigger 429 errors and potentially get your API key temporarily blocked.
Pitfall 12: PII in Event Names
// WRONG — PII in event names is unsanitizableawait cio.track(userId, { name: `email_sent_to_john@example.com` });
// CORRECT — PII only in data properties (can be deleted per GDPR)await cio.track(userId, {
name: "email_sent",
data: { recipient: "john@example.com" },
});
Why: Event names are indexed and cached. PII in event names can't be deleted for GDPR compliance. Always put PII in event data properties.
Quick Reference
#
Pitfall
Fix
1
Wrong API key type
Track key for tracking, App key for transactional
2
Millisecond timestamps
Math.floor(Date.now() / 1000)
3
Track before identify
Always identify() first
4
Email as user ID
Use immutable database ID
5
Missing email attribute
Include email in identify()
6
Dynamic event names
Static names, dynamic data properties
7
Blocking request path
Fire-and-forget with .catch()
8
No bounce handling
Suppress bounced users via webhook
9
New client per request
Singleton pattern
10
Inconsistent event names
Always snake_case
11
No rate limiting
Use Bottleneck or p-queue
12
PII in event names
PII in data properties only
Integration Audit Script
# Quick grep audit for common pitfalls in your codebaseecho"=== Customer.io Pitfall Audit ==="echo"--- Checking for Date.now() without /1000 ---"
grep -rn "created_at.*Date.now()" --include="*.ts" --include="*.js" \
| grep -v "/ 1000" || echo"OK"echo"--- Checking for new TrackClient inside functions ---"
grep -rn "new TrackClient" --include="*.ts" --include="*.js" \
| grep -v "^.*const\|^.*let\|^.*export" || echo"OK"echo"--- Checking for dynamic event names ---"
grep -rn "name:.*\`" --include="*.ts" --include="*.js" \
| grep "track\|Track" || echo"OK"echo"--- Checking for blocking await in routes ---"
grep -rn "await.*cio\.\|await.*track\.\|await.*identify" --include="*.ts" \
| grep "router\.\|app\." || echo"Review these for fire-and-forget"