New: Headless Commerce with Remix! read more

Importing Product Data

If you have hundreds, thousands or more products, inputting all the data by hand via the Admin UI can be too inefficient. To solve this, Vendure supports bulk-importing product and other data.

Data import is also useful for setting up test or demo environments, and is also used by the @vendure/testing package for end-to-end tests.

Product Import Format

Vendure uses a flat .csv format for importing product data. The format encodes data about:

  • products
  • product variants
  • product & variant assets
  • product & variant facets
  • product & variant custom fields

Here’s an example which defines 2 products, “Laptop” and “Clacky Keyboard”. The laptop has 4 variants, and the keyboard only a single variant.

name            , slug            , description               , assets                      , facets                              , optionGroups    , optionValues , sku         , price   , taxCategory , stockOnHand , trackInventory , variantAssets , variantFacets
Laptop          , laptop          , "Description of laptop"   , laptop_01.jpg|laptop_02.jpg , category:electronics|brand:Apple    , screen size|RAM , 13 inch|8GB  , L2201308    , 1299.00 , standard    , 100         , false          ,               , 
                ,                 ,                           ,                             ,                                     ,                 , 15 inch|8GB  , L2201508    , 1399.00 , standard    , 100         , false          ,               , 
                ,                 ,                           ,                             ,                                     ,                 , 13 inch|16GB , L2201316    , 2199.00 , standard    , 100         , false          ,               , 
                ,                 ,                           ,                             ,                                     ,                 , 15 inch|16GB , L2201516    , 2299.00 , standard    , 100         , false          ,               , 
Clacky Keyboard , clacky-keyboard , "Description of keyboard" , keyboard_01.jpg             , category:electronics|brand:Logitech ,                 ,              , A4TKLA45535 , 74.89   , standard    , 100         , false          ,               ,

Here’s an explanation of each column:

  • name: The name of the product. Rows with an empty “name” are interpreted as variants of the preceeding product row.
  • slug: The product’s slug. Can be omitted, in which case will be generated from the name.
  • description: The product description.
  • assets: One or more asset file names separated by the pipe (|) character. The files must be located on the local file system, and the path is interpreted as being relative to the importAssetsDir as defined in the VendureConfig. The first asset will be set as the featuredAsset.
  • facets: One or more facets to apply to the product separated by the pipe (|) character. A facet has the format <facet-name>:<facet-value>.
  • optionGroups: OptionGroups define what variants make up the product. Applies only to products with more than one variant.
  • optionValues: For each optionGroup defined, a corresponding value must be specified for each variant. Applies only to products with more than one variant.
  • sku: The Stock Keeping Unit (unique product code) for this product variant.
  • price: The price can be either with or without taxes, depending on your channel settings (can be set later).
  • taxCategory: The name of an existing tax category. Tax categories can be also be imported using the InitialData object.
  • stockOnHand: The number of units in stock.
  • trackInventory: Whether this variant should have its stock level tracked, i.e. the stock level is automatically decreased for each unit ordered.
  • variantAssets: Same as assets but applied to the product variant.
  • variantFacets: Same as facets but applied to the product variant.

Importing Custom Field Data

If you have CustomFields defined on your Product or ProductVariant entities, this data can also be encoded in the import csv:

  • product:<customFieldName>: The value of this column will populate Product.customFields[customFieldName].
  • variant:<customFieldName>: The value of this column will populate ProductVariant.customFields[customFieldName].

Importing relation custom fields

To import custom fields with the type relation, the value in the CSV must be a stringified object with an id property:

... ,product:featuredReview
... ,"{ ""id"": 123 }"

Importing list custom fields

To import custom fields with list set to true, the data should be separated with a pipe (|) character:

... ,product:keywords
... ,tablet|pad|android

Importing data in multiple languages

If a field is translatable (i.e. of localeString type), you can use column names with an appended language code (e.g. name:en, name:de, product:keywords:en, product:keywords:de) to specify its value in multiple languages.

Use of language codes has to be consistent throughout the file. You don’t have to translate every translatable field. If there are no translated columns for a field, the generic column’s value will be used for all languages. But when you do translate columns, the set of languages for each of them needs to be the same. As an example, you cannot use name:en and name:de, but only provide slug:en (it’s okay to use only a slug column though, in which case this slug will be used for both the English and the German version).

Initial Data

As well as product data, other initialization data can be populated using the InitialData object. This format is intentionally limited; more advanced requirements (e.g. setting up ShippingMethods that use custom checkers & calculators) should be carried out via scripts which interact with the Admin GraphQL API.

import { InitialData, LanguageCode } from '@vendure/core';

export const initialData: InitialData = {
    paymentMethods: [
            name: 'Standard Payment',
            handler: {
                code: 'dummy-payment-handler',
                arguments: [{ name: 'automaticSettle', value: 'false' }],
    roles: [
            code: 'administrator',
            description: 'Administrator',
            permissions: [
    defaultLanguage: LanguageCode.en,
    countries: [
        { name: 'Austria', code: 'AT', zone: 'Europe' },
        { name: 'Malaysia', code: 'MY', zone: 'Asia' },
        { name: 'United Kingdom', code: 'GB', zone: 'Europe' },
    defaultZone: 'Europe',
    taxRates: [
        { name: 'Standard Tax', percentage: 20 },
        { name: 'Reduced Tax', percentage: 10 },
        { name: 'Zero Tax', percentage: 0 },
    shippingMethods: [{ name: 'Standard Shipping', price: 500 }, { name: 'Express Shipping', price: 1000 }],
    collections: [
            name: 'Electronics',
            filters: [
                    code: 'facet-value-filter',
                    args: { facetValueNames: ['Electronics'], containsAny: false },
            assetPaths: ['jakob-owens-274337-unsplash.jpg'],
  • paymentMethods: Defines which payment methods are available.
    • name: Name of the payment method.
    • handler: Payment plugin handler information.
  • roles: Defines which user roles are available.
    • code: Role code name.
    • description: Role description.
    • permissions: List of permissions to applied to the role.
  • defaultLanguage: Sets the language which will be used for all translatable entities created by the initial data e.g. Products, ProductVariants, Collections etc. Should correspond to the language used in your product csv file.
  • countries: Defines which countries are available.
    • name: The name of the country in the language specified by defaultLanguage
    • code: A standardized code for the country, e.g. ISO 3166-1
    • zone: A Zone to which this country belongs.
  • defaultZone: Sets the default shipping & tax zone for the default Channel. The zone must correspond to a value of zone set in the countries array.
  • taxRates: For each item, a new TaxCategory is created, and then a TaxRate is created for each unique zone defined in the countries array.
  • shippingMethods: Allows simple flat-rate ShippingMethods to be defined.
  • collections: Allows Collections to be created. Currently, only collections based on facet values can be created (code: 'facet-value-filter'). The assetPaths and facetValueNames value must correspond to a values specified in the products csv file. The name should match the value specified in the product csv file (or can be a normalized - lower-case & hyphenated - version thereof). If there are FacetValues in multiple Facets with the same name, the facet may be specified with a colon delimiter, e.g. brand:apple, flavour: apple.

Populating The Server

The populate() function

The @vendure/core package exposes a populate() function which can be used along with the data formats described above to populate your Vendure server:

// populate-server.ts
import { bootstrap, DefaultJobQueuePlugin } from '@vendure/core';
import { populate } from '@vendure/core/cli';

import { config } from './vendure-config.ts';
import { initialData } from './my-initial-data.ts';

const productsCsvFile = path.join(__dirname, 'path/to/products.csv')

const populateConfig = {
  plugins: (config.plugins || []).filter(
    // Remove your JobQueuePlugin during populating to avoid
    // generating lots of unnecessary jobs as the Collections get created.
    plugin => plugin !== DefaultJobQueuePlugin,

  () => bootstrap(config),
  'my-channel-token' // optional - used to assign imported 
)                    // entities to the specified Channel

.then(app => {
  return app.close();
  () => process.exit(0),
  err => {

Custom populate scripts

If you require more control over how your data is being imported - for example if you also need to import data into custom entities - you can create your own CLI script to do this: see Stand-Alone CLI Scripts.

In your script you can make use of the internal parse and import services:

Using these specialized import services is preferable to using the normal service-layer services (ProductService, ProductVariantService etc.) for bulk imports. This is because these import services are optimized for bulk imports (they omit unnecessary checks, use optimized SQL queries) and also do not publish events when creating new entities.