Skip to main content
Technology & EngineeringEmail Services276 lines

Customerio

Send transactional and marketing email with Customer.io. Use this skill when the

Quick Summary34 lines
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 lines
Paste into your CLAUDE.md or agent config

Customer.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_at as 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 identify with 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

Get CLI access →