diff --git a/components/ui/MultiLangCodeBlock.tsx b/components/ui/MultiLangCodeBlock.tsx
index 730a13dca..d1de508fb 100644
--- a/components/ui/MultiLangCodeBlock.tsx
+++ b/components/ui/MultiLangCodeBlock.tsx
@@ -43,6 +43,8 @@ const snippets = {
.default,
"users.setChannelData": require("../../data/code/users/set-channel-data")
.default,
+ "users.setChannelDataDevices":
+ require("../../data/code/users/set-channel-data-devices").default,
"users.setChannelData-push":
require("../../data/code/users/set-channel-data-push").default,
"users.setChannelData-one-signal":
diff --git a/content/integrations/push/apns.mdx b/content/integrations/push/apns.mdx
index 4305c2210..1112cb712 100644
--- a/content/integrations/push/apns.mdx
+++ b/content/integrations/push/apns.mdx
@@ -81,6 +81,9 @@ Overrides are merged into the notification payload sent to APNs. See the Apple developer documentation.
-| Property | Type | Description |
-| -------- | -------- | ------------------------- |
-| tokens\* | string[] | One or more device tokens |
+Alternatively, you can store a list of `devices` objects when using [device metadata](/integrations/push/device-metadata).
+
+| Property | Type | Description |
+| --------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------- |
+| tokens\* | `string[]` | One or more device tokens. Required when not using `devices`. |
+| devices\* | [`PushDevice[]`](/managing-recipients/setting-channel-data#the-pushdevice-object) | One or more device objects. Required when not using `tokens`. |
diff --git a/content/integrations/push/aws-sns.mdx b/content/integrations/push/aws-sns.mdx
index 49012476f..8fe1e97da 100644
--- a/content/integrations/push/aws-sns.mdx
+++ b/content/integrations/push/aws-sns.mdx
@@ -305,6 +305,9 @@ See Setting up an Amazon SNS platform endpoint for mobile notifications for more details on creating platform endpoints.
-| Property | Type | Description |
-| ------------- | -------- | -------------------------------------------------------------------------------------------- |
-| target_arns\* | string[] | One or more platform endpoint ARNs associated with a platform application and a device token |
+Alternatively, you can store a list of `devices` objects when using [device metadata](/integrations/push/device-metadata).
+
+| Property | Type | Description |
+| ------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
+| target_arns\* | `string[]` | One or more platform endpoint ARNs associated with a platform application and a device token. Required when not using `devices`. |
+| devices\* | [`PushDevice[]`](/managing-recipients/setting-channel-data#the-pushdevice-object) | One or more device objects. Required when not using `target_arns`. |
diff --git a/content/integrations/push/device-metadata.mdx b/content/integrations/push/device-metadata.mdx
new file mode 100644
index 000000000..2f514a990
--- /dev/null
+++ b/content/integrations/push/device-metadata.mdx
@@ -0,0 +1,97 @@
+---
+title: Push notification device metadata
+description: How to use channel data to store device-level metadata for push notifications.
+tags:
+ [
+ "push token",
+ "channel data",
+ "device token",
+ "device metadata",
+ "locale",
+ "timezone",
+ ]
+section: Integrations > Push
+layout: integrations
+---
+
+When [setting channel data for push channels](/managing-recipients/setting-channel-data#push-channels), Knock provides the option to set additional metadata alongside a device token. When set, a device-level `locale` will be used when [translating](/concepts/translations) message content for the device, and the `timezone` will be used to evalute [send windows](/designing-workflows/send-windows).
+
+This feature is available for all push providers except OneSignal.
+
+## Availability
+
+| Provider | Device metadata supported? |
+| ---------- | -------------------------- |
+| APNs | ✅ |
+| FCM | ✅ |
+| Expo | ✅ |
+| Amazon SNS | ✅ |
+| OneSignal | ❌ |
+
+## How it works
+
+When setting a recipient's channel data for a supported push channel, you can pass a list of `devices` objects (containing `token`, `locale`, and `timezone`) rather than a list of `tokens` strings.
+
+When a workflow includes a push channel step and the `recipient` channel data includes device metadata, any device-level values will take precedence over the recipient-level `locale` and `timezone` [properties](/managing-recipients/identifying-recipients#reserved-properties). This means:
+
+- The translation language used to render message content for the device will be according to the device-level `locale` property.
+- The timezone used to evaluate the send window for the device will be according to the device-level `timezone` property.
+
+All other features of push channels (including [token deregistration](/integrations/push/token-deregistration)) will function the same way, regardless of whether device metadata is set.
+
+
+ If you're using Knock's Amazon SNS push notification integration, a{" "}
+ target_arn or target_arns will take the place of{" "}
+ token or tokens when setting channel data. Their
+ functionality is interchangeable. Reference the{" "}
+ provider-specific documentation{" "}
+ for more details.
+ >
+ }
+/>
+
+### Setting device metadata
+
+In the example below, we're setting a user's device token by passing `devices` rather than `tokens`.
+
+
+
+If you do not require device-level locale or timezone properties, you can simply set channel data by passing a list of `tokens` strings.
+
+
+
+### Getting push channel data
+
+Regardless of whether you set `tokens` or `devices`, you'll see them returned in both formats when retrieving channel data. Devices will include `null` values for the `locale` and `timezone` properties if they were not provided when channel data was set.
+
+```json title="Example push channel data response"
+{
+ "__typename": "ChannelData",
+ "channel_id": "123e4567-e89b-12d3-a456-426614174000",
+ "data": {
+ "devices": [
+ {
+ "locale": "en-US",
+ "timezone": "America/New_York",
+ "token": "user_device_token_1"
+ },
+ {
+ "locale": null,
+ "timezone": null,
+ "token": "user_device_token_2"
+ }
+ ],
+ "tokens": ["user_device_token_1", "user_device_token_2"]
+ }
+}
+```
diff --git a/content/integrations/push/expo.mdx b/content/integrations/push/expo.mdx
index 28c414822..9b5207884 100644
--- a/content/integrations/push/expo.mdx
+++ b/content/integrations/push/expo.mdx
@@ -60,6 +60,9 @@ Overrides are merged into the notification payload sent to Expo. See the Expo developer documentation.
-| Property | Type | Description |
-| -------- | -------- | ------------------------- |
-| tokens\* | string[] | One or more device tokens |
+Alternatively, you can store a list of `devices` objects when using [device metadata](/integrations/push/device-metadata).
+
+| Property | Type | Description |
+| --------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------- |
+| tokens\* | `string[]` | One or more device tokens. Required when not using `devices`. |
+| devices\* | [`PushDevice[]`](/managing-recipients/setting-channel-data#the-pushdevice-object) | One or more device objects. Required when not using `tokens`. |
diff --git a/content/integrations/push/firebase.mdx b/content/integrations/push/firebase.mdx
index effeb0093..2e5940311 100644
--- a/content/integrations/push/firebase.mdx
+++ b/content/integrations/push/firebase.mdx
@@ -142,8 +142,11 @@ The following are common FCM errors you may see in your message delivery logs:
## Channel data requirements
-In order to use a configured FCM channel you must store a list of one or more device tokens for the user or the object that you wish to deliver a notification to. If you use multiple device tokens for a single user or object, Knock will generate and try and deliver a notification for each unique token.
+In order to use a configured FCM channel you must store a list of one or more device tokens for the user or the object that you wish to deliver a notification to. If you use multiple device tokens for a single user or object, Knock will generate and try to deliver a notification for each unique token.
-| Property | Type | Description |
-| -------- | -------- | ------------------------- |
-| tokens\* | string[] | One or more device tokens |
+Alternatively, you can store a list of `devices` objects when using [device metadata](/integrations/push/device-metadata).
+
+| Property | Type | Description |
+| --------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------- |
+| tokens\* | `string[]` | One or more device tokens. Required when not using `devices`. |
+| devices\* | [`PushDevice[]`](/managing-recipients/setting-channel-data#the-pushdevice-object) | One or more device objects. Required when not using `tokens`. |
diff --git a/content/integrations/push/overview.mdx b/content/integrations/push/overview.mdx
index e0d7ef434..829fc7ee1 100644
--- a/content/integrations/push/overview.mdx
+++ b/content/integrations/push/overview.mdx
@@ -12,7 +12,8 @@ Knock supports sending push notifications directly to native services such as Ap
- **No stateful connections to manage**: we take care of all of the complexity of managing and maintaining stateful connections to your push providers, just simply send us notifications and we'll get them delivered!
- **Cross-provider, single template**: you can send the same templated message across multiple providers to reduce the amount of templates to maintain.
-- **Token deregistration**: if a recipient's token(s) is invalid resulting in a `bounced` message when attempting to send, remove the token.
+- [**Token deregistration**](/integrations/push/token-deregistration): if a recipient's invalid device token results in a `bounced` message when attempting to send, Knock can optionally remove the token.
+- [**Device metadata**](/integrations/push/device-metadata): set device-level `locale` and `timezone` properties for translations and send window evaluation with supported providers.
## Channel groups
diff --git a/content/integrations/push/token-deregistration.mdx b/content/integrations/push/token-deregistration.mdx
index 5adffc1c1..cb2cb9bc6 100644
--- a/content/integrations/push/token-deregistration.mdx
+++ b/content/integrations/push/token-deregistration.mdx
@@ -6,7 +6,7 @@ section: Integrations > Push
layout: integrations
---
-For push providers only, Knock provides an opt-in, provider-agnostic token management capability known as token deregistration. Knock removes invalid tokens from a recipient's corresponding channel data if that token results in a `bounced` message on send.
+For push providers only, Knock provides an opt-in, provider-agnostic token management capability known as token deregistration. Knock removes invalid tokens (and devices, when using [device metadata](/integrations/push/device-metadata)) from a recipient's corresponding channel data if that token results in a `bounced` message on send.
This feature is available for all push providers, except OneSignal when the recipient mode is set to `external_id`. In this case, Knock only has access to the user's ID and therefore cannot deregister the associated external token.
diff --git a/content/managing-recipients/setting-channel-data.mdx b/content/managing-recipients/setting-channel-data.mdx
index 37388c2ca..4e5b1b3cf 100644
--- a/content/managing-recipients/setting-channel-data.mdx
+++ b/content/managing-recipients/setting-channel-data.mdx
@@ -13,7 +13,7 @@ At Knock we call this concept `
- For channel types that require channel data (such as [push](/integrations/push/overview) channels and [chat](/integrations/chat/overview) channels like Slack), the channel step will be skipped during a workflow run if the required `channel_data` is not stored on the recipient.
- Knock stores channel data for you but makes no assumptions about whether the stored channel data is valid. That means that if a push token expires, it's your responsibility to omit/update that token for future notifications.
- - For push providers, Knock offers an opt-in [token deregistration](/integrations/push/token-deregistration) feature that automatically removes invalid tokens from a recipient's channel data when messages bounce.
+- For push providers, Knock offers an opt-in [token deregistration](/integrations/push/token-deregistration) feature that automatically removes invalid tokens from a recipient's channel data when messages bounce.
- Setting channel data always requires a `channel_id`, which can be obtained in the Dashboard under the **Channels and sources** page in your account settings. A channel ID is always a UUID v4.
## Setting channel data
@@ -134,27 +134,63 @@ Any previously set channel data can be cleared by issuing an `unsetChannelData`
## Provider data requirements
-Channel data requirements for each provider are listed below. Typically `channel_data` comprises a `token` or other value that is used to uniquely identify a user's device.
+Channel data requirements for each channel type and provider are listed below. Typically `channel_data` comprises a `token` or other value that is used to uniquely identify a user's device.
### Push channels
-
-
- | Property | Type | Description |
- | -------- | ---------- | ------------------------- |
- | tokens\* | `string[]` | One or more device tokens |
+You can set push channel data by passing either:
+
+- A list of `tokens` (or `target_arns` for [Amazon SNS](/integrations/push/aws-sns)) strings
+- A list of `devices` objects for supported push providers. If set, this [device-level metadata](/integrations/push/device-metadata) will be used when evaluating [translations](/concepts/translations) and [send windows](/designing-workflows/send-windows).
+
+#### The `PushDevice` object
+
+The `PushDevice` object is used to optionally set device-level metadata for a push channel. It contains the following properties:
+| Property | Type | Description |
+| ------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
+| token\* | `string` | The device token to send the push notification to. Required for providers other than Amazon SNS. |
+| target_arn\* | `string` | The ARN of a platform endpoint associated with a platform application and a device token. Required for Amazon SNS. |
+| locale | `string` | The [locale](/concepts/translations#supported-locales) of the device. |
+| timezone | `string` | The timezone of the device. Must be a valid [tz database time zone string](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). |
+
+#### Provider-specific requirements
+
+
+
+ You must provide one of `tokens` or `devices`.
+
+ | Property | Type | Description |
+ | --------- | ---------------------------------------- | --------------------------- |
+ | tokens\* | `string[]` | One or more device tokens. |
+ | devices\* | [`PushDevice[]`](#the-pushdevice-object) | One or more device objects. |
+
- | Property | Type | Description |
- | -------- | ---------- | ------------------------- |
- | tokens\* | `string[]` | One or more device tokens |
+ You must provide one of `tokens` or `devices`.
+
+ | Property | Type | Description |
+ | --------- | ---------------------------------------- | --------------------------- |
+ | tokens\* | `string[]` | One or more device tokens. |
+ | devices\* | [`PushDevice[]`](#the-pushdevice-object) | One or more device objects. |
- | Property | Type | Description |
- | -------- | ---------- | ------------------------- |
- | tokens\* | `string[]` | One or more device tokens |
+ You must provide one of `tokens` or `devices`.
+
+ | Property | Type | Description |
+ | --------- | ---------------------------------------- | --------------------------- |
+ | tokens\* | `string[]` | One or more device tokens. |
+ | devices\* | [`PushDevice[]`](#the-pushdevice-object) | One or more device objects. |
+
+
+
+ You must provide one of `target_arns` or `devices`.
+
+ | Property | Type | Description |
+ | ------------- | ---------------------------------------- | ------------------------------- |
+ | target_arns\* | `string[]` | One or more device target ARNs. |
+ | devices\* | [`PushDevice[]`](#the-pushdevice-object) | One or more device objects. |
diff --git a/data/code/users/set-channel-data-devices.ts b/data/code/users/set-channel-data-devices.ts
new file mode 100644
index 000000000..8ad1dcde3
--- /dev/null
+++ b/data/code/users/set-channel-data-devices.ts
@@ -0,0 +1,170 @@
+const languages = {
+ curl: `
+# Find the channel_id in your Knock dashboard under Integrations > Channels
+curl -X PUT https://api.knock.app/v1/users/1/channel_data/8209f26c-62a5-461d-95e2-a5716a26e652 \\
+ -H "Content-Type: application/json" \\
+ -H "Authorization: Bearer sk_test_12345" \\
+ -d '{
+ "data": {
+ "devices": [
+ { "token": "user_device_token_1", "locale": "en-US", "timezone": "America/New_York" },
+ { "token": "user_device_token_2" }
+ ]
+ }
+ }'
+`,
+ node: `
+import Knock from "@knocklabs/node";
+const knockClient = new Knock({ apiKey: process.env.KNOCK_API_KEY });
+
+// Find this value in your Knock dashboard under Integrations > Channels
+const APNS_CHANNEL_ID = "8209f26c-62a5-461d-95e2-a5716a26e652";
+
+await knockClient.users.setChannelData(user.id, APNS_CHANNEL_ID, {
+ data: {
+ devices: [
+ { token: userDeviceToken1, locale: "en-US", timezone: "America/New_York" },
+ { token: userDeviceToken2 },
+ ],
+ },
+});
+`,
+ python: `
+from knockapi import Knock
+client = Knock(api_key="sk_12345")
+
+# Find this value in your Knock dashboard under Integrations > Channels
+apns_channel_id = "8209f26c-62a5-461d-95e2-a5716a26e652"
+
+client.users.set_channel_data(
+ user_id=user.id,
+ channel_id=apns_channel_id,
+ data={
+ "devices": [
+ {"token": "user_device_token_1", "locale": "en-US", "timezone": "America/New_York"},
+ {"token": "user_device_token_2"}
+ ]
+ }
+)
+`,
+ ruby: `
+require "knockapi"
+
+client = Knockapi::Client.new(api_key: "sk_12345")
+
+# Find this value in your Knock dashboard under Integrations > Channels
+apns_channel_id = "8209f26c-62a5-461d-95e2-a5716a26e652"
+
+client.users.set_channel_data(user.id, apns_channel_id, {
+ devices: [
+ { token: "user_device_token_1", locale: "en-US", timezone: "America/New_York" },
+ { token: "user_device_token_2" }
+ ]
+})
+`,
+ csharp: `
+var knockClient = new KnockClient(
+ new KnockOptions { ApiKey = "sk_12345" });
+
+var devices = new List> {
+ new() { { "token", "user_device_token_1" }, { "locale", "en-US" }, { "timezone", "America/New_York" } },
+ new() { { "token", "user_device_token_2" } }
+};
+
+// Find this value in your Knock dashboard under Integrations > Channels
+var apnsChannelId = "8209f26c-62a5-461d-95e2-a5716a26e652";
+
+var channelData = new Dictionary{
+ { "devices", devices }
+};
+
+await knockClient.Users.SetChannelData(user.Id, apnsChannelId, channelData);
+`,
+ elixir: `
+knock_client = MyApp.Knock.client()
+
+# Find this value in your Knock dashboard under Integrations > Channels
+apns_channel_id = "8209f26c-62a5-461d-95e2-a5716a26e652"
+
+Knock.Users.set_channel_data(knock_client, user.id, apns_channel_id, %{
+ devices: [
+ %{token: "user_device_token_1", locale: "en-US", timezone: "America/New_York"},
+ %{token: "user_device_token_2"}
+ ]
+})
+`,
+ php: `
+use Knock\\KnockSdk\\Client;
+
+$client = new Client('sk_12345');
+
+// Find this value in your Knock dashboard under Integrations > Channels
+$apns_channel_id = "8209f26c-62a5-461d-95e2-a5716a26e652";
+
+$client->users()->setChannelData($user->id(), $apns_channel_id, [
+ 'devices' => [
+ ['token' => 'user_device_token_1', 'locale' => 'en-US', 'timezone' => 'America/New_York'],
+ ['token' => 'user_device_token_2']
+ ]
+]);
+`,
+ go: `
+import (
+ "context"
+
+ "github.com/knocklabs/knock-go"
+ "github.com/knocklabs/knock-go/option"
+ "github.com/knocklabs/knock-go/param"
+)
+
+ctx := context.Background()
+knockClient := knock.NewClient(option.WithAPIKey("sk_12345"))
+
+// Find this value in your Knock dashboard under Integrations > Channels
+apnsChannelId := "8209f26c-62a5-461d-95e2-a5716a26e652"
+
+_, _ = knockClient.Users.SetChannelData(ctx, user.ID, apnsChannelId, knock.UserSetChannelDataParams{
+ ChannelDataRequest: knock.ChannelDataRequestParam{
+ Data: param.Raw(map[string]interface{}{
+ "devices": []map[string]interface{}{
+ {"token": "user_device_token_1", "locale": "en-US", "timezone": "America/New_York"},
+ {"token": "user_device_token_2"},
+ },
+ }),
+ },
+})
+`,
+ java: `
+import app.knock.api.client.KnockClient;
+import app.knock.api.client.okhttp.KnockOkHttpClient;
+import app.knock.api.models.users.ChannelData;
+import app.knock.api.models.users.UserSetChannelDataParams;
+import app.knock.api.core.JsonValue;
+import java.util.List;
+import java.util.Map;
+
+KnockClient client = KnockOkHttpClient.builder()
+ .apiKey("sk_12345")
+ .build();
+
+// Find this value in your Knock dashboard under Integrations > Channels
+String apnsChannelId = "8209f26c-62a5-461d-95e2-a5716a26e652";
+
+List