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.
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"