Customerio
Send transactional and marketing email with Customer.io. Use this skill when the
You are an expert in Customer.io for transactional and marketing email. You understand
the Track API for customer data and events, the Transactional API for triggered sends,
campaign workflows, segmentation, Liquid templating, and behavioral email automation.
## Key Points
- **Confusing the two API keys** -- The Track API uses site_id + API key. The Transactional API uses a separate App API key. Mixing them causes auth errors with unhelpful error messages.
- **Use the Track API for campaigns, Transactional API for triggered sends.** Campaign
- **Send `created_at` as a Unix timestamp.** Customer.io uses this to anchor time-based
- **Use Liquid templates for personalization.** Customer.io templates support Liquid.
- **Segment in Customer.io, not in your code.** Let Customer.io handle audience
- **Set up a suppression workflow.** When you receive bounce or spam webhooks, update
- **Prefer identified users over anonymous events.** Anonymous events are useful for
- **Confusing the two API keys.** The Track API uses site_id + API key. The
- **Sending strings instead of Unix timestamps.** Customer.io expects integer Unix
- **Not idempotently identifying users.** Calling `identify` with a new email address
- **Exceeding the Track API rate limit.** The Track API allows ~100 requests/second
- **Forgetting to handle unsubscribe in transactional messages.** Transactional
## Quick Example
```bash
npm install customerio-node
```
```typescript
import { APIClient, SendEmailRequest, RegionUS } from "customerio-node";
const api = new APIClient(process.env.CUSTOMERIO_APP_API_KEY!, {
region: RegionUS,
});
```skilldb get email-services-skills/CustomerioFull skill: 276 linesCustomer.io — Email Service
You are an expert in Customer.io for transactional and marketing email. You understand the Track API for customer data and events, the Transactional API for triggered sends, campaign workflows, segmentation, Liquid templating, and behavioral email automation.
Core Philosophy
Customer.io bridges the gap between developer-managed data and marketer-designed campaigns. The developer's responsibility is to keep the customer data model accurate and current -- identifying users with correct attributes and tracking events that represent meaningful business actions. The marketer's responsibility is to build campaigns in the visual workflow builder that react to those events and segments. When this separation is clean, marketing can iterate on email content and targeting without requiring code changes.
Events are the heartbeat of Customer.io. Every significant user action -- completing onboarding, making a purchase, upgrading a plan, abandoning a cart -- should be tracked as a named event with structured data. These events trigger campaign workflows, update segments, and drive personalization. A Customer.io workspace with rich event data enables sophisticated behavioral automation; one with only identify calls is just an expensive contact database.
Keep transactional and campaign email on separate rails. The Transactional API exists for immediate, one-off messages (password resets, receipts, security alerts) that must bypass campaign unsubscribe preferences. Campaign emails (onboarding drips, re-engagement sequences, product announcements) go through the workflow builder and respect user preferences. Mixing these -- sending marketing content through the Transactional API -- violates consent laws and erodes trust.
Anti-Patterns
- Sending marketing content through the Transactional API -- The Transactional API bypasses unsubscribe preferences by design. Using it for promotional content violates CAN-SPAM/GDPR and damages sender reputation.
- Passing ISO date strings instead of Unix timestamps -- Customer.io expects integer Unix timestamps for date attributes. ISO strings cause silent failures in time-based segments and campaign delays.
- Looping individual identify calls for bulk imports -- The Track API rate limit is approximately 100 requests/second. Bulk importing thousands of users via individual calls risks rate limiting. Use the batch endpoint or CSV import.
- Duplicating audience logic in application code -- Let Customer.io's segment builder handle targeting. Your code should focus on keeping attributes and events accurate; segmentation belongs in the platform.
- Confusing the two API keys -- The Track API uses site_id + API key. The Transactional API uses a separate App API key. Mixing them causes auth errors with unhelpful error messages.
Overview
Customer.io is a messaging automation platform that sends emails (and push, SMS, in-app) triggered by customer behavior. Unlike pure transactional email APIs, Customer.io combines a customer data platform with a visual workflow builder. Developers feed customer attributes and events via the Track API; marketers build campaigns in the UI that react to those events. The separate Transactional API handles one-off triggered messages (password resets, receipts) outside of campaign workflows.
Setup & Configuration
Installation
npm install customerio-node
Track API client (for customer data and events)
import { TrackClient, RegionUS, RegionEU } from "customerio-node";
const tracker = new TrackClient(
process.env.CUSTOMERIO_SITE_ID!,
process.env.CUSTOMERIO_API_KEY!,
{ region: RegionUS } // or RegionEU for EU data center
);
Transactional API client (for triggered sends)
import { APIClient, SendEmailRequest, RegionUS } from "customerio-node";
const api = new APIClient(process.env.CUSTOMERIO_APP_API_KEY!, {
region: RegionUS,
});
Environment variables
CUSTOMERIO_SITE_ID=your-site-id
CUSTOMERIO_API_KEY=your-tracking-api-key
CUSTOMERIO_APP_API_KEY=your-app-api-key
Domain authentication
Configure your sending domain in Customer.io Settings > Email. Add the provided DKIM and SPF DNS records. Enable a custom tracking domain for link tracking to align with your brand and improve deliverability under strict DMARC policies.
Core Patterns
Identify a customer (create or update)
async function identifyCustomer(
userId: string,
attributes: Record<string, unknown>
) {
await tracker.identify(userId, {
email: attributes.email,
first_name: attributes.firstName,
created_at: Math.floor(Date.now() / 1000), // Unix timestamp
plan: attributes.plan,
...attributes,
});
}
// Usage — call on signup or profile update
await identifyCustomer("user_123", {
email: "alice@example.com",
firstName: "Alice",
plan: "pro",
});
Track an event (triggers campaign workflows)
async function trackEvent(
userId: string,
eventName: string,
data: Record<string, unknown> = {}
) {
await tracker.track(userId, {
name: eventName,
data,
});
}
// Usage — this event can trigger a campaign in the workflow builder
await trackEvent("user_123", "order_completed", {
orderId: "ORD-456",
total: 79.99,
items: ["Widget A", "Widget B"],
});
Send a transactional email
async function sendTransactional(
to: string,
transactionalMessageId: number,
data: Record<string, unknown>
) {
const request = new SendEmailRequest({
to,
transactional_message_id: transactionalMessageId,
message_data: data,
identifiers: { email: to },
});
const response = await api.sendEmail(request);
return response; // { delivery_id, queued_at }
}
// Usage — transactional message created in Customer.io UI
await sendTransactional("alice@example.com", 1, {
resetUrl: "https://app.example.com/reset?token=abc",
expiresIn: "24 hours",
});
Track anonymous events (pre-signup)
async function trackAnonymousEvent(
anonymousId: string,
eventName: string,
data: Record<string, unknown>
) {
await tracker.trackAnonymous(anonymousId, {
name: eventName,
data,
});
}
// Later, when user signs up, merge anonymous activity:
await tracker.mergeCustomers("cio_" + anonymousId, "user_123");
Delete a customer (GDPR compliance)
async function deleteCustomer(userId: string) {
await tracker.destroy(userId);
}
Handle webhooks for reporting
import { Router, Request, Response } from "express";
import crypto from "crypto";
const router = Router();
function verifySignature(req: Request): boolean {
const signature = req.headers["x-cio-signature"] as string;
const hmac = crypto
.createHmac("sha256", process.env.CUSTOMERIO_WEBHOOK_SECRET!)
.update(JSON.stringify(req.body))
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(hmac));
}
router.post("/webhooks/customerio", (req: Request, res: Response) => {
if (!verifySignature(req)) {
return res.status(401).end();
}
const event = req.body;
switch (event.metric) {
case "delivered":
// Email reached inbox
break;
case "opened":
// Recipient opened
break;
case "clicked":
// Link clicked — event.data.href has the URL
break;
case "bounced":
// Handle bounce
break;
case "spammed":
// Spam complaint — suppress address
break;
case "unsubscribed":
// User opted out
break;
}
res.status(200).end();
});
Best Practices
- Use the Track API for campaigns, Transactional API for triggered sends. Campaign emails (onboarding drips, re-engagement) are driven by events and segments in the workflow builder. One-off messages (password resets, receipts) go through the Transactional API for immediate, reliable delivery.
- Send
created_atas a Unix timestamp. Customer.io uses this to anchor time-based segments and campaign delays. Without it, time-based workflows will not trigger correctly. - Use Liquid templates for personalization. Customer.io templates support Liquid.
Pass structured data in events and attributes so templates can use conditionals
and loops:
{% if customer.plan == "pro" %}. - Segment in Customer.io, not in your code. Let Customer.io handle audience targeting via its segment builder. Your code should focus on keeping attributes and events accurate and up to date.
- Set up a suppression workflow. When you receive bounce or spam webhooks, update your internal records. Customer.io handles suppression automatically for campaigns, but your app should reflect these states in its own user model.
- Prefer identified users over anonymous events. Anonymous events are useful for pre-signup tracking, but merge them into identified profiles as soon as possible to avoid orphaned data.
Common Pitfalls
- Confusing the two API keys. The Track API uses site_id + API key. The Transactional API uses a separate App API key. Mixing them up causes auth errors with no helpful message.
- Sending strings instead of Unix timestamps. Customer.io expects integer Unix timestamps for date attributes. Passing ISO strings causes silent filtering failures in segments.
- Not idempotently identifying users. Calling
identifywith a new email address for an existing user_id updates the profile — it does not create a duplicate. But calling it with a new user_id and the same email creates a duplicate. Deduplicate by user_id, not email. - Exceeding the Track API rate limit. The Track API allows ~100 requests/second per workspace by default. For bulk imports, use the batch endpoint or the Customer.io data import CSV feature instead of looping identify calls.
- Forgetting to handle unsubscribe in transactional messages. Transactional messages bypass campaign unsubscribe preferences by design. If you send marketing content through the Transactional API, you violate consent — keep transactional content strictly transactional.
Install this skill directly: skilldb add email-services-skills
Related Skills
AWS Ses
Send email at scale with Amazon SES (Simple Email Service). Use this skill when
Brevo
Send transactional and marketing email with Brevo (formerly Sendinblue). Use this
Courier
Send transactional notifications including email with Courier. Use this skill when
Email Deliverability
Optimize email deliverability across any provider. Use this skill when the project
Loops
Send transactional and marketing email with Loops. Use this skill when the project
Mailchimp Transactional
Send transactional email with Mailchimp Transactional (formerly Mandrill). Use