Updating Ecwid product’s options on BATCH, instead of one by one…

We have a customer that has an Ecwid store with some hundreds of products there, and he came to us looking for a solution to update their product’s options fast, because ecwid interface just permit you to update this options one by one, you need to edit each product to configure the options.

I have been studying the Ecwid’s API, they have two versions:

Ecwid API v1.0 (Obsolete, http://help.ecwid.com/customer/portal/articles/1163920-product-api)

Ecwid oAuth API v3.0 (Recommended, http://api.ecwid.com/)

My customer was looking for something cheap and simple, then I tough about developing a simple powershell script or console application to do that for him. But, as a local application, is not possible to receive the oAuth response containing the authentication token. For that, I should have an application hosted on some website to receive this credentials and then use them. But I managed to that using a workaround. That’s what I am posting here.

Remember that, the correct way would be create an APP for Ecwid store to add this kind of new feature to your Ecwid store. But this works well too.

1. Getting an Authentication Token

There’s is one APP that helps you develop for Ecwid, it’s a API Background:

https://api-playground.ecwid.com/#!/

API Console_ explore and play with Ecwid API

This app will help us to get an application token, and since Ecwid does not have an APP Secret, this will work for our application too.

Folow these steps

  1. Login to this application using you Ecwid account.
  2. You need to give the app the rights to access your data.
  3. They will instruct you to install a chrome extension called Postman
  4. The app will give you a URL that will look like this : https://ecwid-api-playground.s3.amazonaws.com/2676429_73e978474842387.json.postman_collection
  5. You should use this URL on Import function of Postman, as described by the app.
  6. Once you entered your Postman extension and imported successfully this file, click on Ecwid API that will be shown on the left space of Postman, then click on Products and on Search Products.
  7. As soon as you do this you will see a URL showing your AUTHENTICATION TOKEN!
  8. Copy that and store somewhere on your computer.
  9. You can use the button to check if your token is working, it must return some json string with your products.

Postman after imported file

2015-07-01 19_23_38-Postman

Postman showing your token (the token was partially purposely hidden)

2015-07-01 19_23_58-Postman

2. Ecwid Options Manager

I have written a small console (command prompt) application for windows that does the rest. You just need to pass your store ID, your token and choose what you want to do. Remember that this program is given freely with no warranties, cannot be sold, and is free to modify since you keep the rights to the author and linking to this page.

Usage samples:

EcwidOptionsMgr -store 123... -token HuEy... -listproducts

What is does?: List all products with no filter

EcwidOptionsMgr -store 123... -token HuEy... -listproducts -filter "ac/dc"

What is does?: List all products with no filtering by string “ac/dc”

EcwidOptionsMgr -store 123... -token HuEy... -all -addoption "option1"

What is does?: Add the option to all products

EcwidOptionsMgr -store 123... -token HuEy... -all -addoption "option1" -pricemodifier "12,5"

What is does?: Add the option to all products with the specified price modifier

EcwidOptionsMgr -store 123... -token HuEy... -filter "ac/dc" -addoption "option1" -pricemodifier "12,5"

What is does?: Add the option just to products that matches filter with the specified price modifier

EcwidOptionsMgr -store 123... -token HuEy... -all -removeoption "option1"

What is does?: Remove any option named “option1″ from all products

EcwidOptionsMgr -store 123... -token HuEy... -filter "ac/dc" -all -removeoption "option1"

What is does?: Remove any option named “option1″ from all products that matches filter

Parameters explained

Store and Token

  •  You should always pass this parameters, they make it possible to connect to your Ecwid store. To make the commands smaller there’s included on the package a BATCH called myecwid.bat that you can EDIT to put your token an store id and then use it to call program EcwidOptionsMgr.exe. All the parameters passed to this BATCH will be bypass to the EcwidOptionsMgr.exe.

Sample call for myecwid.bat

myecwid.bat -listproducts

Action { listproducts | addoption | removeoption }, you should choose one always.

  • list products – just list all the products the will be affected by your query, if you are using a “-all” scope, it will list all the products, if you are using a “-filter” parameter, it will filter the products by name and will just show you the products containing the string you passed.
  • add option – add an option, the name of the new option will be the string passed right after this parameter. This action will respect your chosen scope “-all” or “-filter”, this means that it will just add this option to the products matched on your scope, all products or filtered products. It’s interesting to use the “list products” action prior to this to check what’s going to be modified by this action. The parameter “-price” just works with this action.
  • remove option – remove an option from all the products on the scope, exactly the opposite of “add option” action.

Price

  • This parameter is optional, you should use it when you need to add a price modifier to a new option.

Sample: Listing products behavior

2015-07-01 19_59_48-Administrator_ Command Prompt

Sample: Removing options for a small set of products filtered by the string “minions”

2015-07-01 20_01_53-Administrator_ Command Prompt

Sample: Adding option for a small set of products filtered by the string “minions”2015-07-01 20_01_22-Administrator_ Command Prompt

Sample: Adding option for a small set of products filtered by the string “minions” with a price of $ 1.20 modifier

2015-07-01 20_06_30-Administrator_ Command Prompt

Result: Option Added!

2015-07-01 20_08_08-Case Minions UFC

3. Are you a developer? No problem, the code is shown below!

>>> Just remember that this program is given freely with no warranties, cannot be sold, and is free to modify since you keep the rights to the author and linking to this page. <<<

Download binaries (Paid!)

Ecwid Options Manager [Download]

Source Files (Don’t forget to add Newtonsoft.Json to compile!)

Program.cs

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EcwidOptionsMgr
{
    class Program
    {
        #region Attributes
        static int? _storeId = null;
        static string _token = null;
        #endregion

        #region Properties
        #endregion

        #region SubTypes
        struct Parameters
        {
            public ActionType? Action;
            public ScopeType? Scope;
            public string ScopeFilter;
            public string Option;
            public string Token;
            public int? Store;
            public double? PriceModifier;
        }
        #endregion

        #region Constants
        const string apiPath = "https://app.ecwid.com/api/v3";

        public enum ActionType
        {
            List,
            Add,
            Remove
        }

        public enum ScopeType
        {
            All,
            Filter
        }
        #endregion

        #region Constructors
        #endregion

        #region Methods

        #region Private
        private static void Process(Parameters parameters)
        {
            _storeId = parameters.Store.Value;
            _token = parameters.Token;

            switch (parameters.Action)
            {
                case ActionType.List:
                    if (parameters.Scope == null)
                        parameters.Scope = ScopeType.All;
                    ListProducts(parameters.Scope.Value, parameters.ScopeFilter);
                    break;
                case ActionType.Add:
                    AddOption(parameters.Option, parameters.PriceModifier, parameters.Scope.Value, parameters.ScopeFilter);
                    break;
                case ActionType.Remove:
                    RemoveOption(parameters.Option, parameters.Scope.Value, parameters.ScopeFilter);
                    break;
            }
        }

        private static void RemoveOption(string option, ScopeType scope, string filter)
        {
            var products = RetrieveProducts(scope == ScopeType.Filter ? filter : null);
            int modified = 0;

            foreach (dynamic prod in products)
            {
                dynamic product = RetrieveFullProduct((int)prod.id);
                JArray choices = product.options[0].choices;
                JArray newChoices = new JArray();

                foreach (dynamic c in choices)
                {
                    if (((string)c.text).ToLower() != option.ToLower())
                        newChoices.Add(new JObject(
                           new JProperty("text", c.text),
                           new JProperty("priceModifier", c.priceModifier),
                           new JProperty("priceModifierType", c.priceModifierType)));
                }

                if (newChoices.Count != choices.Count) // something removed.
                {
                    modified++;

                    Console.WriteLine("Removing any option \"{0}\" from product ({1}) {2}", option, prod.id, prod.name);

                    product.options[0].choices = newChoices;

                    string url = string.Format("{0}/{1}/products/{3}?token={2}", apiPath, _storeId, _token, prod.id);

                    JsonHelper.PutJSON<dynamic>(url, JsonConvert.SerializeObject(new { id = prod.id, options = product.options }));
                }
                else
                    Console.WriteLine("WARNING! Option \"{0}\" not found on product ({1}) {2}", option, prod.id, prod.name);
            }

            Console.WriteLine("Modified products: {0}\r\n", modified);
        }

        private static void AddOption(string option, double? pricemodifier, ScopeType scope, string filter)
        {
            var products = RetrieveProducts(scope == ScopeType.Filter ? filter : null);
            int modified = 0;

            foreach (dynamic prod in products)
            {
                Console.WriteLine("Adding option \"{0}\" on product ({1}) {2}", option, prod.id, prod.name);

                dynamic product = RetrieveFullProduct((int)prod.id);
                JArray choices = product.options[0].choices;

                if (!(choices.ToList().Exists(c => ((string)((dynamic)c).text).ToLower() == option.ToLower())))
                {
                    modified++;

                    choices.Add(new JObject(
                                   new JProperty("text", option),
                                   new JProperty("priceModifier", pricemodifier ?? 0.0),
                                   new JProperty("priceModifierType", "ABSOLUTE")));

                    string url = string.Format("{0}/{1}/products/{3}?token={2}", apiPath, _storeId, _token, prod.id);

                    JsonHelper.PutJSON<dynamic>(url, JsonConvert.SerializeObject(new { id = prod.id, options = product.options }));
                }
                else
                    Console.WriteLine("WARNING! Option \"{0}\" already exists on product ({1}) {2}", option, prod.id, prod.name);
            }

            Console.WriteLine("Modified products: {0}\r\n", modified);
        }

        private static void ListProducts(ScopeType scope, string filter)
        {
            var products = RetrieveProducts(scope == ScopeType.Filter ? filter : null);

            foreach (dynamic item in products)
                Console.WriteLine(string.Format("Id: {0}\r\nProduct: {1}\r\nPrice: {2:C2}\r\n\r\n", item.id, item.name, item.price));

            Console.WriteLine(string.Format("Total products: {0}\r\n", products.Count));
        }

        private static JObject RetrieveFullProduct(int id)
        {
            string url = string.Format("{0}/{1}/products/{3}?token={2}", apiPath, _storeId, _token, id);
            return JsonHelper.GetRequestAsJSON<JObject>(url);
        }

        private static List<JObject> RetrieveProducts(string filter)
        {
            List<JObject> products = new List<JObject>();
            int count = 0;

            Console.WriteLine("\r\nRetrieving products...\r\n");

            string url = "{0}/{1}/products?token={2}&offset={3}{4}";
            filter = string.IsNullOrEmpty(filter) ? string.Empty : "&keyword=" + filter.Trim().Trim('"');

            dynamic prods = null;

            do
            {
                prods = JsonHelper.GetRequestAsJSON<dynamic>(string.Format(url, apiPath, _storeId, _token, count, filter));
                ((JArray)prods.items).ToList().ForEach(o => products.Add((JObject)o));
                count += (int)prods.count;
            }
            while (prods.count != 0 && (count < (int)prods.total));

            return products;
        }

        private static bool IsParametersValid(Parameters p)
        {
            if (p.Token == null)
                return ParameterError("You must have a token to connect your ecwid store");
            else if (p.Store == null)
                return ParameterError("You must pass your store id to connect your ecwid store");
            else if (!p.Action.HasValue)
                return ParameterError("Action not defined");
            else
            {
                if (p.Scope == ScopeType.Filter && p.ScopeFilter == null)
                    return ParameterError("Scope filter not defined");
                else if (p.Action != ActionType.List && p.Scope == null)
                    return ParameterError("Scope must be defined for this action");
                else if (p.Action != ActionType.List && p.Option == null)
                    return ParameterError("Option not defined");

                return true;
            }
        }

        private static bool ParameterError(string message)
        {
            Console.WriteLine(string.Format("Invalid Parameters:\r\n{0}\r\n\r\n", message));
            return false;
        }

        private static Parameters GetParameters(string[] args)
        {
            Parameters p = new Parameters();

            for (int i = 0; i < args.Length; i++)
            {
                if (args[i].ToLower() == "-listproducts") p.Action = ActionType.List;
                else if (args[i].ToLower() == "-all") p.Scope = ScopeType.All;
                else if (args[i].ToLower() == "-filter")
                {
                    p.Scope = ScopeType.Filter;
                    if ((i + 1) < args.Length) p.ScopeFilter = args[++i];
                }
                else if (args[i].ToLower() == "-addoption")
                {
                    p.Action = ActionType.Add;
                    if ((i + 1) < args.Length) p.Option = args[++i];
                }
                else if (args[i].ToLower() == "-removeoption")
                {
                    p.Action = ActionType.Remove;
                    if ((i + 1) < args.Length) p.Option = args[++i];
                }
                else if (args[i].ToLower() == "-price")
                {
                    if ((i + 1) < args.Length)
                    {
                        double price = 0;
                        double.TryParse(args[++i], out price);
                        p.PriceModifier = price != 0 ? (double?)price : null;
                    }
                }
                else if (args[i].ToLower() == "-store")
                {
                    if ((i + 1) < args.Length)
                    {
                        int storeId = 0;
                        int.TryParse(args[++i], out storeId);
                        p.Store = storeId != 0 ? (int?)storeId : null;
                    }
                }
                else if (args[i].ToLower() == "-token")
                {
                    if ((i + 1) < args.Length) p.Token = args[++i];
                }
            }

            return p;
        }
        #endregion

        #region Public
        static void Main(string[] args)
        {
            if (args.Length == 0)
                PrintUsage();
            else
            {
                Parameters p = GetParameters(args);
                if (IsParametersValid(p))
                    Process(p);
                else
                    PrintUsage();
            }
        }

        private static void PrintUsage()
        {
            string[,] samples = new string[,] {
                {"EcwidOptionsMgr -store 123... -token HuEy... -listproducts","List all products with no filter"},
                {"EcwidOptionsMgr -store 123... -token HuEy... -listproducts -filter \"ac/dc\"","List all products with no filtering by string \"ac/dc\""},
                {"EcwidOptionsMgr -store 123... -token HuEy... -all -addoption \"option1\"","Add the option to all products"},
                {"EcwidOptionsMgr -store 123... -token HuEy... -all -addoption \"option1\" -pricemodifier \"12,5\"","Add the option to all products with the specified price modifier"},
                {"EcwidOptionsMgr -store 123... -token HuEy... -filter \"ac/dc\" -addoption \"option1\" -pricemodifier \"12,5\"","Add the option just to products that matches filter with the specified price modifier"},
                {"EcwidOptionsMgr -store 123... -token HuEy... -all -removeoption \"option1\"","Remove any option named \"option1\" from all products"},
                {"EcwidOptionsMgr -store 123... -token HuEy... -filter \"ac/dc\" -all -removeoption \"option1\"","Remove any option named \"option1\" from all products that matches filter"},
            };
            Console.Write("Usage Samples:\r\n\r\n");

            for (int i = 0; i < (samples.Length / samples.Rank); i++)
            {
                Console.Write(samples[i, 0]);
                Console.Write("\r\nRemarks: ");
                Console.Write(samples[i, 1]);
                Console.Write("\r\n\r\n");
            }
        }
        #endregion

        #endregion

        #region Events
        #endregion
    }
}

JsonHelper.cs

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Web;

namespace EcwidOptionsMgr
{
    public static class JsonHelper
    {
        //static NetworkCredential _credentials;

        static JsonHelper()
        {
            // Use when you need credentials to login in.
            //_credentials = new NetworkCredential(User, Pass);
        }

        public static T PostJSON<T>(string request, string json)
        {
            return SendJSON<T>("POST", request, json);
        }

        public static T PutJSON<T>(string request, string json)
        {
            return SendJSON<T>("PUT", request, json);
        }

        public static T DeleteJSON<T>(string request, string json)
        {
            return SendJSON<T>("DELETE", request, json);
        }

        public static T SendJSON<T>(string method, string request, string json)
        {
            byte[] bytes = Encoding.UTF8.GetBytes(json);
            HttpWebRequest req = CreateRequest(request, method, "application/json", contentLength: bytes.Length);
            Stream stm = req.GetRequestStream();
            stm.Write(bytes, 0, bytes.Length);
            stm.Close();
            return GetJSONResponse<T>(req);
        }

        public static T GetRequestAsJSON<T>(string request)
        {
            return GetJSONResponse<T>(CreateRequest(request, "GET", "application/json"));
        }

        public static T GetJSONResponse<T>(HttpWebRequest req)
        {
            using (var response = (HttpWebResponse)req.GetResponse())
            {
                if (response.StatusCode != HttpStatusCode.OK)
                    throw new HttpException((int)response.StatusCode, response.StatusDescription);

                return JsonConvert.DeserializeObject<T>(new StreamReader(response.GetResponseStream()).ReadToEnd());
            }
        }

        public static HttpWebRequest CreateRequest(string request, string method, string contentType, int contentLength = 0)
        {
            HttpWebRequest wr = (HttpWebRequest)WebRequest.Create(request);

            wr.Method = method;
            if (contentLength != 0) wr.ContentLength = contentLength;
            wr.Accept = wr.ContentType = contentType;
            //wr.Credentials = _credentials;

            return wr;
        }
    }
}

Enjoy!! =]