Skip to main content

Customer.io

Server Source code Package Beta

Server-side event delivery to Customer.io via the official customerio-node SDK. Tracks events with TrackClient.track(), manages identities with identify(), and supports the full lifecycle: page views, device registration, suppress/unsuppress, profile merging, and transactional messaging via APIClient.sendEmail() / sendPush().

Where this fits

Customer.io is a server destination in the walkerOS flow:

Receives events server-side from the collector, resolves a `customerId` (or falls back to `anonymousId`), optionally fires `identify()`, then forwards the event to Customer.io for campaigns, broadcasts, and transactional messaging.

Installation

npm install @walkeros/server-destination-customerio
import { startFlow } from '@walkeros/collector';
import { destinationCustomerIo } from '@walkeros/server-destination-customerio';

await startFlow({
destinations: {
  customerio: {
    code: destinationCustomerIo,
    config: {
      settings: {
        siteId: 'YOUR_SITE_ID',
        apiKey: 'YOUR_API_KEY',
      },
    },
  },
},
});

Configuration

This destination uses the standard destination config wrapper (consent, data, env, id, ...). For the shared fields see destination configuration. Package-specific fields live under config.settings and are listed below.

Settings

PropertyTypeDescriptionMore
siteId*stringCustomer.io Site ID. Find it in Settings > Workspace Settings > API Credentials.
apiKey*stringCustomer.io API Key. Find it in Settings > Workspace Settings > API Credentials.
appApiKeystringApp API Key for transactional messaging (sendEmail/sendPush). Find it in Settings > Workspace Settings > API Credentials > App API Keys.
region'us' | 'eu'Data center region. Must match where your Customer.io workspace was created. Default: us.
timeoutintegerHTTP request timeout in milliseconds. Default: 10000.
customerIdstringwalkerOS mapping value path to resolve customerId from each event (like user.id).
anonymousIdstringwalkerOS mapping value path to resolve anonymousId from each event (like user.session).
identifyanyDestination-level identity mapping. Resolves to { email?, first_name?, ... } attributes. Fires identify() on first push and re-fires when values change.
* Required fields

Mapping

Per-event rules under config.mapping. For the standard rule fields (consent, condition, data, batch, name, policy) see mapping.

PropertyTypeDescriptionMore
identifyanyPer-event identify attributes. Resolves to { email?, first_name?, ... }. Use with silent: true on login/identify events.
pageanyPer-event page view. Resolves to { url, ... }. Calls trackPageView(). Use with silent: true.
destroybooleanPermanently delete person from Customer.io. Set true on delete events with silent: true.
suppressbooleanSuppress person (stop messaging without deleting data). Set true with silent: true.
unsuppressbooleanUnsuppress person (resume messaging). Set true with silent: true.
addDeviceanyRegister push device. Resolves to { deviceId, platform, data? }. Use with silent: true.
deleteDeviceanyRemove push device. Resolves to { deviceId, platform }. Use with silent: true.
mergeanyMerge duplicate profiles. Resolves to { primaryType, primaryId, secondaryType, secondaryId }. Use with silent: true.
sendEmailanySend transactional email. Resolves to { to, transactional_message_id, message_data?, identifiers? }. Requires appApiKey. Use with silent: true.
sendPushanySend transactional push. Resolves to { transactional_message_id, message_data?, identifiers? }. Requires appApiKey. Use with silent: true.

Examples

Anonymous track

When no customer id is resolved the event is sent via trackAnonymous keyed by the session id.

Event
{
  "name": "product view",
  "data": {
    "id": "ers",
    "name": "Everyday Ruck Snack",
    "color": "black",
    "size": "l",
    "price": 420
  },
  "context": {
    "shopping": [
      "detail",
      0
    ]
  },
  "globals": {
    "pagegroup": "shop"
  },
  "custom": {
    "completely": "random"
  },
  "user": {
    "session": "s3ss10n"
  },
  "nested": [],
  "consent": {
    "functional": true
  },
  "id": "0157acc60b5eabe4",
  "trigger": "load",
  "entity": "product",
  "action": "view",
  "timestamp": 1700000103,
  "timing": 3.14,
  "source": {
    "count": 1,
    "trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
    "type": "collector",
    "schema": "4"
  }
}
Out
trackClient.trackAnonymous("s3ss10n", {
  "name": "product view",
  "data": {},
  "timestamp": 1700000
})

Default track

A walkerOS event is forwarded to Customer.io as a track call keyed by the user id.

Event
{
  "name": "product view",
  "data": {
    "id": "ers",
    "name": "Everyday Ruck Snack",
    "color": "black",
    "size": "l",
    "price": 420
  },
  "context": {
    "shopping": [
      "detail",
      0
    ]
  },
  "globals": {
    "pagegroup": "shop"
  },
  "custom": {
    "completely": "random"
  },
  "user": {
    "id": "us3r",
    "session": "s3ss10n"
  },
  "nested": [],
  "consent": {
    "functional": true
  },
  "id": "c2dea3c29dbf4aa4",
  "trigger": "load",
  "entity": "product",
  "action": "view",
  "timestamp": 1700000100,
  "timing": 3.14,
  "source": {
    "count": 1,
    "trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
    "type": "collector",
    "schema": "4"
  }
}
Out
trackClient.track("us3r", {
  "name": "product view",
  "data": {},
  "timestamp": 1700000
})

Destination identify

Destination-level identify fires a Customer.io identify call once on the first push, before the track, attaching user attributes.

Event
{
  "name": "page view",
  "data": {
    "domain": "www.example.com",
    "title": "walkerOS documentation",
    "referrer": "https://www.walkeros.io/",
    "search": "?foo=bar",
    "hash": "#hash",
    "id": "/docs/"
  },
  "context": {
    "dev": [
      "test",
      1
    ]
  },
  "globals": {
    "pagegroup": "docs"
  },
  "custom": {
    "completely": "random"
  },
  "user": {
    "id": "us3r",
    "session": "s3ss10n",
    "email": "user@example.com"
  },
  "nested": [
    {
      "entity": "child",
      "data": {
        "is": "subordinated"
      }
    }
  ],
  "consent": {
    "functional": true
  },
  "id": "bba41f7ee0c4091c",
  "trigger": "load",
  "entity": "page",
  "action": "view",
  "timestamp": 1700000104,
  "timing": 3.14,
  "source": {
    "count": 1,
    "trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
    "type": "collector",
    "schema": "4"
  }
}
Out
trackClient.identify("us3r", {
  "email": "user@example.com"
});

trackClient.track("us3r", {
  "name": "page view",
  "data": {},
  "timestamp": 1700000
})

Destroy person

A user delete event permanently removes the person from Customer.io via trackClient.destroy.

Event
{
  "name": "user delete",
  "data": {
    "string": "foo",
    "number": 1,
    "boolean": true,
    "array": [
      0,
      "text",
      false
    ]
  },
  "context": {
    "dev": [
      "test",
      1
    ]
  },
  "globals": {
    "lang": "elb"
  },
  "custom": {
    "completely": "random"
  },
  "user": {
    "id": "us3r",
    "session": "s3ss10n"
  },
  "nested": [
    {
      "entity": "child",
      "data": {
        "is": "subordinated"
      }
    }
  ],
  "consent": {
    "functional": true
  },
  "id": "1f081fa0d9c575d9",
  "trigger": "test",
  "entity": "user",
  "action": "delete",
  "timestamp": 1700000107,
  "timing": 3.14,
  "source": {
    "count": 1,
    "trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
    "type": "collector",
    "schema": "4"
  }
}
Mapping
{
  "silent": true,
  "settings": {
    "destroy": true
  }
}
Out
trackClient.destroy("us3r")

Mapped properties

A data mapping transforms the event payload into Customer.io track properties for an order.

Event
{
  "name": "order complete",
  "data": {
    "id": "0rd3r1d",
    "total": 555,
    "currency": "EUR"
  },
  "context": {
    "shopping": [
      "complete",
      0
    ]
  },
  "globals": {
    "pagegroup": "shop"
  },
  "custom": {
    "completely": "random"
  },
  "user": {
    "id": "us3r",
    "session": "s3ss10n"
  },
  "nested": [
    {
      "entity": "product",
      "data": {
        "id": "ers",
        "name": "Everyday Ruck Snack",
        "color": "black",
        "size": "l",
        "price": 420
      },
      "context": {
        "shopping": [
          "complete",
          0
        ]
      },
      "nested": []
    },
    {
      "entity": "product",
      "data": {
        "id": "cc",
        "name": "Cool Cap",
        "size": "one size",
        "price": 42
      },
      "context": {
        "shopping": [
          "complete",
          0
        ]
      },
      "nested": []
    },
    {
      "entity": "gift",
      "data": {
        "name": "Surprise"
      },
      "context": {
        "shopping": [
          "complete",
          0
        ]
      },
      "nested": []
    }
  ],
  "consent": {
    "functional": true
  },
  "id": "fc521b3f52941d4f",
  "trigger": "load",
  "entity": "order",
  "action": "complete",
  "timestamp": 1700000102,
  "timing": 3.14,
  "source": {
    "count": 1,
    "trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
    "type": "collector",
    "schema": "4"
  }
}
Mapping
{
  "name": "purchase",
  "data": {
    "map": {
      "order_id": "data.id",
      "value": "data.total",
      "currency": "data.currency"
    }
  }
}
Out
trackClient.track("us3r", {
  "name": "purchase",
  "data": {
    "order_id": "0rd3r1d",
    "value": 555,
    "currency": "EUR"
  },
  "timestamp": 1700000
})

Rename event

A mapping rule renames the walker event to a Customer.io-specific event name such as purchase.

Event
{
  "name": "order complete",
  "data": {
    "id": "0rd3r1d",
    "currency": "EUR",
    "shipping": 5.22,
    "taxes": 73.76,
    "total": 555
  },
  "context": {
    "shopping": [
      "complete",
      0
    ]
  },
  "globals": {
    "pagegroup": "shop"
  },
  "custom": {
    "completely": "random"
  },
  "user": {
    "id": "us3r",
    "session": "s3ss10n"
  },
  "nested": [
    {
      "entity": "product",
      "data": {
        "id": "ers",
        "name": "Everyday Ruck Snack",
        "color": "black",
        "size": "l",
        "price": 420
      },
      "context": {
        "shopping": [
          "complete",
          0
        ]
      },
      "nested": []
    },
    {
      "entity": "product",
      "data": {
        "id": "cc",
        "name": "Cool Cap",
        "size": "one size",
        "price": 42
      },
      "context": {
        "shopping": [
          "complete",
          0
        ]
      },
      "nested": []
    },
    {
      "entity": "gift",
      "data": {
        "name": "Surprise"
      },
      "context": {
        "shopping": [
          "complete",
          0
        ]
      },
      "nested": []
    }
  ],
  "consent": {
    "functional": true
  },
  "id": "19c1445a5e40aaba",
  "trigger": "load",
  "entity": "order",
  "action": "complete",
  "timestamp": 1700000101,
  "timing": 3.14,
  "source": {
    "count": 1,
    "trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
    "type": "collector",
    "schema": "4"
  }
}
Mapping
{
  "name": "purchase"
}
Out
trackClient.track("us3r", {
  "name": "purchase",
  "data": {},
  "timestamp": 1700000
})

Page view

A page view fires trackPageView with the URL and referrer instead of a generic track call.

Event
{
  "name": "page view",
  "data": {
    "url": "https://example.com/pricing",
    "referrer": "https://google.com"
  },
  "context": {
    "dev": [
      "test",
      1
    ]
  },
  "globals": {
    "pagegroup": "docs"
  },
  "custom": {
    "completely": "random"
  },
  "user": {
    "id": "us3r",
    "session": "s3ss10n"
  },
  "nested": [
    {
      "entity": "child",
      "data": {
        "is": "subordinated"
      }
    }
  ],
  "consent": {
    "functional": true
  },
  "id": "87f62a1e1c82321d",
  "trigger": "load",
  "entity": "page",
  "action": "view",
  "timestamp": 1700000106,
  "timing": 3.14,
  "source": {
    "count": 1,
    "trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
    "type": "collector",
    "schema": "4"
  }
}
Mapping
{
  "silent": true,
  "settings": {
    "page": {
      "map": {
        "url": "data.url",
        "referrer": "data.referrer"
      }
    }
  }
}
Out
trackClient.trackPageView("us3r", "https://example.com/pricing", {
  "referrer": "https://google.com"
})

Suppress person

A user suppress event stops messaging for the person without deleting their profile data.

Event
{
  "name": "user suppress",
  "data": {
    "string": "foo",
    "number": 1,
    "boolean": true,
    "array": [
      0,
      "text",
      false
    ]
  },
  "context": {
    "dev": [
      "test",
      1
    ]
  },
  "globals": {
    "lang": "elb"
  },
  "custom": {
    "completely": "random"
  },
  "user": {
    "id": "us3r",
    "session": "s3ss10n"
  },
  "nested": [
    {
      "entity": "child",
      "data": {
        "is": "subordinated"
      }
    }
  ],
  "consent": {
    "functional": true
  },
  "id": "c1592f429eb27931",
  "trigger": "test",
  "entity": "user",
  "action": "suppress",
  "timestamp": 1700000108,
  "timing": 3.14,
  "source": {
    "count": 1,
    "trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
    "type": "collector",
    "schema": "4"
  }
}
Mapping
{
  "silent": true,
  "settings": {
    "suppress": true
  }
}
Out
trackClient.suppress("us3r")

Unsuppress person

A user unsuppress event resumes messaging for a previously suppressed Customer.io profile.

Event
{
  "name": "user unsuppress",
  "data": {
    "string": "foo",
    "number": 1,
    "boolean": true,
    "array": [
      0,
      "text",
      false
    ]
  },
  "context": {
    "dev": [
      "test",
      1
    ]
  },
  "globals": {
    "lang": "elb"
  },
  "custom": {
    "completely": "random"
  },
  "user": {
    "id": "us3r",
    "session": "s3ss10n"
  },
  "nested": [
    {
      "entity": "child",
      "data": {
        "is": "subordinated"
      }
    }
  ],
  "consent": {
    "functional": true
  },
  "id": "52cdfff3146bdb70",
  "trigger": "test",
  "entity": "user",
  "action": "unsuppress",
  "timestamp": 1700000109,
  "timing": 3.14,
  "source": {
    "count": 1,
    "trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
    "type": "collector",
    "schema": "4"
  }
}
Mapping
{
  "silent": true,
  "settings": {
    "unsuppress": true
  }
}
Out
trackClient.unsuppress("us3r")

User login identify

A user login triggers only a Customer.io identify call with profile attributes, skipping the track.

Event
{
  "name": "user login",
  "data": {
    "email": "user@acme.com",
    "first_name": "Jane",
    "plan": "premium"
  },
  "context": {
    "dev": [
      "test",
      1
    ]
  },
  "globals": {
    "lang": "elb"
  },
  "custom": {
    "completely": "random"
  },
  "user": {
    "id": "us3r",
    "session": "s3ss10n"
  },
  "nested": [
    {
      "entity": "child",
      "data": {
        "is": "subordinated"
      }
    }
  ],
  "consent": {
    "functional": true
  },
  "id": "36fa74557eefbb08",
  "trigger": "test",
  "entity": "user",
  "action": "login",
  "timestamp": 1700000105,
  "timing": 3.14,
  "source": {
    "count": 1,
    "trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
    "type": "collector",
    "schema": "4"
  }
}
Mapping
{
  "silent": true,
  "settings": {
    "identify": {
      "map": {
        "email": "data.email",
        "first_name": "data.first_name",
        "plan": "data.plan"
      }
    }
  }
}
Out
trackClient.identify("us3r", {
  "email": "user@acme.com",
  "first_name": "Jane",
  "plan": "premium"
})

Identity is resolved automatically from each event: customerId defaults to user.id and anonymousId defaults to user.session. Customer.io requires a customerId or anonymousId per event. When customerId is missing, the destination automatically falls back to trackAnonymous() so anonymous visitor data is preserved.

Customer lifecycle

Per-rule mapping settings control which lifecycle methods are called in addition to the default track():

Mapping SettingSDK CallUse For
identifyidentify()Login, signup, profile updates
pagetrackPageView()Page view events
destroydestroy()GDPR delete / user removal
suppresssuppress()Stop messaging (keep data)
unsuppressunsuppress()Resume messaging
addDeviceaddDevice()Register push notification token
deleteDevicedeleteDevice()Remove push token
mergemergeCustomers()Consolidate duplicate profiles

Use skip: true on the rule to fire only the lifecycle call without tracking a second track() event.

Transactional messaging

Set settings.appApiKey to enable sendEmail / sendPush via Customer.io's App API. These mappings resolve to the full request body expected by the SDK.

💡 Need implementation support?
elbwalker offers hands-on support: setup review, measurement planning, destination mapping, and live troubleshooting. Book a 2-hour session (€399)