# Customer.io 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 `email` `delivered`, `opened`, `clicked`, or `bounced` events?
* Why is my Customer.io webhook signature verification failing?
* How do I identify events by `object_type` + `metric` instead 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 the `metric` (`sent`, `delivered`, `opened`, `clicked`, `bounced`, `dropped`,
  `spammed`, `failed`, `converted`, `unsubscribed`, …). There is no `email.opened` field — you
  build that pair yourself from `object_type` + `metric`.
* Custom signature scheme (not Standard Webhooks). The signed string is
  `v0:<X-CIO-Timestamp>:<raw body>`, HMAC-SHA256, hex digest. There are no
  `webhook-id` / `webhook-signature` headers.
* No verification SDK. `customerio-node` and the `customerio` pip package are API clients
  only — they do not ship webhook signature helpers. Verify manually (shown below).
* Strict 4-second timeout. Return `2xx` within 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.

```javascript
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:
> 
> * [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/customerio-webhooks/examples/express/)
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/customerio-webhooks/examples/nextjs/)
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/customerio-webhooks/examples/fastapi/)

## 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_type`s — always branch on both. See
[references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/customerio-webhooks/references/overview.md) for the full matrix.

## Environment Variables

```bash
# Signing key from the Reporting Webhooks integration page (account settings)
CUSTOMERIO_WEBHOOK_SIGNING_KEY=your_signing_key

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 customerio --path /webhooks/customerio

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/customerio-webhooks/references/overview.md) - Customer.io webhook concepts, full event matrix
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/customerio-webhooks/references/setup.md) - Dashboard configuration, signing key
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/customerio-webhooks/references/verification.md) - Signature verification details and gotchas