Skip to content

Importing Data

Stream offers built-in tooling to help you migrate from your current chat provider while keeping the process smooth.

To import data into Stream, create an import file and upload it either through the dashboard or by using the CLI.

You can refer to the File Format section below for details about the expected format.

To get started, you can download a sample file to familiarize yourself with the expected structure, and then import it using the CLI.

Import with the CLI

1. Install the CLI

Install the Stream CLI:

$ curl -fsSL https://getstream.io/cli.sh | bash

For other installation methods, see the installation guide.

2. Configure Authentication

Log in once per machine, then link your working directory to your app:

$ getstream login
$ getstream init

In CI or scripts, skip login and set STREAM_API_KEY and STREAM_API_SECRET in the environment instead.

3. Upload a file

Once validated, upload your file to start the import. The maximum file size supported for upload is 300 MB. Pass --watch to follow the import task until it finishes:

$ getstream import chat my-data.jsonl --watch

4. Check Import Status

Monitor the status of your import using the task ID printed by the upload:

$ getstream import status 79502357-3f4b-486e-9a78-400a184a1088

To list all imports for your application:

$ getstream import list

For more detailed descriptions of all CLI import commands, see the getstream import reference.

How Imported Data Is Processed

Imports are processed as an upsert: every item in the file is imported — items that don't exist yet are created, and items that already exist are updated with the data from the file.

For items that already exist, the fields marked in the upserted column of each type's field table are overwritten — and this applies to every upserted field, whether or not it is present in your file:

  • If the field is present in the file, the existing value is replaced with the value from the file.
  • If the field is missing from the file, the existing value is still overwritten — with the field's zero value (empty string, false, empty array, null) or its documented default (for example, role defaults to user and created_at defaults to the import time).
  • Custom data is replaced by what the file contains.
  • Blocked users (blocked_user_ids) are the exception: they are merged, not replaced. The imported IDs are added to the user's existing blocks (deduplicated). For example, if a user already blocks A and B and the file has them blocking C, they end up blocking A, B, and C.
Warning:

An upsert is not a partial update: omitting a field does not mean "keep the existing value". For example, re-importing an existing user without their teams field clears their teams, and without their role resets them to user. Always include all the data you want to persist for each item, not just the fields you want to change.

Info:

Imports never delete data: an import can create items and overwrite fields, but it cannot remove existing items (or existing blocks). To delete data, use the regular API.

Import from the Dashboard

Info:

While we work on improving the import experience, importing from the dashboard is temporarily disabled.
Please use the CLI in the meantime.

File Format

As you prepare your import file, keep the following requirements in mind—otherwise the import may fail during validation.

  • File Structure: The file should be generated using the JSON Lines format, where each line in the file should be a valid JSON object. Each object represents one item of type user, device, channel, member, message, reaction, future_channel_ban, poll, or poll_vote that will be imported into your application.

  • Item order: Items in the file should be in a specific order, otherwise the validation step will fail. This order makes reference validation more efficient.
    For a Chat import, the items in the file should be defined in the following order:

    • users
    • devices
    • future_channel_ban
    • channels
    • members
    • messages
    • reactions
    • polls
    • poll_votes
  • Object References: Every object you reference in the import should either appear as its own object in the file, or the record should already exist in your Stream application.
    For example, if you import a message that references a user_id, your import file may include a separate user with the same ID, or this user should already exist in your application with the same ID.
    This part of the validation process can be tricky, especially with large files. Be cautious with this as it can cause the import to fail.

  • Read State: By default, all imported messages will be marked as read. If you provide the last_read timestamp on a member item, then that member’s unread count will be determined based on the amount of messages that have been created after the last_read timestamp.

  • Distinct Channels: Distinct channels are channels that are created by providing a list of member IDs rather than a channel ID. Under the hood, our API generates a unique channel ID from the member IDs. To import a distinct channel, include the member_ids field and omit the id field entirely.

  • Timestamps: All timestamps must use the same format as the API (RFC 3339), for example: 1985-04-12T23:20:50.52Z.

Object format

As mentioned earlier, each line in the file should be a valid JSON object with the following format:

Name Value Description
type string the item type for this object, allowed values: user, device, channel, member, message, reaction, future_channel_ban, poll, poll_vote
data object the data for this object, see below for the format of each type

Here's an example of a valid file:

{"type":"user","data":{"id":"user_001","name":"Jesse","image":"http://getstream.com","created_at":"2017-01-01T01:00:00Z","role":"moderator","invisible":true,"description":"Taj Mahal guitar player at some point"}}
{"type":"device","data":{"id":"device_001","user_id":"user_001","push_provider_type":"firebase","push_provider_name":"firebase"}}
{"type":"channel","data":{"id":"channel_001","type":"messaging","created_by":"user_001","name":"Rock'n Roll Circus"}}
{"type":"member","data":{"channel_type":"messaging","channel_id":"channel_001","user_id":"user_001","is_moderator":true,"created_at":"2017-02-01T02:00:00Z"}}
{"type":"message","data":{"id":"message_001","channel_type":"messaging","channel_id":"channel_001","user":"user_001","text":"Learn how to build a chat app with Stream","type":"regular","created_at":"2017-02-01T02:00:00Z","attachments":[{"type":"video","asset_url":"https://www.youtube.com/watch?v=o-am4BY-dhs","image_url":"https://i.ytimg.com/vi/o-am4BY-dhs/mqdefault.jpg","thumb_url":"https://i.ytimg.com/vi/o-am4BY-dhs/mqdefault.jpg"}]}}
{"type":"reaction","data":{"message_id":"message_001","type":"love","user_id":"user_001","created_at":"2019-03-02T15:00:00Z"}}

Data format

Info:

All time fields should be in RFC 3339 format

Info:

Note that you can add custom fields to users, channels, members, messages (including attachments) and reactions. The limit is 5KB of custom field data per object.

User Type

The user type fields are shown below:

name type description required upserted
blocked_user_ids array list of blocked users (only user IDs). On upsert these are merged (union, deduplicated) with the user's existing blocks, not replaced.
channel_mutes array list of muted channels (only channel CIDs)
created_at string creation time (default to import time)
deactivated_at string deactivation time
deleted_at string deletion time
id string unique user ID (required)
invisible boolean visibility state (default to false)
language string language
privacy_settings object control user's privacy settings: delivery receipts, typing indicators and read receipts
push_preferences object push preferences
role string the user's role (default to user)
teams array list of teams the user is part of
teams_role object mapping of teams to user roles (see more)
user_mutes array list of muted users (only user IDs)
* string/array/object add as many custom fields as needed (up to 5 KiB)
{
  "type": "user",
  "data": {
    "id": "user_001",
    "name": "Jesse",
    "image": "http://getstream.com",
    "created_at": "2017-01-01T01:00:00Z",
    "role": "moderator",
    "invisible": true,
    "teams": ["admins"],
    "teams_role": { "admins": "team_moderator" },
    "description": "Taj Mahal guitar player at some point"
  }
}

Device Type

Importing devices is the equivalent of registering devices with Stream.

This is useful when migrating from another chat provider to Stream because:

  • Users already have devices registered for push notifications
  • You want to preserve these registrations so users continue receiving notifications immediately after migration
  • Without importing devices, users would miss notifications until they open the app again

The device type fields are shown below:

name type description required upserted
created_at string creation time (default to import time)
id string unique device id
push_provider_type string must be one of the following: firebase, apn, huawei orxiaomi
push_provider_name string name that matches the Push Configuration on your app
user_id string user ID
{
  "type": "device",
  "data": {
    "id": "device_001",
    "user_id": "user_001",
    "created_at": "2019-01-11T02:00:00Z",
    "push_provider_type": "firebase",
    "push_provider_name": "production-firebase-config"
  }
}

Channel Type

The channel type fields are shown below:

name type description required upserted
banned_users array list of banned users (only user IDs)
created_at string creation time (default to import time)
created_by string user who created the channel (user ID)
disabled boolean disabled status (default to false)
frozen boolean frozen status (default to false)
id string unique channel ID (required only if member_ids is not provided)
member_ids array user IDs used for distinct channels (required only if id is not provided)
team string channel team
truncated_at string truncation time
type string channel type (required)
* string/list/object add as many custom fields as needed (up to 5 KiB)
// with channel ID
{
  "type": "channel",
  "data": {
    "id": "channel_001",
    "type": "livestream",
    "created_by": "user_001",
    "name": "Rock'n Roll Circus"
  }
}

Member Type

Channel members store the mapping between users and channels. The fields are shown below:

name type description required upserted
archived_at string time when the channel was archived
channel_id string channel ID (required only if channel_member_ids is not provided)
channel_role string member role (default to channel_member)
channel_type string channel type
created_at string creation time (default to import time)
channel_member_ids array user IDs for distinct channels (required only if id is not provided)
hide_channel boolean hidden status (default to false)
hide_messages_before string messages will be hidden before this time
invited boolean whether the user was invited (default to false)
invite_accepted_at string time when the user accepted the invite
invite_rejected_at string time when the user rejected the invite
last_read string last time the member read the channel
user_id string user ID
* string/list/object add as many custom fields as needed (up to 5 KiB)
Info:

If your app uses multi-tenancy, the referenced channel and user items must have a matching team.

{
  "type": "member",
  "data": {
    "channel_id": "channel_001",
    "channel_type": "livestream",
    "user_id": "user_001",
    "channel_role": "channel_member",
    "created_at": "2017-02-01T02:00:00Z"
  }
}

Message Type

The message type fields are shown below:

name type description required upserted
attachments array message attachments, see the attachment section below
channel_id string channel ID (required only if channel_member_ids is not provided)
channel_member_ids list of strings user IDs for distinct channels (required only if id is not provided)
channel_type string channel type
created_at string creation time (default to import time)
deleted_at string deletion time
html string safe HTML generated from the text
id string unique message ID
mentioned_users_ids array mentioned user IDs
parent_id string parent message ID (type should be "reply")
pin_expires string time when pin expires (requires pinned_at and pinned_by_id)
pinned_at string time when message was pinned (requires pin_expires and pinned_by_id)
pinned_by_id string pinned_by user ID (requires pinned_at and pin_expires)
quoted_message_id string quoted message ID
restricted_visibility array user IDs that can see this message (see documentation for more information)
show_in_channel bool define if reply should be shown in the channel as well (default to false)
text string message text
type string message type (available type: regular, reply, deleted or system)
user string user ID who posted the message
* string/list/object add as many custom fields as needed (up to 5 KiB)
{
  "type": "message",
  "data": {
    "id": "message_001",
    "channel_type": "livestream",
    "channel_id": "channel_001",
    "user": "user_001",
    "text": "Such a great song, check out my solo at 2:25",
    "type": "regular",
    "created_at": "2017-02-01T02:00:00Z"
  }
}

Message Attachments

The attachments are a great way to extend Stream's functionality. If you want to have a custom product attachment, location attachment, checkout, etc., attachments are the way to go.
The fields below are automatically picked up and shown by our component libraries.

Info:

Note that all attachment URLs must be publicly accessible, otherwise the import will fail.

name type description required
asset_url string URL to the audio, video, or image resource
image_url string URL to the attached image
migrate_resources boolean if true, attachment will be migrated to our CDN (default to false)
thumb_url string URL to the attachment thumbnail (recommended for images and videos)
type string attachment type (built-in types: audio, video, image and text)
* string/list/object add as many custom fields as needed (up to 5 KiB)
{"type":"message","data":{...,"attachments":[{"type":"image","image_url":"https://my.domain.com/image.jpg","thumb_url":"https://my.domain.com/image-thumb.jpg"},{"type":"video","asset_url":"https://my.domain.com/video.mp4","thumb_url":"https://my.domain.com/video-thumb.jpg"}]}}

For attachment migration, only image_url, thumb_url and asset_url fields will be migrated to our CDN and the original URL will be replaced with the new one. The files should not be empty. The import will fail if resource migration fails. In the error you can see the URL and message ID for the failed migration.

Reaction Type

The reaction type fields are shown below:

name type description required upserted
created_at string creation time
message_id string message ID
type string reaction type
user_id string user ID
score int reaction score (defaults to 1 when omitted). Summed into sum_scores.
* string/list/object add as many custom fields as needed (up to 5 KiB)
{
  "type": "reaction",
  "data": {
    "message_id": "message_001",
    "type": "love",
    "user_id": "user_001",
    "score": 1,
    "created_at": "2019-03-02T15:00:00Z"
  }
}

Future Channel Ban Type

The future_channel_ban type fields are shown below:

name type description required upserted
created_at string creation time
created_by string user ID who initiated this future channel ban
target_id string user ID who will be ban for all future channels
shadow boolean determine if the future channel ban is shadowed
reason string determine the reason for the ban
{
  "type": "future_channel_ban",
  "data": {
    "created_by": "user_001",
    "target_id": "user_002",
    "shadow": true,
    "created_at": "2019-03-02T15:00:00Z"
  }
}

Poll Type

Polls are a shared entity: the same poll and poll_vote items are used by both Chat and Feeds. Import the poll first, then its votes and answers as separate poll_vote items. Poll options are nested inline on the poll; their ids are preserved on import so that each poll_vote.option_id lines up.

The poll type fields are shown below:

name type description required upserted
id string unique poll ID (max 255 chars)
name string the poll question / title
created_by_id string user ID of the poll creator
description string poll description
voting_visibility string public or anonymous (default to public)
enforce_unique_vote boolean only allow a single vote per user (default to false)
max_votes_allowed integer maximum votes per user, between 1 and 10
allow_user_suggested_options boolean allow users to suggest new options (default to false)
allow_answers boolean allow free-form text answers (default to false)
is_closed boolean whether the poll is closed for voting (default to false)
options array poll options, see the option fields below
created_at string creation time (default to import time)
updated_at string last update time (default to created_at)
* string/list/object add as many custom fields as needed (up to 5 KiB)

Each entry in options has the following fields:

name type description required
id string unique option ID, referenced by poll_vote.option_id (max 255 chars)
text string the option text
* string/list/object add as many custom fields as needed (up to 5 KiB)
{
  "type": "poll",
  "data": {
    "id": "poll_001",
    "name": "Which feature should we build next?",
    "created_by_id": "user_001",
    "voting_visibility": "public",
    "enforce_unique_vote": true,
    "max_votes_allowed": 1,
    "options": [
      { "id": "option_001", "text": "Threads" },
      { "id": "option_002", "text": "Voice messages" },
      { "id": "option_003", "text": "Polls" }
    ],
    "created_at": "2024-01-01T00:00:00Z"
  }
}

Poll Vote Type

A poll_vote represents either a vote (pointing at an option via option_id) or an answer (free-form text via answer_text). The two are mutually exclusive — set is_answer to true and provide answer_text for an answer; otherwise provide option_id for a regular vote. Answers are only accepted when the referenced poll has allow_answers enabled.

The poll_vote type fields are shown below:

name type description required upserted
id string unique poll vote ID (max 255 chars)
poll_id string ID of the poll this vote belongs to
user_id string user ID who cast the vote
option_id string ID of the chosen option (required for a vote, omit for an answer)
is_answer boolean set to true when this is a free-form answer (default to false)
answer_text string the free-form answer text (required when is_answer is true)
created_at string creation time (default to import time)
updated_at string last update time (default to created_at)
{
  "type": "poll_vote",
  "data": {
    "id": "vote_001",
    "poll_id": "poll_001",
    "option_id": "option_001",
    "user_id": "user_002",
    "created_at": "2024-01-01T01:00:00Z"
  }
}

Validation

We use JSON Schema to define and validate the structure of our data.
The Chat schema files are available here.

These schema files cover approximately 99% of our validation rules. Some validations depend on your specific configuration, such as custom permission policies or custom channel type configurations.

To validate that your data is in the correct format, you can either:

  • validate the data on the fly (while the data is generated), or
  • validate the data once the file is generated

There are many JSON Schema validators available for different programming languages that you can use to validate your data.

Error Messages

When problems occur during analysis, they will show up in the dashboard. A list of errors will be shown in JSON format. Where applicable, the offending item will be included, for example:

{
  "errors": [
    {
      "error": "Validation error: max channelID length exceeded (64)",
      "item_type": "channel",
      "item": {
        "id": "waytoolongwaytoolongwaytoolongwaytoolongwaytoolongwaytoolongwaytoolong",
        "type": "messaging",
        "created_by": "userA-7D3CA510-CB3C-479E-B5FA-69FC2D48410F",
        "created_at": "0001-01-01T00:00:00Z",
        "updated_at": "0001-01-01T00:00:00Z"
      }
    }
  ],
  "stats": {
    "total": {
      "messages": 0,
      "members": 0,
      "reactions": 0,
      "channels": 1,
      "users": 0
    }
  }
}
Error Description
Validation error: max "field" length exceeded (field-length) Maximum length of field exceeded
Validation error: either channel.id or channel.member_ids should be provided, but not both Either define channel as a regular channel or a distinct channel, but not both
Validation error: channel.id or channel.member_ids required, but not both At least one of channel.id or channel.member_ids must be provided
Validation error: "field" required Missing required field
Validation error: "field" is a reserved field Field provided is reserved
Validation error: duplicated item "id" Item and id combination is duplicated
Validation error: created_by user id doesn't exist (channel "messaging:abc"). please include all users as separate user entries All users referenced by all objects, for example in channel.created_by, should be included in the import file
Validation error: 'value' is not a valid field The value provided for a particular field is not valid. For example, a channel.id contains invalid characters
Validation error: user id with teams X cannot be a member of channel Y with team Z The member item references a user and channel that do not have a matching team
Parse error: invalid item type "foobar" An item was included with an invalid item type, only: user, device, channel, member, message, reaction, future_channel_ban, poll and poll_vote are allowed
Parse error: invalid character ',' looking for beginning of value The import contains malformed JSON

This is not an exhaustive list of possible errors, but these are the most common ones.