Agent skill
Customer.io Webhooks Skill
Receive and verify Customer.io Reporting Webhooks. Use when setting up Customer.io webhook handlers, debugging X-CIO-Signature verification, or handling messaging events like email delivered, email opened, email clicked, email bounced, sms sent, push delivered, or customer unsubscribed.
Install this skill
npx skills add hookdeck/webhook-skills --skill customerio-webhooks
When to Use This Skill
- How do I receive Customer.io Reporting Webhooks?
- How do I verify Customer.io webhook signatures (
X-CIO-Signature)? - How do I handle
emaildelivered,opened,clicked, orbouncedevents? - Why is my Customer.io webhook signature verification failing?
- How do I identify events by
object_type+metricinstead of a dotted name?
How Customer.io Webhooks Are Different
- No single event-name string. Each POST is one event object. Identify it by the
object_type(customer,email,push,sms,in_app,slack,webhook,whatsapp) plus themetric(sent,delivered,opened,clicked,bounced,dropped,spammed,failed,converted,unsubscribed, …). There is noemail.openedfield — you build that pair yourself fromobject_type+metric. - Custom signature scheme (not Standard Webhooks). The signed string is
v0:<X-CIO-Timestamp>:<raw body>, HMAC-SHA256, hex digest. There are nowebhook-id/webhook-signatureheaders. - No verification SDK.
customerio-nodeand thecustomeriopip package are API clients only — they do not ship webhook signature helpers. Verify manually (shown below). - Strict 4-second timeout. Return
2xxwithin 4 seconds or Customer.io retries with exponential backoff for 7 days and backlogs later events. Do heavy work asynchronously.
Verification (core)
Build the string v0:<X-CIO-Timestamp>:<raw body> (version is always v0), HMAC-SHA256 it with your webhook signing key, and hex-compare against X-CIO-Signature. Use the raw, unmodified body — don't JSON.parse first.
const crypto = require('crypto');
function verifyCustomerIoWebhook(rawBody, timestamp, signature, signingKey) {
if (!timestamp || !signature) return false;
// Signed content: "v0:<timestamp>:<raw body>". Feed the raw body straight
// into the HMAC so it is never re-encoded.
const hmac = crypto.createHmac('sha256', signingKey);
hmac.update(`v0:${timestamp}:`);
hmac.update(rawBody); // Buffer or string of the unmodified request body
const expected = hmac.digest('hex');
try {
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
);
} catch {
return false; // length mismatch / non-hex signature
}
}
For complete handlers with route wiring, event dispatch, and tests, see:
Common Event Types (object_type + metric)
object_type | metric | Fires when |
|---|---|---|
email | sent | Message handed to the sending provider |
email | delivered | Recipient's mail server accepted the message |
email | opened | Recipient opened the email |
email | clicked | Recipient clicked a tracked link (data.href, data.link_id) |
email | bounced | Delivery hard/soft bounced |
email | dropped | Customer.io dropped before sending (suppression, etc.) |
email | spammed | Recipient marked the email as spam |
email | converted | Recipient completed the campaign conversion goal |
sms | sent / delivered / clicked | SMS lifecycle |
push | sent / delivered / opened | Push lifecycle |
customer | subscribed / unsubscribed | Subscription state changed |
The same metric appears across object_types — always branch on both. See references/overview.md for the full matrix.
Environment Variables
# Signing key from the Reporting Webhooks integration page (account settings)
CUSTOMERIO_WEBHOOK_SIGNING_KEY=your_signing_key
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 customerio --path /webhooks/customerio
Reference Materials
- references/overview.md - Customer.io webhook concepts, full event matrix
- references/setup.md - Dashboard configuration, signing key
- references/verification.md - Signature verification details and gotchas