Skip to content
customerio-webhooks-events logo

Customer.io Webhooks & Events

customerio-webhooks-events

Implement Customer.io webhook and reporting event handling.

SKILL.md

Full skill instructions

Customer.io Webhooks & Events

Overview

Implement Customer.io reporting webhook handling: receive real-time delivery events (sent, delivered, opened, clicked, bounced, complained, unsubscribed), verify HMAC-SHA256 signatures, process events reliably with queuing, and stream to a data warehouse.

How Reporting Webhooks Work

Customer.io                    Your Server                   Data Warehouse
──────────                    ───────────                   ──────────────
Email sent   →  POST /​webhooks/​cio  →  Verify signature
Email opened →  POST /​webhooks/​cio  →  Parse event type
Link clicked →  POST /​webhooks/​cio  →  Route to handler  →  INSERT INTO events
Email bounced → POST /​webhooks/​cio  →  Suppress user

Configure at: Data & Integrations > Integrations > Reporting Webhooks

Prerequisites

  • Public HTTPS endpoint for webhook receiver
  • Webhook signing key from Customer.io dashboard
  • Express or similar HTTP framework

Instructions

Step 1: Define Webhook Event Types

// types/​customerio-webhooks.ts

// Customer.io reporting webhook event metrics
type CioMetric =
  | "sent"          // Message sent to delivery provider
  | "delivered"     // Delivery provider confirmed receipt
  | "opened"        // Recipient opened the email
  | "clicked"       // Recipient clicked a link
  | "converted"     // Recipient completed a conversion goal
  | "bounced"       // Email bounced (hard or soft)
  | "spammed"       // Recipient marked as spam
  | "unsubscribed"  // Recipient unsubscribed
  | "dropped"       // Message dropped (suppressed, invalid)
  | "deferred"      // Delivery temporarily deferred
  | "failed";       // Delivery failed

interface CioWebhookEvent {
  // Event metadata
  event_id: string;
  metric: CioMetric;
  timestamp: number;          // Unix seconds

  // Recipient info
  customer_id: string;
  email_address?: string;

  // Message info
  subject?: string;
  template_id?: number;
  campaign_id?: number;
  broadcast_id?: number;
  action_id?: number;

  // Delivery details
  delivery_id?: string;

  // Link tracking (for "clicked" events)
  href?: string;
  link_id?: number;
}

Step 2: Webhook Handler with Signature Verification

// routes/​webhooks/​customerio.ts
import { createHmac, timingSafeEqual } from "crypto";
import { Router, Request, Response } from "express";

const WEBHOOK_SECRET = process.env.CUSTOMERIO_WEBHOOK_SECRET!;

function verifySignature(rawBody: Buffer, signature: string): boolean {
  const expected = createHmac("sha256", WEBHOOK_SECRET)
    .update(rawBody)
    .digest("hex");

  try {
    return timingSafeEqual(
      Buffer.from(signature, "utf-8"),
      Buffer.from(expected, "utf-8")
    );
  } catch {
    return false;
  }
}

const router = Router();

// IMPORTANT: Use raw body parser for this route — JSON parsing breaks signature verification
router.post("/​webhooks/​customerio", (req: Request, res: Response) => {
  const signature = req.headers["x-cio-signature"] as string;
  const rawBody = (req as any).rawBody as Buffer;

  if (!signature || !rawBody) {
    res.status(401).json({ error: "Missing signature" });
    return;
  }

  if (!verifySignature(rawBody, signature)) {
    console.error("CIO webhook: invalid signature");
    res.status(401).json({ error: "Invalid signature" });
    return;
  }

  const event: CioWebhookEvent = JSON.parse(rawBody.toString());

  // Respond 200 immediately — process async to avoid timeouts
  res.sendStatus(200);

  // Process event asynchronously
  handleWebhookEvent(event).catch((err) =>
    console.error("Webhook processing failed:", err)
  );
});

Step 3: Event Router and Handlers

// services/​customerio-webhook-handler.ts
import { TrackClient, RegionUS } from "customerio-node";

const cio = new TrackClient(
  process.env.CUSTOMERIO_SITE_ID!,
  process.env.CUSTOMERIO_TRACK_API_KEY!,
  { region: RegionUS }
);

// Deduplication set (use Redis in production)
const processedEvents = new Set<string>();

async function handleWebhookEvent(event: CioWebhookEvent): Promise<void> {
  // Deduplicate by event_id
  if (processedEvents.has(event.event_id)) return;
  processedEvents.add(event.event_id);

  // Limit in-memory set size
  if (processedEvents.size > 100_000) {
    const iterator = processedEvents.values();
    for (let i = 0; i < 50_000; i++) iterator.next();
    // In production, use Redis with TTL instead
  }

  switch (event.metric) {
    case "bounced":
      await handleBounce(event);
      break;
    case "spammed":
      await handleSpamComplaint(event);
      break;
    case "unsubscribed":
      await handleUnsubscribe(event);
      break;
    case "opened":
    case "clicked":
      await handleEngagement(event);
      break;
    case "delivered":
    case "sent":
      await handleDelivery(event);
      break;
    default:
      console.log(`CIO webhook: ${event.metric} for ${event.customer_id}`);
  }
}

async function handleBounce(event: CioWebhookEvent): Promise<void> {
  console.warn(`BOUNCE: ${event.email_address} (user: ${event.customer_id})`);

  // Update user profile with bounce info
  await cio.identify(event.customer_id, {
    email_bounced: true,
    email_bounced_at: event.timestamp,
    last_bounce_delivery_id: event.delivery_id,
  });

  // Suppress after hard bounce to protect sender reputation
  await cio.suppress(event.customer_id);
}

async function handleSpamComplaint(event: CioWebhookEvent): Promise<void> {
  console.error(`SPAM COMPLAINT: ${event.email_address}`);

  // Suppress immediately — spam complaints damage sender reputation
  await cio.suppress(event.customer_id);

  // Record for compliance
  await cio.identify(event.customer_id, {
    spam_complaint: true,
    spam_complaint_at: event.timestamp,
  });
}

async function handleUnsubscribe(event: CioWebhookEvent): Promise<void> {
  await cio.identify(event.customer_id, {
    unsubscribed: true,
    unsubscribed_at: event.timestamp,
  });
}

async function handleEngagement(event: CioWebhookEvent): Promise<void> {
  await cio.identify(event.customer_id, {
    last_email_engaged_at: event.timestamp,
    last_email_metric: event.metric,
  });
}

async function handleDelivery(event: CioWebhookEvent): Promise<void> {
  // Log for analytics — lightweight processing
  console.log(
    `${event.metric}: ${event.delivery_id} to ${event.customer_id}`
  );
}

Step 4: Express Server Setup

// server.ts
import express from "express";
import webhookRouter from "./​routes/​webhooks/​customerio";

const app = express();

// Raw body parser for webhook signature verification
// MUST be before any JSON body parser
app.use(
  "/​webhooks/​customerio",
  express.raw({ type: "application/​json" }),
  (req, _res, next) => {
    (req as any).rawBody = req.body;
    req.body = JSON.parse(req.body.toString());
    next();
  }
);

// JSON parser for all other routes
app.use(express.json());

app.use(webhookRouter);

app.listen(3000, () => console.log("Webhook server on :3000"));

Step 5: Stream to Data Warehouse (BigQuery)

// services/​customerio-warehouse.ts
import { BigQuery } from "@google-cloud/​bigquery";

const bq = new BigQuery();
const dataset = bq.dataset("messaging");
const table = dataset.table("customerio_events");

async function streamToWarehouse(event: CioWebhookEvent): Promise<void> {
  await table.insert({
    event_id: event.event_id,
    metric: event.metric,
    customer_id: event.customer_id,
    email_address: event.email_address,
    delivery_id: event.delivery_id,
    campaign_id: event.campaign_id,
    template_id: event.template_id,
    href: event.href,
    timestamp: new Date(event.timestamp * 1000).toISOString(),
    received_at: new Date().toISOString(),
  });
}

Dashboard Configuration

  1. Go to Data & Integrations > Integrations > Reporting Webhooks
  2. Click Add Reporting Webhook
  3. Enter your endpoint URL: https://your-domain.com/​webhooks/​customerio
  4. Select events to receive (recommended: all for analytics)
  5. Copy the webhook signing key to CUSTOMERIO_WEBHOOK_SECRET

Error Handling

IssueSolution
Invalid signatureVerify webhook secret matches dashboard value
Duplicate eventsDeduplicate by event_id (use Redis SET with TTL in production)
Slow processingReturn 200 immediately, process async
Missing eventsCheck endpoint is publicly accessible, verify IP allowlist

Resources

Next Steps

After webhook setup, proceed to customerio-performance-tuning for optimization.

More skills from jeremylongshore

openrouter-compliance-review logo
jeremylongshore/claude-code-plugins-plus-skills

openrouter-compliance-review

Review OpenRouter integration for regulatory compliance (SOC2, GDPR,

2.8K 0
View
maintainx-data-handling logo
jeremylongshore/claude-code-plugins-plus-skills

maintainx-data-handling

Data synchronization, ETL patterns, and data management for MaintainX.

2.8K 0
View
groq-core-workflow-a logo
jeremylongshore/claude-code-plugins-plus-skills

groq-core-workflow-a

Execute Groq primary workflow: chat completions with tool use and JSON

2.8K 0
View
validating-csrf-protection logo
jeremylongshore/claude-code-plugins-plus-skills

validating-csrf-protection

Validate CSRF protection implementations for security gaps. Use when

2.8K 0
View
api-contract logo
jeremylongshore/claude-code-plugins-plus-skills

api-contract

Configure this skill should be used when the user asks about "API contract",

2.8K 0
View
ideogram-incident-runbook logo
jeremylongshore/claude-code-plugins-plus-skills

ideogram-incident-runbook

Execute Ideogram incident response with triage, mitigation, and postmortem.

2.8K 0
View
gamma-sdk-patterns logo
jeremylongshore/claude-code-plugins-plus-skills

gamma-sdk-patterns

Reusable patterns for the Gamma REST API (no SDK exists).

2.8K 0
View
deepgram-sdk-patterns logo
jeremylongshore/claude-code-plugins-plus-skills

deepgram-sdk-patterns

Apply production-ready Deepgram SDK patterns for TypeScript and Python.

2.8K 0
View
openrouter-upgrade-migration logo
jeremylongshore/claude-code-plugins-plus-skills

openrouter-upgrade-migration

Migrate to OpenRouter from direct provider APIs or upgrade between SDK/model

2.8K 0
View
openrouter-common-errors logo
jeremylongshore/claude-code-plugins-plus-skills

openrouter-common-errors

Diagnose and fix common OpenRouter API errors. Use when encountering

2.8K 0
View
linear-cost-tuning logo
jeremylongshore/claude-code-plugins-plus-skills

linear-cost-tuning

Optimize Linear API usage, reduce unnecessary calls, and maximize

2.8K 0
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View