Foreword

Although the Snapchat API isn’t publicly available, it is quite simple to hook into it. A while ago, a full disclosure by Gibson Security was published. However, that documentation is somewhat outdated which is why I want to share more current information on my blog.

At the time of writing, there is no official Snapchat app for Windows Phone and the only functional third-party app, 6snap by the renowned developer Rudy Huyn, is missing a lot of features. Several months ago in an attempt to bring Windows Phone to parity with other platforms, a couple of developers and I worked together to create one for Windows Phone. Unfortunately, none of us have enough time or willpower to finish it. Instead of letting the WinRT API we developed go to waste, we are releasing it to the public in hopes that someone else will be able to create a high quality Snapchat client for Windows Phone. In addition, I want to cover in detail each aspect of our API and the Snapchat API in hopes it will help people understand how it works and all that.

Special thanks goes to GibSec for the full disclosure and Alex Reed for doing most of the reverse engineering work while helping me develop the app.

In this first post of many more to come, I will cover the low level stuff needed to send valid POST and GET requests to the API.

Endpoints

There are three known endpoints as of version 5.0.5 which is at least 1 major release behind at the time of writing. It doesn’t really matter that much since Snapchat doesn’t normally shut down their old endpoints. Regardless, I still want to take another look one day and document new things that I can find.

ph – Obsolete; unused in the latest version of Snapchat

– Obsolete; unused in the latest version of Snapchat bq – Still widely used for many features

– Still widely used for many features loq – The newest version; Used by new features like chat, etc

Since we’ll be using different endpoints in our app, we will need to create an instance that manages requests for each endpoint. This is where the EndpointManager comes in.

/// <summary> /// Provides various methods for sending GET and POST requests to a Snapchat endpoint. /// </summary> internal class EndpointManager { /// <summary> /// Initializes a new <see cref="EndpointManager"/> for an endpoint. /// </summary> /// <param name="baseUri"> /// The base URI of the endpoint. /// </param> /// <param name="staticToken"> /// The static token to use when generating a request token. /// </param> /// <param name="secretToken"> /// The secret token to use when generating a request token. /// </param> /// <param name="userAgent"> /// The user agent to use when sending requests. /// </param> public EndpointManager(Uri baseUri, string staticToken, string secretToken, string userAgent) { Contract.Requires<ArgumentNullException>(baseUri != null); Contract.Requires<ArgumentNullException>(staticToken != null); Contract.Requires<ArgumentNullException>(secretToken != null); Contract.Requires<ArgumentNullException>(userAgent != null); BaseUri = baseUri; StaticToken = staticToken; SecretToken = secretToken; UserAgent = userAgent; } /// <summary> /// Gets the base URI of the API. /// </summary> public Uri BaseUri { get; private set; } /// <summary> /// Gets the static token used when generating a request token. /// </summary> public string StaticToken { get; private set; } /// <summary> /// Gets the secret token used when generating a request token. /// </summary> public string SecretToken { get; private set; } /// <summary> /// Gets or sets the user agent to use when sending requests. /// </summary> public string UserAgent { get; set; } }

In our WinRT API, the SnapchatManager class provides a list of available endpoints exposed internally.

/// <summary> /// Provides various methods for authentication and account registration. /// </summary> public class SnapchatManager { /// <summary> /// The default user agent to use when sending requests. /// </summary> public const string DefaultUserAgent = "User-Agent: Snapchat/5.0.5 (Nexus 4; Android 19; gzip)"; /// <summary> /// Provides a <see cref="EndpointManager"/> for each API version. /// </summary> internal readonly IReadOnlyDictionary<string, EndpointManager> Endpoints; /// <summary> /// Defines all supported API versions. /// </summary> private static readonly IReadOnlyList<string> EndpointVersions = new[] { "bq", "ph", "loq", }; /// <summary> /// Defines the base URI of the API. /// </summary> private static readonly Uri BaseApiUri = new Uri("https://api.snapchat.com/"); /// <summary> /// Initializes a new instance of the Snapchat API with the given auth tokens. /// </summary> /// <param name="staticToken"> /// The static token to use when generating a request token. /// </param> /// <param name="secretToken"> /// The secret token to use when generating a request token. /// </param> public SnapchatManager(string staticToken, string secretToken) { _userAgent = DefaultUserAgent; // Create an endpoint manager for each API version. var managers = new Dictionary<string, EndpointManager>(); foreach (string version in EndpointVersions) { var endpointUri = new Uri(BaseApiUri, version); managers.Add(version, new EndpointManager(endpointUri, staticToken, secretToken, _userAgent)); } Endpoints = managers; } /// <summary> /// Gets or sets the user agent to use when sending requests. /// </summary> public string UserAgent { get { return _userAgent; } set { foreach (EndpointManager ep in Endpoints.Values) ep.UserAgent = value; _userAgent = value; } } private string _userAgent; }

I would’ve included the static and secret tokens used by the official Snapchat app as constants but I’m not too sure about the legality of that. You can find them in GibSec’s full disclosure however. Not to mention that Snapchat hinted at the possibility of releasing a public API which would most likely require you to provide your own tokens.

Generating Request Tokens

In order to send an authenticated POST request, you will need to generate a request token and pass it along with whatever you’re sending to Snapchat, otherwise you’ll face a 401 Unauthorized error. All API calls that use POST require a valid token.

The static token is used for logging in and registration. For all other API calls using POST, you will need to use the authentication token given to you in the response after a successful login request.

The request token is generated by combining the hashes of the token and timestamp each salted with the secret token. The combination is done according to a hashing pattern. Let’s create a GenerateRequestToken method in the EndpointManager class.

/// <summary> /// Specifies the hashing pattern to use when generating a request token. /// </summary> private static readonly string HashingPattern = "0001110111101110001111010101111011010001001110011000110001000110"; /// <summary> /// Generates a request token from the given token and timestamp. /// </summary> /// <param name="token"> /// The auth token. /// </param> /// <param name="timestamp"> /// The timestamp of the request. /// </param> /// <returns> /// A request token, all nice. /// </returns> private string GenerateRequestToken(string token, string timestamp) { Contract.Requires<ArgumentNullException>(token != null); Contract.Requires<ArgumentNullException>(timestamp != null); // SHA-256 hashing function Func<string, string> hash = data => { var input = CryptographicBuffer.ConvertStringToBinary(data, BinaryStringEncoding.Utf8); var hashedData = HashAlgorithmProvider.OpenAlgorithm("SHA256").HashData(input); return CryptographicBuffer.EncodeToHexString(hashedData); }; // Generate hashes. var s1 = hash(SecretToken + token); var s2 = hash(timestamp + SecretToken); // Create a token based on the generated hashes and the hashing pattern. var output = new StringBuilder(); for (var i = 0; i < HashingPattern.Length; i++) { output.Append(HashingPattern[i] == '0' ? s1[i] : s2[i]); } return output.ToString(); }

Snapchat expects the timestamp to be in the encoding used by JavaScript instead of the standard (and more reasonable) Unix timestamp. As far as I know, there is no built-in way to convert it to a JavaScript-based timestamp, so we will need to write an extension method for DateTime objects.

/// <summary> /// Provides utility methods used to convert between various timestamp formats. /// </summary> public static class Timestamps { /// <summary> /// Represents the Unix epoch. /// </summary> public static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); /// <summary> /// Converts this <see cref="DateTime"/> object into a JScript-based timestamp (milliseconds /// since the epoch). /// </summary> /// <returns> /// A 64-bit integer representing the number of milliseconds elapsed since the epoch. /// </returns> public static long ToJScriptTime(this DateTime value) { return Convert.ToInt64((value - Epoch).TotalMilliseconds); } /// <summary> /// Converts a JScript-based timestamp (milliseconds since the epoch) into a /// <see cref="DateTime"/> object. /// </summary> /// <returns> /// A <see cref="DateTime"/> object representing the timestamp. /// </returns> public static DateTime FromJScriptTime(long jscriptTime) { return Epoch.AddMilliseconds(jscriptTime); } }

Finally, we can now generate valid request tokens like so:

var timestamp = DateTime.UtcNow.ToJScriptTime().ToString(); var requestToken = GenerateRequestToken(token, timestamp);

Sending POST and GET Requests

A vast majority of API calls are sent using POST but certain media-related stuff such as downloading thumbnails use GET.

POST

All POST requests require a set of arguments to be passed along. Let’s create a class that represents the function parameters and implement a method to encode it into a URI query string.

/// <summary> /// Represents a key-value collection of API parameters. /// /// This class cannot be inherited. /// </summary> internal sealed class RequestParameters : Dictionary<string, string> { /// <summary> /// Encodes the API parameters into a string. /// </summary> /// <returns>The encoded parameters.</returns> public string Encode() { return String.Join("&", this.Select(o => String.Format("{0}={1}", o.Key, Uri.EscapeDataString(o.Value)))); } };

Firstly, we will need a method in our EndpointManager which can send a POST request and accept a HttpResponseMessage . In this method, we need to create a set of arguments (which includes the request token and timestamp) to pass along with the request. Then, we need to create a HttpClient and define the proper headers before sending a POST request.

/// <summary> /// Sends a POST request to an API function using a <paramref name="token"/>. /// </summary> /// <param name="function"> /// The name of the function. /// </param> /// <param name="args"> /// A set of parameters to pass along with the request. /// </param> /// <param name="token"> /// The auth token. /// </param> /// <param name="headerValues"> /// A dictionary containing additional headers to include in the POST request. /// </param> /// <returns> /// A <see cref="HttpResponseMessage"/> received from the server. /// </returns> public async Task<HttpResponseMessage> PostAsync( string function, RequestParameters args, string token, Dictionary<string, string> headerValues) { Contract.Requires<ArgumentNullException>(token != null); Contract.Requires<ArgumentNullException>(function != null); // Append the timestamp and request token to the arguments. args = args ?? new RequestParameters(); args["timestamp"] = DateTime.UtcNow.ToJScriptTime().ToString(); args["req_token"] = GenerateRequestToken(token, args["timestamp"]); // Create the HTTP client. var client = new HttpClient(); client.DefaultRequestHeaders.TryAppendWithoutValidation("User-Agent", UserAgent); client.DefaultRequestHeaders.TryAppendWithoutValidation("Accept", "*/*"); client.DefaultRequestHeaders.TryAppendWithoutValidation("Accept-Encoding", "gzip,deflate"); if (headerValues != null) { foreach (var header in headerValues) client.DefaultRequestHeaders.Add(header.Key, header.Value); } // Send a POST request to the function, and return the response. var endpoint = new Uri(BaseUri, function); var postBody = new HttpStringContent(args.Encode(), UnicodeEncoding.Utf8, "application/x-www-form-urlencoded"); return await client.PostAsync(endpoint, postBody); }

GET

Results from GET requests are typically encrypted, so you do not need to pass a request token or timestamp. The GetAsync method is similar to the PostAsync method above but without parameters.

/// <summary> /// Sends a GET request to an API function. /// </summary> /// <param name="function"> /// The name of the function. /// </param> /// <param name="headerValues"> /// A dictionary containing additional headers to include in the POST request. /// </param> /// <returns> /// A <see cref="HttpResponseMessage"/> received from the server. /// </returns> public async Task<HttpResponseMessage> GetAsync(string function, Dictionary<string, string> headerValues) { Contract.Requires<ArgumentNullException>(function != null); // Create the HTTP client. var client = new HttpClient(); client.DefaultRequestHeaders.TryAppendWithoutValidation("User-Agent", UserAgent); client.DefaultRequestHeaders.TryAppendWithoutValidation("Accept", "*/*"); client.DefaultRequestHeaders.TryAppendWithoutValidation("Accept-Encoding", "gzip,deflate"); if (headerValues != null) { foreach (var header in headerValues) client.DefaultRequestHeaders.Add(header.Key, header.Value); } // Send a GET request to the endpoint, and return the response. var endpoint = new Uri(BaseUri, function); return await client.GetAsync(endpoint); }

Conclusion

At this point, we can now communicate with the Snapchat API. The next post in this series will cover processing the response especially decompressing gzipped data and deserializing JSON. Stay tuned!

You can check out the code discussed in this post on GitHub.