C# Utility to remotely trigger an extract refresh using undocumented REST API

Version 3

    We had a requirement to trigger extract refreshes remotely after our data marts have built. You can do this with TabCmd but running it remotely is extremely clunky and there's no feedback to tell you if it worked so i wrote my own utility in C#. The marts are built in SSIS so i wrote a bit of C# to talk to the undocumented internal REST API. I couldn't find anyone else out there that had done this exact thing before i.e. call the runExtractRefreshesOnDatasources method so I thought it would be worth sharing. I figured out how to do it by sniffing the web traffic with fiddler. This is the source code for the proof of concept i did in a console app. I've also attached the exe so it can be run standalone, you just need to change the following settings in the config file:


    1. The URL of your Tableau Server
    2. The Site (only if you are not using the default site)
    3. Username (of an account that has permissions to run extracts)
    4. Password
    5. Datasource IDs - a comma separated list of data source IDs (the ID can be found from the URL e.g. this one is 46 http://tableau/#/site/Sandbox/datasources/46/connectedWorkbooks )


    The only requirement to run this is to have .Net framework 4.0 or above installed (usually included with Windows machines).


    Once you've figured out how to login and get the token from the cookie you can basically automate anything you can that you can do in the front-end so by capturing the request, so if anyone needs any pointers on how to automate anything else then i'm happy to help.


    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Numerics;
    using System.Security.Cryptography;
    using System.Text;
    using System.Web.Script.Serialization;
    using System.Xml.Linq;
    namespace RunExtractRefresh
        class Program
            static void Main(string[] args)
                var baseAddress = Properties.Settings.Default.TableauServerBaseURL;
                var site = Properties.Settings.Default.Site; // Empty for default site
                var username = Properties.Settings.Default.Username;
                var password = Properties.Settings.Default.Password;
                var datasourceIds = Properties.Settings.Default.DataSourceIDs.Split(',');
                var cookies = new CookieContainer();
                var handler = new HttpClientHandler() { CookieContainer = cookies };
                using (var client = new HttpClient(handler))
                    client.BaseAddress = new Uri(baseAddress);
                    Login(cookies, client, username, password, site);
                    RunExtractRefresh(client, datasourceIds);
            private static void Login(CookieContainer cookies, HttpClient client, string username, string password, string site)
                var response = client.GetAsync("auth.xml").Result;
                var authString = response.Content.ReadAsStringAsync().Result;
                var authDoc = XElement.Parse(authString);
                var modulus = authDoc.Elements("modulus").First().Value;
                var exponent = authDoc.Elements("exponent").First().Value;
                var authenticityToken = authDoc.Elements("authenticity_token").First().Value;
                var encryptedPassword = EncryptPassword(modulus, exponent, password);
                using (var content = new MultipartFormDataContent())
                    var values = new[]  
                        new KeyValuePair<string, string>("username", username),  
                        new KeyValuePair<string, string>("format", "xml"),  
                        new KeyValuePair<string, string>("crypted", encryptedPassword),  
                        new KeyValuePair<string, string>("target_site", site),
                        new KeyValuePair<string, string>("authenticity_token", authenticityToken),  
                    foreach (var keyValuePair in values)
                        content.Add(new StringContent(keyValuePair.Value), String.Format("\"{0}\"", keyValuePair.Key));
                    var result = client.PostAsync("manual/auth/login.xml", content).ContinueWith(x => x.Result.EnsureSuccessStatusCode()).Result;
                    var responseCookies = cookies.GetCookies(client.BaseAddress).Cast<Cookie>();
                    var token = responseCookies.Where(c => c.Name == "XSRF-TOKEN").Select(c => c.Value).First();
                    client.DefaultRequestHeaders.Add("X-XSRF-TOKEN", token);
            private static string EncryptPassword(string modulus, string exponent, string password)
                var rsaCryptoServiceProvider = new RSACryptoServiceProvider();
                var parameters = new RSAParameters();
                parameters.Modulus = ToByteArray(BigInteger.Parse(modulus, System.Globalization.NumberStyles.HexNumber));
                parameters.Exponent = ToByteArray(BigInteger.Parse(exponent, System.Globalization.NumberStyles.HexNumber));
                var encryptedByteArray = rsaCryptoServiceProvider.Encrypt(Encoding.UTF8.GetBytes(password), false);
                return BitConverter.ToString(encryptedByteArray).Replace("-", "").ToLower();
            public static byte[] ToByteArray(BigInteger b)
                var x = b.ToByteArray(); // x is little-endian  
                Array.Reverse(x);        // now it is big-endian  
                if (x[0] == 0)
                    var newarray = new byte[x.Length - 1];
                    Array.Copy(x, 1, newarray, 0, newarray.Length);
                    return newarray;
                    return x;
            public static void RunExtractRefresh(HttpClient client, string[] datasourceIds)
                var url = "/vizportal/api/web/v1/runExtractRefreshesOnDatasources";
                var request = new
                    method = "runExtractRefreshesOnDatasources",
                    @params = new
                        ids = datasourceIds,
                        type = "RefreshExtract"
                var content = new JavaScriptSerializer().Serialize(request);
                var response = client.PostAsync(url, new StringContent(content, Encoding.UTF8, "application/json"))
                    .ContinueWith(x => x.Result.EnsureSuccessStatusCode())
                var responseString = response.Content.ReadAsStringAsync().Result;
                if (responseString.Contains("errors"))
                    throw new Exception("Run extract refreshes on datasources unsucessful: " + responseString);
                    Console.WriteLine("Data sources have sucessfully been triggered to extract: " + String.Join(",", datasourceIds.Select(p => p.ToString()).ToArray()));