# Customer.io Destination Config Reference


> Complete Rudder CLI reference for the Customer.io destination config keys, API versions, web and mobile device mode settings, and secrets.



[Customer.io](https://www.rudderstack.com/docs/destinations/streaming-destinations/customer-io/setup-guide/) is a messaging and marketing automation destination. RudderStack sends events to Customer.io's APIs from its servers, or through Customer.io's own SDKs in device mode on web, Android, and iOS.

In a Customer.io destination spec:

- `type: customerio`
- `definition_version: 1`

## Sample configuration

```yaml
version: rudder/v1
kind: destination
metadata:
  name: customerio-prod
spec:
  id: customerio-prod
  display_name: Customer.io Production
  type: customerio
  definition_version: 1
  enabled: true
  config:
    site_id: "{{ .CUSTOMERIO_SITE_ID }}"
    api_key: "{{ .CUSTOMERIO_API_KEY }}"
    datacenter: US
    api_version: v2
    user_id_identifier_type: id
    device_token_event_name: Device Token Registered

    sdk_version:
      web: v2
    write_key:
      web: "{{ .CUSTOMERIO_WRITE_KEY }}"
    anonymous_in_app:
      web: false
    send_page_name_in_sdk:
      web: true
    auto_track_device_attributes:
      android: true
      ios: true
    background_queue_min_number_of_tasks:
      android: "10"
    background_queue_seconds_delay:
      android: "30"

    connection_mode:
      web: device
      android: device
      cloud: cloud
    consent_management:
      web:
        - provider: oneTrust
          consents:
            - marketing
```

The above example sends server-side events through the v2 API, and connects web and Android sources in `device` mode, where the SDK settings apply. Web loads the v2 JavaScript client, so it needs `write_key` — see [Key dependencies](#key-dependencies).

## Config keys

`config` accepts only the keys listed below. The [shared config key rules](https://www.rudderstack.com/docs/rudder-cli/destination-types/#config-key-rules) cover unknown keys, defaults, and immutability.

### Key dependencies

Which credentials Customer.io needs depends on the modes your sources connect in, and on the web SDK version. Rudder CLI enforces each requirement below.

| Key | Required when |
| :-----| :-----|
| `api_key` | Always, unless `web` is the only source type and connects in `device` mode. Also checked per connection — see [Connect a source](#connect-a-source). |
| `site_id` | Always, unless `web` is the only source type and connects in `device` mode with `sdk_version.web` set to `v2`. Also checked per connection. |
| `write_key` | `connection_mode.web` is `device` and `sdk_version.web` is `v2` |
| `user_id_identifier_type` | `api_version` is `v2`, including when it's omitted |

**Warning:** Write `sdk_version` out to use the v2 web client. When the block is omitted, nothing fills it in — the destination is stored without a version, which Customer.io&#39;s web SDK and the API&#39;s checks both read as `v1`. The `v2` default applies only inside an `sdk_version` block you write.

### Connection

<div class="rs-ckeys">
<div class="rs-ckey" id="site_id">
  <div class="rs-ckey__title">
    <p class="rs-ckey__name">site_id</p>RequiredSecret</div>
  <p class="rs-ckey__field">
    Type:
    <code>string</code>
  </p>
<p class="rs-ckey__field">
  Description:
  Your Customer.io site ID.
</p>

<div class="rs-ckey__notes">
  <p class="rs-ckey__field">Notes:</p>
  <ul>
<li>Required unless <code>web</code> is the only source type and connects in <code>device</code> mode with <code>sdk_version.web</code> set to <code>v2</code>. See <a href="#key-dependencies" >Key dependencies</a>.</li>
<li>At most 100 characters, and must not contain line breaks.</li>
<li>A <code>{{ path || fallback }}</code> template is accepted in place of a literal.</li>
</ul>

</div>

<div class="rs-ckey__extra">
  See <a href="https://www.rudderstack.com/docs/destinations/streaming-destinations/customer-io/setup-guide/#where-can-i-find-the-customerio-api-key-and-site-id" >Where can I find the Customer.io API key and site ID?</a>.
</div>

</div>

<div class="rs-ckey" id="api_key">
  <div class="rs-ckey__title">
    <p class="rs-ckey__name">api_key</p>RequiredSecret</div>
  <p class="rs-ckey__field">
    Type:
    <code>string</code>
  </p>
<p class="rs-ckey__field">
  Description:
  Your Customer.io Tracking API key, paired with <code>site_id</code>.
</p>

<div class="rs-ckey__notes">
  <p class="rs-ckey__field">Notes:</p>
  <ul>
<li>Required unless <code>web</code> is the only source type and connects in <code>device</code> mode.</li>
<li>At most 100 characters, and must not contain line breaks.</li>
<li>A <code>{{ path || fallback }}</code> template is accepted in place of a literal.</li>
</ul>

</div>

<div class="rs-ckey__extra">
  Supply it as a <code>{{ .VAR }}</code> reference rather than a literal — see <a href="#secrets" >Secrets</a>.
</div>

</div>

<div class="rs-ckey" id="datacenter">
  <div class="rs-ckey__title">
    <p class="rs-ckey__name">datacenter</p>Required</div>
  <p class="rs-ckey__field">
    Type:
    <code>string</code>
  </p>
<p class="rs-ckey__field">
  Description:
  Region of your Customer.io account.
</p>

<div class="rs-ckey__notes">
  <p class="rs-ckey__field">Notes:</p>
  <ul>
<li><code>US</code> or <code>EU</code>.</li>
<li>The dashboard defaults this field to <code>US</code>. Rudder CLI requires it explicitly.</li>
<li>A spec that omits this key fails validation.</li>
</ul>

</div>

</div>

<div class="rs-ckey" id="device_token_event_name">
  <div class="rs-ckey__title">
    <p class="rs-ckey__name">device_token_event_name</p></div>
  <p class="rs-ckey__field">
    Type:
    <code>string</code>
  </p>
<p class="rs-ckey__field">
  Description:
  Name of the event your app fires right after it sets the device token, so RudderStack can send the token to Customer.io immediately. Applies in both modes.
</p>

<div class="rs-ckey__notes">
  <p class="rs-ckey__field">Notes:</p>
  <ul>
<li>At most 100 characters, and must not contain line breaks.</li>
<li>A <code>{{ path || fallback }}</code> template is accepted in place of a literal.</li>
</ul>

</div>

</div>

</div>

### Cloud mode

These keys apply to events sent in `cloud` mode. They have no effect on device-mode delivery.

<div class="rs-ckeys">
<div class="rs-ckey" id="api_version">
  <div class="rs-ckey__title">
    <p class="rs-ckey__name">api_version</p></div>
  <p class="rs-ckey__field">
    Type:
    <code>string</code>
  </p><p class="rs-ckey__field">
    Default value:
    <code>v2</code>
  </p>
<p class="rs-ckey__field">
  Description:
  Customer.io API that delivers cloud-mode events.
</p>

<div class="rs-ckey__notes">
  <p class="rs-ckey__field">Notes:</p>
  <ul>
<li><code>v2</code> — Customer.io&rsquo;s unified <a href="https://docs.customer.io/integrations/api/track/tag/track_v2/batch/" >batch API</a>. Needs <code>user_id_identifier_type</code>.</li>
<li><code>v1</code> — Customer.io&rsquo;s per-endpoint APIs.</li>
<li>Omitting this key is the same as setting <code>v2</code>.</li>
</ul>

</div>

</div>

<div class="rs-ckey" id="user_id_identifier_type">
  <div class="rs-ckey__title">
    <p class="rs-ckey__name">user_id_identifier_type</p>Required</div>
  <p class="rs-ckey__field">
    Type:
    <code>string</code>
  </p>
<p class="rs-ckey__field">
  Description:
  Customer.io identifier that receives the RudderStack <code>userId</code>.
</p>

<div class="rs-ckey__notes">
  <p class="rs-ckey__field">Notes:</p>
  <ul>
<li>Required when <code>api_version</code> is <code>v2</code> — including when you omit <code>api_version</code>, since <code>v2</code> is the default. Leave it unset only with <code>v1</code>.</li>
<li>One of <code>id</code>, <code>email</code>, <code>phone</code>, or <code>cio_id</code>. With <code>phone</code>, the value must read as E.164 — for example <code>+15551234567</code>.</li>
</ul>

</div>

<div class="rs-ckey__extra">
  <strong>Warning:</strong> <code>userId</code> is sent as this identifier for every event, with no fallback — choose <code>id</code> and send an email address, and it's sent as an ID. A wrong choice attaches events to the wrong profiles or creates new ones, and that can't be reversed. With <code>v2</code>, events without a <code>userId</code> fail.
</div>

</div>

</div>

### Device mode

These keys configure Customer.io's SDKs, so they apply only to sources connected in `device` mode. Each is keyed by the source type it applies to.

<div class="rs-ckeys">
<div class="rs-ckey" id="sdk_version">
  <div class="rs-ckey__title">
    <p class="rs-ckey__name">sdk_version</p></div>
  <p class="rs-ckey__field">
    Type:
    <code>object</code>
  </p>
<p class="rs-ckey__field">
  Description:
  Customer.io JavaScript client that web device mode loads. <code>v1</code> loads the legacy snippet and uses <code>site_id</code>. <code>v2</code> loads the Data Pipelines JavaScript client and needs <code>write_key</code>. Switching an existing destination to <code>v2</code> changes where your web events land.
</p>

<div class="rs-ckey__notes">
  <p class="rs-ckey__field">Notes:</p>
  <ul>
<li><code>web</code> — <code>v1</code> or <code>v2</code>. Defaults to <code>v2</code>, but only inside an <code>sdk_version</code> block you write — an omitted block is read as <code>v1</code>.</li>
<li>Applies when <code>connection_mode.web</code> is <code>device</code>.</li>
</ul>

</div>

<div class="rs-ckey__extra">
  <div class="rs-code">
  <div class="rs-code__head">yaml<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-yaml" data-lang="yaml">sdk_version:
  web: v2</code></pre></div>
</div>
</div>

</div>

<div class="rs-ckey" id="write_key">
  <div class="rs-ckey__title">
    <p class="rs-ckey__name">write_key</p>Required</div>
  <p class="rs-ckey__field">
    Type:
    <code>object</code>
  </p>
<p class="rs-ckey__field">
  Description:
  Write key of your Customer.io Data Pipelines JavaScript source. On <code>v2</code>, web events go to this source instead of the Track API.
</p>

<div class="rs-ckey__notes">
  <p class="rs-ckey__field">Notes:</p>
  <ul>
<li><code>web</code> — string. Required when <code>connection_mode.web</code> is <code>device</code> and <code>sdk_version.web</code> is <code>v2</code>.</li>
<li>At most 100 characters, and must not contain line breaks. A <code>{{ path || fallback }}</code> template is measured as literal text against the same limit.</li>
</ul>

</div>

</div>

<div class="rs-ckey" id="anonymous_in_app">
  <div class="rs-ckey__title">
    <p class="rs-ckey__name">anonymous_in_app</p></div>
  <p class="rs-ckey__field">
    Type:
    <code>object</code>
  </p>
<p class="rs-ckey__field">
  Description:
  Show in-app messages to website visitors you haven&rsquo;t identified yet. Needs in-app messaging turned on in your Customer.io workspace, and a Customer.io plan that supports anonymous in-app messages. In-app messages for identified users need no setting on <code>v2</code>.
</p>

<div class="rs-ckey__notes">
  <p class="rs-ckey__field">Notes:</p>
  <ul>
<li><code>web</code> — boolean. The dashboard defaults it to <code>false</code>.</li>
<li>Applies when <code>sdk_version.web</code> is <code>v2</code>. Rudder CLI also accepts it with <code>v1</code>, where it has no effect.</li>
</ul>

</div>

</div>

<div class="rs-ckey" id="send_page_name_in_sdk">
  <div class="rs-ckey__title">
    <p class="rs-ckey__name">send_page_name_in_sdk</p></div>
  <p class="rs-ckey__field">
    Type:
    <code>object</code>
  </p>
<p class="rs-ckey__field">
  Description:
  Send the <code>page</code> name to Customer.io. When <code>false</code>, Customer.io records the page URL instead.
</p>

<div class="rs-ckey__notes">
  <p class="rs-ckey__field">Notes:</p>
  <ul>
<li><code>web</code> — boolean. The dashboard defaults it to <code>true</code>; Rudder CLI doesn&rsquo;t fill it in.</li>
</ul>

</div>

</div>

<div class="rs-ckey" id="data_use_in_app">
  <div class="rs-ckey__title">
    <p class="rs-ckey__name">data_use_in_app</p></div>
  <p class="rs-ckey__field">
    Type:
    <code>object</code>
  </p>
<p class="rs-ckey__field">
  Description:
  Enable Customer.io <a href="https://customer.io/docs/journeys/in-app-getting-started/#javascript-snippet" >in-app messages</a> on your website.
</p>

<div class="rs-ckey__notes">
  <p class="rs-ckey__field">Notes:</p>
  <ul>
<li><code>web</code> — boolean. The dashboard defaults it to <code>false</code>.</li>
<li>Applies to the <code>v1</code> web client. Rudder CLI also accepts it with <code>v2</code>, where it has no effect.</li>
</ul>

</div>

</div>

<div class="rs-ckey" id="auto_track_device_attributes">
  <div class="rs-ckey__title">
    <p class="rs-ckey__name">auto_track_device_attributes</p></div>
  <p class="rs-ckey__field">
    Type:
    <code>object</code>
  </p>
<p class="rs-ckey__field">
  Description:
  Let Customer.io&rsquo;s mobile SDK track device attributes automatically. Set <code>false</code> to track them yourself.
</p>

<div class="rs-ckey__notes">
  <p class="rs-ckey__field">Notes:</p>
  <ul>
<li><code>android</code>, <code>ios</code> — booleans. The dashboard defaults both to <code>true</code>; Rudder CLI doesn&rsquo;t fill them in.</li>
</ul>

</div>

<div class="rs-ckey__extra">
  <div class="rs-code">
  <div class="rs-code__head">yaml<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-yaml" data-lang="yaml">auto_track_device_attributes:
  android: true
  ios: false</code></pre></div>
</div>
</div>

</div>

<div class="rs-ckey" id="background_queue_min_number_of_tasks">
  <div class="rs-ckey__title">
    <p class="rs-ckey__name">background_queue_min_number_of_tasks</p></div>
  <p class="rs-ckey__field">
    Type:
    <code>object</code>
  </p>
<p class="rs-ckey__field">
  Description:
  Minimum number of tasks the Android SDK keeps in its background queue.
</p>

<div class="rs-ckey__notes">
  <p class="rs-ckey__field">Notes:</p>
  <ul>
<li><code>android</code> — a whole number written as a string, <code>&quot;10&quot;</code> rather than <code>10</code>. At most 100 characters, or a template.</li>
<li>The dashboard defaults it to <code>&quot;10&quot;</code>; Rudder CLI doesn&rsquo;t fill it in.</li>
</ul>

</div>

</div>

<div class="rs-ckey" id="background_queue_seconds_delay">
  <div class="rs-ckey__title">
    <p class="rs-ckey__name">background_queue_seconds_delay</p></div>
  <p class="rs-ckey__field">
    Type:
    <code>object</code>
  </p>
<p class="rs-ckey__field">
  Description:
  Delay, in seconds, that the Android SDK holds events in its background queue.
</p>

<div class="rs-ckey__notes">
  <p class="rs-ckey__field">Notes:</p>
  <ul>
<li><code>android</code> — a whole number written as a string, <code>&quot;30&quot;</code> rather than <code>30</code>. At most 100 characters, or a template.</li>
<li>The dashboard defaults it to <code>&quot;30&quot;</code>; Rudder CLI doesn&rsquo;t fill it in.</li>
</ul>

</div>

</div>

</div>

### Event filtering

**Warning:** [Client-side event filtering](https://www.rudderstack.com/docs/sources/event-streams/sdks/event-filtering/) applies only when `connection_mode.web` is `device` — the dashboard shows these controls only then, and the SDK is what applies the filter. Rudder CLI accepts the block in any mode, but it has no effect on other connections.

<div class="rs-ckeys">
<div class="rs-ckey" id="event_filtering">
  <div class="rs-ckey__title">
    <p class="rs-ckey__name">event_filtering</p></div>
  <p class="rs-ckey__field">
    Type:
    <code>object</code>
  </p>
<p class="rs-ckey__field">
  Description:
  Restricts which <code>track</code> events the web SDK passes to Customer.io, by event name.
</p>

<div class="rs-ckey__notes">
  <p class="rs-ckey__field">Notes:</p>
  <ul>
<li>Applies when <code>connection_mode.web</code> is <code>device</code>. Leave it unset otherwise.</li>
<li><code>whitelist</code> — array of event names to allow; every other <code>track</code> event is dropped.</li>
<li><code>blacklist</code> — array of event names to drop; every other <code>track</code> event is allowed.</li>
<li>The two are mutually exclusive, and Rudder CLI enforces it — setting both fails validation.</li>
<li>Each name is at most 100 characters, or a <code>{{ path || fallback }}</code> template.</li>
</ul>

</div>

</div>

</div>

### Per-source keys

Both keys are objects keyed by the local source type — the tokens listed under [Source types](#source-types). A key naming a source type this destination doesn't support fails validation.

<div class="rs-ckeys">
<div class="rs-ckey" id="connection_mode">
  <div class="rs-ckey__title">
    <p class="rs-ckey__name">connection_mode</p></div>
  <p class="rs-ckey__field">
    Type:
    <code>object</code>
  </p>
<p class="rs-ckey__field">
  Description:
  Maps each source type you connect to the mode its events reach Customer.io in, using the modes in <a href="#source-types" >Source types</a>.
</p>

<div class="rs-ckey__notes">
  <p class="rs-ckey__field">Notes:</p>
  <ul>
<li>An entry is required for each source type you connect — see <a href="#connect-a-source" >Connect a source</a>.</li>
<li>A mode the source type doesn&rsquo;t support on this destination fails validation — for example <code>device</code> for <code>android_kotlin</code>.</li>
</ul>

</div>

<div class="rs-ckey__extra">
  <div class="rs-code">
  <div class="rs-code__head">yaml<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-yaml" data-lang="yaml">connection_mode:
  web: device
  android: device
  ios: cloud</code></pre></div>
</div>
</div>

</div>

<div class="rs-ckey" id="consent_management">
  <div class="rs-ckey__title">
    <p class="rs-ckey__name">consent_management</p></div>
  <p class="rs-ckey__field">
    Type:
    <code>object</code>
  </p>
<p class="rs-ckey__field">
  Description:
  Consent provider configuration per source type. The entry shape, accepted providers, and the rules on <code>resolution_strategy</code> and <code>consents</code> are shared across all destination types — see <a href="https://www.rudderstack.com/docs/rudder-cli/destination-types/#consent-management" >Consent management</a>.
</p>

</div>

</div>

## Source types

Customer.io accepts events from these source types in the mentioned connection modes:

| Source type | Connection mode |
| :-----| :-----|
| `android` | `cloud`, `device` |
| `android_kotlin` | `cloud` |
| `ios` | `cloud`, `device` |
| `ios_swift` | `cloud` |
| `web` | `cloud`, `device` |
| `unity` | `cloud` |
| `cloud` | `cloud` |
| `react_native` | `cloud` |
| `flutter` | `cloud` |
| `cordova` | `cloud` |
| `warehouse` | `cloud` |

`web`, `android`, and `ios` offer `device` mode. The Kotlin and Swift SDKs — `android_kotlin` and `ios_swift` — are `cloud` only. See [Customer.io device mode](https://www.rudderstack.com/docs/destinations/streaming-destinations/customer-io/device-mode/).

`warehouse` is the token a [Reverse ETL source](https://www.rudderstack.com/docs/sources/reverse-etl/) resolves to. Rudder CLI manages the destination’s `warehouse` entry — if the destination is connected to a Reverse ETL source, set `connection_mode.warehouse: cloud` in its spec, or `apply` removes it.

Rudder CLI can also declare the Reverse ETL connection itself, for the types listed under [Supported destinations](https://www.rudderstack.com/docs/rudder-cli/yaml-retl-connections/#supported-destinations) — see [Manage Reverse ETL Connections using Rudder CLI](https://www.rudderstack.com/docs/rudder-cli/retl-connections/).

**Info**

**Important considerations**

- Rudder CLI cannot declare a Reverse ETL connection to Customer.io — it syncs Reverse ETL data through its own flow, which connection specs can’t express. The `warehouse` entry still belongs in the spec so that `apply` keeps a connection you set up in the dashboard.
- The dashboard additionally offers Customer.io to AMP and Shopify sources. Rudder CLI doesn&#39;t manage those connections, so `amp` and `shopify` are invalid here.

## Connect a source

An event stream connection to this destination is checked against three rules at `validate` time.

**The source's type must be supported.** A source's type resolves to one of the tokens above before the check — a JavaScript source resolves to `web`, and webhook and server-side SDK sources resolve to `cloud`. Every type an event stream source can resolve to is supported here, so this check always passes.

**The destination config must carry a `connection_mode` entry for that source type.** This lives on the destination spec, not on the [connection spec](https://www.rudderstack.com/docs/rudder-cli/yaml-connections/). Without it:

```text
destination 'customerio-prod' config has no 'connection_mode' entry for source type 'web'
```

**Every source needs `api_key` and `site_id`, except `web` connecting in `device` mode.** Without them:

```text
destination 'customerio-prod' config is missing fields required to connect a 'cloud' source: api_key, site_id
```

What `web` needs in `device` mode depends on the other source types and on `sdk_version` — see [Key dependencies](#key-dependencies).

## Secrets

`api_key` and `site_id` are the secret keys. Write each as a `{{ .VAR }}` reference and supply the value at apply time:

```yaml
config:
  site_id: "{{ .CUSTOMERIO_SITE_ID }}"
  api_key: "{{ .CUSTOMERIO_API_KEY }}"
```

```bash
export RUDDER_CUSTOMERIO_SITE_ID="..."
export RUDDER_CUSTOMERIO_API_KEY="..."
rudder-cli apply

# or
rudder-cli apply --var-file secrets.vars.yaml
```

Note that:

- `rudder-cli import` writes only the secrets the API returns, as `{{ .VAR }}` references, and leaves the others out. Add any secret key it leaves out, then supply the variables before you apply — see [Secrets](https://www.rudderstack.com/docs/rudder-cli/destination-types/#secrets).
- In web device mode the site ID (`v1`) or `write_key` (`v2`) is embedded in the page's JavaScript, so neither value is hidden from a visitor. Masking `site_id` protects your YAML, not the value itself. `write_key` isn't a secret key, so `import` writes it as a literal.

See [How to Use Variable Substitution in Rudder CLI](https://www.rudderstack.com/docs/rudder-cli/variable-substitution/).

## Changes by release

<table>
  <thead>
    <tr>
      <th>Version</th>
      <th>Change</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>v0.29.0</td>
      <td><strong>Breaking:</strong> A <code>null</code> config value, written as <code>key: null</code>, <code>key: ~</code>, or <code>key:</code> with no value, fails <code>validate</code> with <code>'&lt;key&gt;' must not be null; set a value or remove the key</code>. Earlier releases accepted it: a top-level null planned an update on every <code>apply</code>, and a null inside a list item failed at the API.<br><strong>What to do:</strong> Remove the key to leave it unset, or give it a value.</td>
    </tr>
    <tr>
      <td>v0.29.0</td>
      <td><strong>Breaking:</strong> An empty string, <code>&quot;&quot;</code>, on a key that takes one of a fixed set of values fails <code>validate</code> with <code>'&lt;key&gt;' must be one of [...]</code>, unless <code>&quot;&quot;</code> is one of the key&rsquo;s values. Earlier releases accepted it, and the API rejected it at <code>apply</code>.<br><strong>What to do:</strong> Remove the key to leave it unset, or set one of the listed values.</td>
    </tr>
    <tr>
      <td>v0.29.0</td>
      <td><strong>Changed:</strong> <code>validate</code> warns when a secret key holds anything but one <code>{{ .VAR }}</code> reference with no default, and when one variable fills two secret keys. It also masks secret values in the lines it prints.</td>
    </tr>
    <tr>
      <td>v0.29.0</td>
      <td><strong>Changed:</strong> <code>rudder-cli import</code> writes each secret reference single-quoted, as <code>'{{ .VAR }}'</code>, so a value containing double quotes, like a JSON key file, substitutes as a string. A single quote in a value that fills such a reference is written as <code>''</code>.<br><strong>What to do:</strong> In a spec imported by an earlier release, quote each reference to a JSON value by hand: <code>'{{ .VAR }}'</code>.</td>
    </tr>
    <tr>
      <td>v0.29.0</td>
      <td><strong>Changed:</strong> <code>validate</code> reports a value of the wrong type once per key, such as <code>'email' must be a string</code>, instead of one raw decoding error.</td>
    </tr>
    <tr>
      <td>v0.28.0</td>
      <td><strong>Changed:</strong> <code>api_key</code> and <code>site_id</code> are required only where a connection needs them. With <code>web</code> as the only source type in <code>device</code> mode, <code>api_key</code> is optional, and so is <code>site_id</code> when <code>sdk_version.web</code> is <code>v2</code>.</td>
    </tr>
    <tr>
      <td>v0.28.0</td>
      <td><strong>Added:</strong> <code>sdk_version</code>, <code>write_key</code>, and <code>anonymous_in_app</code>, for the v2 web client in <code>device</code> mode. A spec without <code>sdk_version</code> runs the <code>v1</code> client, so add these keys to a spec imported by an earlier release if the destination uses <code>v2</code>.</td>
    </tr>
    <tr>
      <td>v0.28.0</td>
      <td><strong>Added:</strong> <code>warehouse</code> source type, for destinations connected to a Reverse ETL source. Set <code>connection_mode.warehouse: cloud</code> for such a destination, or <code>apply</code> removes the entry.</td>
    </tr>
  </tbody>
</table>

For changes across every destination type, see [Destination Type Changes by Release](https://www.rudderstack.com/docs/rudder-cli/destination-types/changes/).

## See more

- [Customer.io Destination](https://www.rudderstack.com/docs/destinations/streaming-destinations/customer-io/setup-guide/) for the API key and site ID, and the per-mode settings
- [Destination Type Reference for Rudder CLI](https://www.rudderstack.com/docs/rudder-cli/destination-types/) for the rules shared across destination types
- [Destination YAML Reference](https://www.rudderstack.com/docs/rudder-cli/yaml-destinations/) for the spec envelope

