Customer.io Connector
The Customer.io connector ingests email engagement events (opens, clicks, sends, bounces, unsubscribes) into GTM Clarity. It supports dual-mode ingestion -- real-time webhook push and historical API pull -- so you get both live event streaming and backfill capability.
Prerequisites
Before connecting Customer.io, you need:
- A Customer.io account with App API access (v2)
- A webhook reporting endpoint configured in Customer.io
- The environment variables listed below
Environment Variables
| Variable | Description |
|---|---|
CONNECTOR_ENCRYPTION_KEY | 32-byte hex string (64 hex chars) for AES-256-GCM API key encryption |
Customer.io Settings (stored per-tenant)
| Setting | Description |
|---|---|
encryptedApiKey | AES-256-GCM encrypted Customer.io App API key |
webhookSecret | HMAC-SHA256 signing secret for webhook verification |
Authentication
Customer.io uses API key authentication. The App API key is encrypted at rest using the same AES-256-GCM pattern as all other connectors:
- Algorithm:
aes-256-gcm - IV length: 12 bytes (randomly generated)
- Storage format:
base64(iv + tag + ciphertext) - Key:
CONNECTOR_ENCRYPTION_KEYenvironment variable
API key validation is performed against the Customer.io region endpoint (https://api.customer.io/v1/accounts/region). Keys do not expire.
Data Flow
Ingestion Modes
Mode 1: Webhook Push (Real-time)
Customer.io sends events to your webhook endpoint as they occur. Each request is verified before processing.
Webhook Verification
The webhook handler verifies every incoming request using HMAC-SHA256:
- The raw request body (as
Buffer) is hashed with the webhook secret - The resulting hex digest is compared to the
X-CIO-Signatureheader - Comparison uses
crypto.timingSafeEqualto prevent timing attacks - Invalid signatures immediately throw an error (HTTP 403)
// Verification signature
verifyWebhookSignature(body: Buffer, signature: string, secret: string): boolean
Webhook Event Structure
| Field | Type | Description |
|---|---|---|
event_id | string | Unique event identifier |
timestamp | number | Unix timestamp (seconds) |
event_type | string | Customer.io event type |
customer_id | string | Customer.io customer identifier |
email_address | string? | Recipient email |
campaign_id | string? | Campaign identifier |
broadcast_id | string? | Broadcast identifier |
subject | string? | Email subject line |
href | string? | Clicked link URL |
Mode 2: API Pull (Historical)
The API pull mode fetches historical campaign metrics from the Customer.io App API v2 for backfill and catch-up.
| Parameter | Value |
|---|---|
| Base URL | https://api.customer.io/v2 |
| Endpoint | /activities?start={timestamp}&limit=100 |
| Auth header | Authorization: Bearer {apiKey} |
| Pagination | Cursor-based via next URL |
| Backfill window | 90 days on initial sync |
| Page size | 100 events |
| Rate limit handling | 15ms delay between pages (~66 req/s) |
Customer.io has a soft limit of 100 requests per second. The connector paces at ~66 req/s with a 15ms inter-page delay, well under the limit.
Event Mapping
Customer.io events are mapped to canonical activity types:
| Customer.io Event | Canonical Type | Channel |
|---|---|---|
email_sent | email_sent | email |
email_opened | email_open | email |
email_clicked | email_click | email |
email_bounced | email_bounced | email |
email_unsubscribed | email_unsubscribed | email |
Unmapped event types are silently skipped (not an error). The transform functions return null for unknown types.
The email_open and email_click types feed directly into the engagement scoring engine. Opens and clicks contribute to the email channel score with time-based decay.
Default Field Mappings
| Source Field | Target Field | Transform | Required |
|---|---|---|---|
customer_id | personExternalId | none | Yes |
email_address | email | lowercase | No |
event_type | type | none | Yes |
timestamp | occurredAt | none | Yes |
campaign_id | properties.campaignId | none | No |
broadcast_id | properties.broadcastId | none | No |
subject | properties.subject | none | No |
href | properties.href | none | No |
Canonical Activity Output
Every Customer.io event produces a CanonicalActivity with these properties:
{
externalId: "evt_abc123", // event_id from webhook or id from API
type: "email_open", // Mapped type
channel: "email", // Always "email"
occurredAt: new Date(1709500000000),// Unix timestamp * 1000
personExternalId: "cust_456", // customer_id
properties: {
source: "customerio",
emailAddress: "user@example.com",
campaignId: "campaign_789",
subject: "Your weekly digest",
// ... additional metadata
}
}
Setup Steps
- Generate an App API key in Customer.io under Settings > App API
- Navigate to Settings > Connectors in the GTM Clarity dashboard
- Click "Connect Customer.io" and paste your API key (it will be encrypted at rest)
- Configure the webhook in Customer.io:
- Set the endpoint to
https://your-domain.com/api/connectors/customerio/webhook - Copy the webhook signing secret into your connector settings
- Select the email event types you want to forward
- Set the endpoint to
- Trigger initial sync to backfill 90 days of historical campaign metrics
- Verify webhook delivery by sending a test email and checking for activity records
Health Check
The health check validates the API key against the Customer.io region endpoint. A successful GET to https://api.customer.io/v1/accounts/region with a 200 response confirms the connector is healthy.
Troubleshooting
| Issue | Cause | Resolution |
|---|---|---|
| Webhook rejected (403) | Invalid HMAC signature | Verify the webhook secret matches between Customer.io and connector settings |
| API pull returns empty | API key lacks App API scope | Generate a new key with App API permissions |
| Missing events | Unmapped event type | Only email_sent, email_opened, email_clicked, email_bounced, and email_unsubscribed are mapped |
| Duplicate events | Both webhook and pull active | This is expected and safe -- deduplication handles overlapping records via externalId |