const NEVER_SEND = new Set([
"ssn", "social_security", "tax_id",
"credit_card", "card_number", "cvv",
"password", "password_hash",
"bank_account", "routing_number",
]);
const HASH_FIELDS = new Set([
"phone", "phone_number",
"ip_address", "ip",
"address", "street_address",
]);
import { createHash } from "crypto";
function hashValue(value: string): string {
return createHash("sha256").update(value).digest("hex").substring(0, 16);
}
export function sanitizeAttributes(
attrs: Record<string, any>
): Record<string, any> {
const clean: Record<string, any> = {};
for (const [key, value] of Object.entries(attrs)) {
const lowerKey = key.toLowerCase();
if (NEVER_SEND.has(lowerKey)) continue;
if (HASH_FIELDS.has(lowerKey) && typeof value === "string") {
clean[`${key}_hash`] = hashValue(value);
continue;
}
clean[key] = value;
}
return clean;
}
import { TrackClient, RegionUS } from "customerio-node";
const cio = new TrackClient(siteId, trackKey, { region: RegionUS });
await cio.identify("user-123", sanitizeAttributes({
email: "user@example.com",
first_name: "Jane",
phone: "+1-555-0123",
ssn: "123-45-6789",
plan: "pro",
}));
import { createHmac, timingSafeEqual } from "crypto";
import { Request, Response, NextFunction } from "express";
const WEBHOOK_SECRET = process.env.CUSTOMERIO_WEBHOOK_SECRET!;
export function verifyCioWebhook(
req: Request,
res: Response,
next: NextFunction
): void {
const signature = req.headers["x-cio-signature"] as string;
if (!signature) {
res.status(401).json({ error: "Missing signature header" });
return;
}
const rawBody = (req as any).rawBody as Buffer;
if (!rawBody) {
res.status(500).json({ error: "Raw body not available" });
return;
}
const expected = createHmac("sha256", WEBHOOK_SECRET)
.update(rawBody)
.digest("hex");
const valid = timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
if (!valid) {
res.status(401).json({ error: "Invalid signature" });
return;
}
next();
}
import express from "express";
const app = express();
app.use("/webhooks/customerio", express.raw({ type: "application/json" }));
app.post("/webhooks/customerio", verifyCioWebhook, (req, res) => {
const event = JSON.parse((req as any).rawBody.toString());
res.sendStatus(200);
});
async function rotateKeys() {
console.log("Customer.io Key Rotation Procedure:");
console.log("1. Go to Settings > Workspace Settings > API & Webhook Credentials");
console.log("2. Click 'Regenerate' next to the key you want to rotate");
console.log("3. Copy the NEW key");
console.log("4. Update your secrets manager:");
console.log(" - GCP: gcloud secrets versions add customerio-track-api-key --data-file=-");
console.log(" - AWS: aws ssm put-parameter --name /cio/track-api-key --value NEW_KEY --overwrite");
console.log("5. Deploy your application (or restart to pick up new secrets)");
console.log("6. Verify: run the connectivity test script");
console.log("7. The old key is immediately invalidated upon regeneration");
console.log("");
console.log("IMPORTANT: Regenerating a key IMMEDIATELY invalidates the old key.");
console.log("Update secrets BEFORE regenerating, or plan for brief downtime.");
}
import { TrackClient, RegionUS } from "customerio-node";
const cio = new TrackClient(
process.env.CUSTOMERIO_SITE_ID!,
process.env.CUSTOMERIO_TRACK_API_KEY!,
{ region: RegionUS }
);
async function handleDeletionRequest(userId: string): Promise<void> {
await cio.suppress(userId);
console.log(`User ${userId} suppressed (no more messages)`);
await cio.destroy(userId);
console.log(`User ${userId} deleted from Customer.io`);
console.log(`GDPR deletion completed for ${userId} at ${new Date().toISOString()}`);
}
async function bulkDelete(userIds: string[]): Promise<void> {
for (const userId of userIds) {
try {
await handleDeletionRequest(userId);
} catch (err: any) {
console.error(`Deletion failed for ${userId}: ${err.message}`);
}
await new Promise((r) => setTimeout(r, 100));
}
}