diff --git a/libs/libblade/libblade.vcxproj b/libs/libblade/libblade.vcxproj
index 5393765671..40baad7dda 100644
--- a/libs/libblade/libblade.vcxproj
+++ b/libs/libblade/libblade.vcxproj
@@ -195,6 +195,7 @@
     <ClCompile Include="src\blade_stack.c" />
     <ClCompile Include="src\blade_transport.c" />
     <ClCompile Include="src\blade_tuple.c" />
+    <ClCompile Include="src\blade_web.c" />
     <ClCompile Include="src\unqlite.c" />
   </ItemGroup>
   <ItemGroup>
@@ -219,6 +220,7 @@
     <ClInclude Include="src\include\blade_transport.h" />
     <ClInclude Include="src\include\blade_tuple.h" />
     <ClInclude Include="src\include\blade_types.h" />
+    <ClInclude Include="src\include\blade_web.h" />
     <ClInclude Include="src\include\unqlite.h" />
   </ItemGroup>
   <ItemGroup>
diff --git a/libs/libblade/libblade.vcxproj.filters b/libs/libblade/libblade.vcxproj.filters
index 95d31a3192..2c7dbb8b54 100644
--- a/libs/libblade/libblade.vcxproj.filters
+++ b/libs/libblade/libblade.vcxproj.filters
@@ -78,6 +78,9 @@
     <ClCompile Include="src\blade_restmgr.c">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="src\blade_web.c">
+      <Filter>Source Files</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="src\include\unqlite.h">
@@ -146,5 +149,8 @@
     <ClInclude Include="src\include\blade_restmgr.h">
       <Filter>Header Files</Filter>
     </ClInclude>
+    <ClInclude Include="src\include\blade_web.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
   </ItemGroup>
 </Project>
\ No newline at end of file
diff --git a/libs/libblade/src/blade_restmgr.c b/libs/libblade/src/blade_restmgr.c
index 34964f43bc..2c2d43a2ef 100644
--- a/libs/libblade/src/blade_restmgr.c
+++ b/libs/libblade/src/blade_restmgr.c
@@ -61,6 +61,7 @@ typedef struct blade_restmgr_service_s {
 	blade_restmgr_service_callback_t callback;
 } blade_restmgr_service_t;
 
+
 static void blade_restmgr_service_cleanup(void *ptr, void *arg, ks_pool_cleanup_action_t action, ks_pool_cleanup_type_t type)
 {
 	blade_restmgr_service_t *brestmgrs = (blade_restmgr_service_t *)ptr;
diff --git a/libs/libblade/src/blade_web.c b/libs/libblade/src/blade_web.c
new file mode 100644
index 0000000000..1bf98dbf70
--- /dev/null
+++ b/libs/libblade/src/blade_web.c
@@ -0,0 +1,674 @@
+/*
+ * Copyright (c) 2017, Shane Bryldt
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of the original author; nor the names of any contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "blade.h"
+
+struct blade_webrequest_s {
+	const char *action;
+	const char *path;
+
+	ks_hash_t *query;
+	ks_hash_t *headers;
+
+	ks_sb_t *content;
+};
+
+struct blade_webresponse_s {
+	const char *status_code;
+	const char *status_message;
+
+	ks_hash_t *headers;
+
+	ks_sb_t *content;
+};
+
+static void blade_webrequest_cleanup(void *ptr, void *arg, ks_pool_cleanup_action_t action, ks_pool_cleanup_type_t type)
+{
+	blade_webrequest_t *bwreq = (blade_webrequest_t *)ptr;
+
+	ks_assert(bwreq);
+
+	switch (action) {
+	case KS_MPCL_ANNOUNCE:
+		break;
+	case KS_MPCL_TEARDOWN:
+		break;
+	case KS_MPCL_DESTROY:
+		break;
+	}
+}
+
+static void blade_webresponse_cleanup(void *ptr, void *arg, ks_pool_cleanup_action_t action, ks_pool_cleanup_type_t type)
+{
+	blade_webresponse_t *bwres = (blade_webresponse_t *)ptr;
+
+	ks_assert(bwres);
+
+	switch (action) {
+	case KS_MPCL_ANNOUNCE:
+		break;
+	case KS_MPCL_TEARDOWN:
+		break;
+	case KS_MPCL_DESTROY:
+		break;
+	}
+}
+
+KS_DECLARE(ks_status_t) blade_webrequest_create(blade_webrequest_t **bwreqP, const char *action, const char *path)
+{
+	ks_pool_t *pool = NULL;
+	blade_webrequest_t *bwreq = NULL;
+
+	ks_assert(bwreqP);
+	ks_assert(action);
+	ks_assert(path);
+
+	ks_pool_open(&pool);
+	ks_assert(pool);
+
+	bwreq = ks_pool_alloc(pool, sizeof(blade_webrequest_t));
+
+	bwreq->action = ks_pstrdup(pool, action);
+	bwreq->path = ks_pstrdup(pool, path);
+
+	ks_hash_create(&bwreq->query, KS_HASH_MODE_CASE_INSENSITIVE, KS_HASH_FLAG_NOLOCK | KS_HASH_FLAG_DUP_CHECK | KS_HASH_FLAG_FREE_KEY | KS_HASH_FLAG_FREE_VALUE, pool);
+	ks_assert(bwreq->query);
+
+	ks_hash_create(&bwreq->headers, KS_HASH_MODE_CASE_INSENSITIVE, KS_HASH_FLAG_NOLOCK | KS_HASH_FLAG_DUP_CHECK | KS_HASH_FLAG_FREE_KEY | KS_HASH_FLAG_FREE_VALUE, pool);
+	ks_assert(bwreq->headers);
+
+	ks_sb_create(&bwreq->content, pool, 0);
+	ks_assert(bwreq->content);
+
+	ks_pool_set_cleanup(bwreq, NULL, blade_webrequest_cleanup);
+
+	*bwreqP = bwreq;
+
+	blade_webrequest_header_add(bwreq, "Content-Type", "application/x-www-form-urlencoded");
+
+	return KS_STATUS_SUCCESS;
+}
+
+KS_DECLARE(ks_status_t) blade_webrequest_load(blade_webrequest_t **bwreqP, struct mg_connection *conn)
+{
+	ks_status_t ret = KS_STATUS_SUCCESS;
+	ks_pool_t *pool = NULL;
+	blade_webrequest_t *bwreq = NULL;
+	struct mg_request_info *info = NULL;
+	char buf[1024];
+	int bytes = 0;
+
+	ks_assert(bwreqP);
+	ks_assert(conn);
+
+	info = mg_get_request_info(conn);
+
+	ks_pool_open(&pool);
+	ks_assert(pool);
+
+	bwreq = ks_pool_alloc(pool, sizeof(blade_webrequest_t));
+
+	bwreq->action = ks_pstrdup(pool, info->request_method);
+	bwreq->path = ks_pstrdup(pool, info->request_uri);
+
+	ks_hash_create(&bwreq->query, KS_HASH_MODE_CASE_INSENSITIVE, KS_HASH_FLAG_NOLOCK | KS_HASH_FLAG_DUP_CHECK | KS_HASH_FLAG_FREE_KEY | KS_HASH_FLAG_FREE_VALUE, pool);
+	ks_assert(bwreq->query);
+
+	ks_hash_create(&bwreq->headers, KS_HASH_MODE_CASE_INSENSITIVE, KS_HASH_FLAG_NOLOCK | KS_HASH_FLAG_DUP_CHECK | KS_HASH_FLAG_FREE_KEY | KS_HASH_FLAG_FREE_VALUE, pool);
+	ks_assert(bwreq->headers);
+
+	ks_sb_create(&bwreq->content, pool, 0);
+	ks_assert(bwreq->content);
+
+	ks_pool_set_cleanup(bwreq, NULL, blade_webrequest_cleanup);
+
+	if (info->query_string && info->query_string[0]) {
+		char *query = ks_pstrdup(pool, info->query_string);
+		char *start = query;
+		char *end = NULL;
+
+		do {
+			char *key = start;
+			char *value = NULL;
+
+			end = strchr(start, '&');
+			if (end) *end = '\0';
+
+			value = strchr(start, '=');
+			if (value) {
+				*value = '\0';
+				value++;
+
+				if (*key && *value) {
+					ks_hash_insert(bwreq->query, (void *)ks_pstrdup(pool, key), (void *)ks_pstrdup(pool, value));
+				}
+			}
+
+			if (end) start = ++end;
+			else start = NULL;
+		} while (start);
+
+		ks_pool_free(&query);
+	}
+
+	for (int index = 0; index < info->num_headers; ++index) {
+		struct mg_header *header = &info->http_headers[index];
+		ks_hash_insert(bwreq->headers, (void *)ks_pstrdup(pool, header->name), (void *)ks_pstrdup(pool, header->value));
+	}
+
+	while ((bytes = mg_read(conn, buf, sizeof(buf))) > 0) ks_sb_append_ex(bwreq->content, buf, bytes);
+	if (bytes < 0) {
+		blade_webrequest_destroy(&bwreq);
+		ret = KS_STATUS_FAIL;
+	}
+	else *bwreqP = bwreq;
+
+	return ret;
+}
+
+KS_DECLARE(ks_status_t) blade_webrequest_destroy(blade_webrequest_t **bwreqP)
+{
+	blade_webrequest_t *bwreq = NULL;
+	ks_pool_t *pool;
+
+	ks_assert(bwreqP);
+	ks_assert(*bwreqP);
+
+	bwreq = *bwreqP;
+	*bwreqP = NULL;
+
+	pool = ks_pool_get(bwreq);
+
+	ks_pool_close(&pool);
+
+	return KS_STATUS_SUCCESS;
+}
+
+KS_DECLARE(const char *) blade_webrequest_action_get(blade_webrequest_t *bwreq)
+{
+	ks_assert(bwreq);
+	return bwreq->action;
+}
+
+KS_DECLARE(const char *) blade_webrequest_path_get(blade_webrequest_t *bwreq)
+{
+	ks_assert(bwreq);
+	return bwreq->path;
+}
+
+KS_DECLARE(ks_status_t) blade_webrequest_query_add(blade_webrequest_t *bwreq, const char *name, const char *value)
+{
+	ks_assert(bwreq);
+	ks_assert(name);
+	ks_assert(value);
+
+	ks_hash_insert(bwreq->query, (void *)ks_pstrdup(ks_pool_get(bwreq), name), (void *)ks_pstrdup(ks_pool_get(bwreq), value));
+
+	return KS_STATUS_SUCCESS;
+}
+
+KS_DECLARE(const char *) blade_webrequest_query_get(blade_webrequest_t *bwreq, const char *name)
+{
+	ks_assert(bwreq);
+	ks_assert(name);
+
+	return (const char *)ks_hash_search(bwreq->query, (void *)name, KS_UNLOCKED);
+}
+
+KS_DECLARE(ks_status_t) blade_webrequest_header_add(blade_webrequest_t *bwreq, const char *header, const char *value)
+{
+	ks_assert(bwreq);
+	ks_assert(header);
+	ks_assert(value);
+
+	ks_hash_insert(bwreq->headers, (void *)ks_pstrdup(ks_pool_get(bwreq), header), (void *)ks_pstrdup(ks_pool_get(bwreq), value));
+	
+	return KS_STATUS_SUCCESS;
+}
+
+KS_DECLARE(ks_status_t) blade_webrequest_header_printf(blade_webrequest_t *bwreq, const char *header, const char *fmt, ...)
+{
+	va_list ap;
+	char *result = NULL;
+
+	ks_assert(bwreq);
+	ks_assert(header);
+	ks_assert(fmt);
+
+	va_start(ap, fmt);
+	result = ks_vpprintf(ks_pool_get(bwreq), fmt, ap);
+	va_end(ap);
+
+	ks_hash_insert(bwreq->headers, (void *)ks_pstrdup(ks_pool_get(bwreq), header), (void *)result);
+
+	return KS_STATUS_SUCCESS;
+}
+
+KS_DECLARE(const char *) blade_webrequest_header_get(blade_webrequest_t *bwreq, const char *header)
+{
+	ks_assert(bwreq);
+	ks_assert(header);
+
+	return (const char *)ks_hash_search(bwreq->headers, (void *)header, KS_UNLOCKED);
+}
+
+KS_DECLARE(ks_status_t) blade_webrequest_content_json_append(blade_webrequest_t *bwreq, cJSON *json)
+{
+	ks_assert(bwreq);
+	ks_assert(json);
+
+	blade_webrequest_header_add(bwreq, "Content-Type", "application/json");
+
+	ks_sb_json(bwreq->content, json);
+
+	return KS_STATUS_SUCCESS;
+}
+
+KS_DECLARE(ks_status_t) blade_webrequest_content_string_append(blade_webrequest_t *bwreq, const char *str)
+{
+	ks_assert(bwreq);
+	ks_assert(str);
+
+	ks_sb_append(bwreq->content, str);
+
+	return KS_STATUS_SUCCESS;
+}
+
+KS_DECLARE(ks_status_t) blade_webrequest_send(blade_webrequest_t *bwreq, ks_bool_t secure, const char *host, ks_port_t port, blade_webresponse_t **bwresP)
+{
+	ks_status_t ret = KS_STATUS_SUCCESS;
+	char buf[1024];
+	struct mg_connection *conn = NULL;
+	const char *path = NULL;
+	ks_sb_t *pathAndQuery = NULL;
+
+	ks_assert(bwreq);
+	ks_assert(host);
+	ks_assert(bwresP);
+
+	if (port == 0) port = secure ? 443 : 80;
+
+	conn = mg_connect_client(host, port, secure, buf, sizeof(buf));
+	if (!conn) {
+		ret = KS_STATUS_FAIL;
+		goto done;
+	}
+
+	path = bwreq->path;
+	if (ks_hash_count(bwreq->query) > 0) {
+		ks_bool_t firstQuery = KS_TRUE;
+
+		ks_sb_create(&pathAndQuery, NULL, 0);
+		ks_sb_append(pathAndQuery, bwreq->path);
+		for (ks_hash_iterator_t *it = ks_hash_first(bwreq->query, KS_UNLOCKED); it; it = ks_hash_next(&it)) {
+			const char *key;
+			const char *value;
+
+			ks_hash_this(it, (const void **)&key, NULL, (void **)&value);
+
+			// @todo make sure key and value are URL encoded
+			mg_url_encode(key, buf, sizeof(buf));
+			ks_sb_printf(pathAndQuery, "%c%s=", firstQuery ? '?' : '&', buf);
+			
+			mg_url_encode(value, buf, sizeof(buf));
+			ks_sb_append(pathAndQuery, buf);
+
+			firstQuery = KS_FALSE;
+		}
+
+		path = ks_sb_cstr(pathAndQuery);
+	}
+
+	mg_printf(conn,
+		"%s %s HTTP/1.1\r\n"
+		"Host: %s\r\n"
+		"Content-Length: %lu\r\n",
+		bwreq->action,
+		path,
+		host,
+		ks_sb_length(bwreq->content));
+
+	if (pathAndQuery) ks_sb_destroy(&pathAndQuery);
+
+	for (ks_hash_iterator_t *it = ks_hash_first(bwreq->headers, KS_UNLOCKED); it; it = ks_hash_next(&it)) {
+		const char *key;
+		const char *value;
+
+		ks_hash_this(it, (const void **)&key, NULL, (void **)&value);
+		mg_printf(conn, "%s: %s\r\n", key, value);
+	}
+
+	mg_write(conn, "\r\n", 2);
+
+	mg_write(conn, ks_sb_cstr(bwreq->content), ks_sb_length(bwreq->content));
+
+	if (mg_get_response(conn, buf, sizeof(buf), 1000) <= 0) {
+		ret = KS_STATUS_FAIL;
+		goto done;
+	}
+
+	ret = blade_webresponse_load(bwresP, conn);
+
+done:
+	if (conn) mg_close_connection(conn);
+	return ret;
+}
+
+KS_DECLARE(ks_status_t) blade_webrequest_oauth2_token_by_credentials_send(ks_bool_t secure, const char *host, ks_port_t port, const char *path, const char *client_id, const char *client_secret, const char **token)
+{
+	ks_status_t ret = KS_STATUS_SUCCESS;
+	blade_webrequest_t *bwreq = NULL;
+	blade_webresponse_t *bwres = NULL;
+	cJSON *json = NULL;
+	char *auth = NULL;
+	char encoded[1024];
+	ks_pool_t *pool = NULL;
+	char *tok = NULL;
+
+	ks_assert(host);
+	ks_assert(path);
+	ks_assert(client_id);
+	ks_assert(client_secret);
+	ks_assert(token);
+
+	blade_webrequest_create(&bwreq, "POST", path);
+
+	auth = ks_psprintf(ks_pool_get(bwreq), "%s:%s", client_id, client_secret);
+	ks_b64_encode((unsigned char *)auth, strlen(auth), (unsigned char *)encoded, sizeof(encoded));
+	ks_pool_free(&auth);
+
+	blade_webrequest_header_printf(bwreq, "Authorization", "Basic %s", encoded);
+
+	json = cJSON_CreateObject();
+	cJSON_AddStringToObject(json, "grant_type", "client_credentials");
+	blade_webrequest_content_json_append(bwreq, json);
+	cJSON_Delete(json);
+
+	if ((ret = blade_webrequest_send(bwreq, secure, host, port, &bwres)) != KS_STATUS_SUCCESS) goto done;
+	
+	if ((ret = blade_webresponse_content_json_get(bwres, &json)) != KS_STATUS_SUCCESS) goto done;
+
+	if ((tok = cJSON_GetObjectCstr(json, "access_token")) == NULL) {
+		ret = KS_STATUS_FAIL;
+		goto done;
+	}
+
+	ks_pool_open(&pool);
+	*token = ks_pstrdup(pool, tok);
+
+done:
+	if (json) cJSON_Delete(json);
+	blade_webrequest_destroy(&bwreq);
+	if (bwres) blade_webresponse_destroy(&bwres);
+
+	return ret;
+}
+
+KS_DECLARE(ks_status_t) blade_webrequest_oauth2_token_by_code_send(ks_bool_t secure, const char *host, ks_port_t port, const char *path, const char *client_id, const char *client_secret, const char *code, const char **token)
+{
+	ks_status_t ret = KS_STATUS_SUCCESS;
+	blade_webrequest_t *bwreq = NULL;
+	blade_webresponse_t *bwres = NULL;
+	cJSON *json = NULL;
+	char *auth = NULL;
+	char encoded[1024];
+	ks_pool_t *pool = NULL;
+	char *tok = NULL;
+
+	ks_assert(host);
+	ks_assert(path);
+	ks_assert(client_id);
+	ks_assert(client_secret);
+	ks_assert(code);
+	ks_assert(token);
+
+	blade_webrequest_create(&bwreq, "POST", path);
+
+	auth = ks_psprintf(ks_pool_get(bwreq), "%s:%s", client_id, client_secret);
+	ks_b64_encode((unsigned char *)auth, strlen(auth), (unsigned char *)encoded, sizeof(encoded));
+	ks_pool_free(&auth);
+
+	blade_webrequest_header_printf(bwreq, "Authorization", "Basic %s", encoded);
+
+	json = cJSON_CreateObject();
+	cJSON_AddStringToObject(json, "grant_type", "authorization_code");
+	cJSON_AddStringToObject(json, "code", code);
+	blade_webrequest_content_json_append(bwreq, json);
+	cJSON_Delete(json);
+
+	if ((ret = blade_webrequest_send(bwreq, secure, host, port, &bwres)) != KS_STATUS_SUCCESS) goto done;
+
+	if ((ret = blade_webresponse_content_json_get(bwres, &json)) != KS_STATUS_SUCCESS) goto done;
+
+	if ((tok = cJSON_GetObjectCstr(json, "access_token")) == NULL) {
+		ret = KS_STATUS_FAIL;
+		goto done;
+	}
+
+	ks_pool_open(&pool);
+	*token = ks_pstrdup(pool, tok);
+
+done:
+	if (json) cJSON_Delete(json);
+	blade_webrequest_destroy(&bwreq);
+	if (bwres) blade_webresponse_destroy(&bwres);
+
+	return ret;
+}
+
+
+KS_DECLARE(ks_status_t) blade_webresponse_create(blade_webresponse_t **bwresP, const char *status)
+{
+	ks_pool_t *pool = NULL;
+	blade_webresponse_t *bwres = NULL;
+
+	ks_assert(bwresP);
+	ks_assert(status);
+
+	ks_pool_open(&pool);
+	ks_assert(pool);
+
+	bwres = ks_pool_alloc(pool, sizeof(blade_webresponse_t));
+
+	bwres->status_code = ks_pstrdup(pool, status);
+	bwres->status_message = ks_pstrdup(pool, mg_get_response_code_text(NULL, atoi(status)));
+	
+	ks_hash_create(&bwres->headers, KS_HASH_MODE_CASE_INSENSITIVE, KS_HASH_FLAG_NOLOCK | KS_HASH_FLAG_DUP_CHECK | KS_HASH_FLAG_FREE_KEY | KS_HASH_FLAG_FREE_VALUE, pool);
+	ks_assert(bwres->headers);
+
+	ks_sb_create(&bwres->content, pool, 0);
+	ks_assert(bwres->content);
+
+	ks_pool_set_cleanup(bwres, NULL, blade_webresponse_cleanup);
+
+	*bwresP = bwres;
+
+	blade_webresponse_header_add(bwres, "Content-Type", "application/x-www-form-urlencoded");
+
+	return KS_STATUS_SUCCESS;
+}
+
+KS_DECLARE(ks_status_t) blade_webresponse_load(blade_webresponse_t **bwresP, struct mg_connection *conn)
+{
+	ks_status_t ret = KS_STATUS_SUCCESS;
+	ks_pool_t *pool = NULL;
+	blade_webresponse_t *bwres = NULL;
+	struct mg_request_info *info = NULL;
+	char buf[1024];
+	int bytes = 0;
+
+	ks_assert(bwresP);
+	ks_assert(conn);
+
+	info = mg_get_request_info(conn);
+
+	ks_pool_open(&pool);
+	ks_assert(pool);
+
+	bwres = ks_pool_alloc(pool, sizeof(blade_webrequest_t));
+
+	bwres->status_code = ks_pstrdup(pool, info->request_uri);
+	bwres->status_message = ks_pstrdup(pool, info->http_version);
+
+	ks_hash_create(&bwres->headers, KS_HASH_MODE_CASE_INSENSITIVE, KS_HASH_FLAG_NOLOCK | KS_HASH_FLAG_DUP_CHECK | KS_HASH_FLAG_FREE_KEY | KS_HASH_FLAG_FREE_VALUE, pool);
+	ks_assert(bwres->headers);
+
+	ks_sb_create(&bwres->content, pool, 0);
+	ks_assert(bwres->content);
+
+	ks_pool_set_cleanup(bwres, NULL, blade_webresponse_cleanup);
+
+	for (int index = 0; index < info->num_headers; ++index) {
+		struct mg_header *header = &info->http_headers[index];
+		ks_hash_insert(bwres->headers, (void *)ks_pstrdup(pool, header->name), (void *)ks_pstrdup(pool, header->value));
+	}
+
+	while ((bytes = mg_read(conn, buf, sizeof(buf))) > 0) ks_sb_append_ex(bwres->content, buf, bytes);
+	if (bytes < 0) {
+		blade_webresponse_destroy(&bwres);
+		ret = KS_STATUS_FAIL;
+	}
+	else *bwresP = bwres;
+
+	return ret;
+}
+
+KS_DECLARE(ks_status_t) blade_webresponse_destroy(blade_webresponse_t **bwresP)
+{
+	blade_webresponse_t *bwres = NULL;
+	ks_pool_t *pool;
+
+	ks_assert(bwresP);
+	ks_assert(*bwresP);
+
+	bwres = *bwresP;
+	*bwresP = NULL;
+
+	pool = ks_pool_get(bwres);
+
+	ks_pool_close(&pool);
+
+	return KS_STATUS_SUCCESS;
+}
+
+KS_DECLARE(ks_status_t) blade_webresponse_header_add(blade_webresponse_t *bwres, const char *header, const char *value)
+{
+	ks_assert(bwres);
+	ks_assert(header);
+	ks_assert(value);
+
+	ks_hash_insert(bwres->headers, (void *)ks_pstrdup(ks_pool_get(bwres), header), (void *)ks_pstrdup(ks_pool_get(bwres), value));
+
+	return KS_STATUS_SUCCESS;
+}
+
+KS_DECLARE(const char *) blade_webresponse_header_get(blade_webresponse_t *bwres, const char *header)
+{
+	ks_assert(bwres);
+	ks_assert(header);
+
+	return (const char *)ks_hash_search(bwres->headers, (void *)header, KS_UNLOCKED);
+}
+
+KS_DECLARE(ks_status_t) blade_webresponse_content_json_append(blade_webresponse_t *bwres, cJSON *json)
+{
+	ks_assert(bwres);
+	ks_assert(json);
+
+	blade_webresponse_header_add(bwres, "Content-Type", "application/json");
+
+	ks_sb_json(bwres->content, json);
+
+	return KS_STATUS_SUCCESS;
+}
+
+KS_DECLARE(ks_status_t) blade_webresponse_content_string_append(blade_webresponse_t *bwres, const char *str)
+{
+	ks_assert(bwres);
+	ks_assert(str);
+
+	ks_sb_append(bwres->content, str);
+
+	return KS_STATUS_SUCCESS;
+}
+
+KS_DECLARE(ks_status_t) blade_webresponse_content_json_get(blade_webresponse_t *bwres, cJSON **json)
+{
+	ks_status_t ret = KS_STATUS_SUCCESS;
+
+	ks_assert(bwres);
+	ks_assert(json);
+
+	if (!(*json = cJSON_Parse(ks_sb_cstr(bwres->content)))) ret = KS_STATUS_FAIL;
+
+	return ret;
+}
+
+KS_DECLARE(ks_status_t) blade_webresponse_send(blade_webresponse_t *bwres, struct mg_connection *conn)
+{
+	ks_assert(bwres);
+	ks_assert(conn);
+
+	mg_printf(conn,
+		"HTTP/1.1 %s %s\r\n"
+		"Content-Length: %lu\r\n"
+		"Connection: close\r\n",
+		bwres->status_code,
+		bwres->status_message,
+		ks_sb_length(bwres->content));
+
+	for (ks_hash_iterator_t *it = ks_hash_first(bwres->headers, KS_UNLOCKED); it; it = ks_hash_next(&it)) {
+		const char *key;
+		const char *value;
+
+		ks_hash_this(it, (const void **)&key, NULL, (void **)&value);
+		mg_printf(conn, "%s: %s\r\n", key, value);
+	}
+
+	mg_write(conn, "\r\n", 2);
+
+	mg_write(conn, ks_sb_cstr(bwres->content), ks_sb_length(bwres->content));
+
+	return KS_STATUS_SUCCESS;
+}
+
+/* For Emacs:
+ * Local Variables:
+ * mode:c
+ * indent-tabs-mode:t
+ * tab-width:4
+ * c-basic-offset:4
+ * End:
+ * For VIM:
+ * vim:set softtabstop=4 shiftwidth=4 tabstop=4 noet:
+ */
diff --git a/libs/libblade/src/include/blade.h b/libs/libblade/src/include/blade.h
index 812e95456c..5e1cb3db82 100644
--- a/libs/libblade/src/include/blade.h
+++ b/libs/libblade/src/include/blade.h
@@ -51,6 +51,7 @@
 #include "blade_channel.h"
 #include "blade_subscription.h"
 #include "blade_tuple.h"
+#include "blade_web.h"
 
 #include "blade_transportmgr.h"
 #include "blade_rpcmgr.h"
diff --git a/libs/libblade/src/include/blade_types.h b/libs/libblade/src/include/blade_types.h
index 7e6f960235..93d61b6f95 100644
--- a/libs/libblade/src/include/blade_types.h
+++ b/libs/libblade/src/include/blade_types.h
@@ -62,6 +62,8 @@ typedef struct blade_connectionmgr_s blade_connectionmgr_t;
 typedef struct blade_sessionmgr_s blade_sessionmgr_t;
 typedef struct blade_session_callback_data_s blade_session_callback_data_t;
 
+typedef struct blade_webrequest_s blade_webrequest_t;
+typedef struct blade_webresponse_s blade_webresponse_t;
 typedef struct blade_restmgr_s blade_restmgr_t;
 
 typedef ks_bool_t (*blade_rpc_request_callback_t)(blade_rpc_request_t *brpcreq, void *data);
diff --git a/libs/libblade/src/include/blade_web.h b/libs/libblade/src/include/blade_web.h
new file mode 100644
index 0000000000..062259f6ee
--- /dev/null
+++ b/libs/libblade/src/include/blade_web.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2017, Shane Bryldt
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 
+ * * Neither the name of the original author; nor the names of any contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ * 
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef _BLADE_WEB_H_
+#define _BLADE_WEB_H_
+#include <blade.h>
+
+KS_BEGIN_EXTERN_C
+KS_DECLARE(ks_status_t) blade_webrequest_create(blade_webrequest_t **bwreqP, const char *action, const char *path);
+KS_DECLARE(ks_status_t) blade_webrequest_load(blade_webrequest_t **bwreqP, struct mg_connection *conn);
+KS_DECLARE(ks_status_t) blade_webrequest_destroy(blade_webrequest_t **bwreqP);
+KS_DECLARE(const char *) blade_webrequest_action_get(blade_webrequest_t *bwreq);
+KS_DECLARE(const char *) blade_webrequest_path_get(blade_webrequest_t *bwreq);
+KS_DECLARE(ks_status_t) blade_webrequest_query_add(blade_webrequest_t *bwreq, const char *name, const char *value);
+KS_DECLARE(const char *) blade_webrequest_query_get(blade_webrequest_t *bwreq, const char *name);
+KS_DECLARE(ks_status_t) blade_webrequest_header_add(blade_webrequest_t *bwreq, const char *header, const char *value);
+KS_DECLARE(ks_status_t) blade_webrequest_header_printf(blade_webrequest_t *bwreq, const char *header, const char *fmt, ...);
+KS_DECLARE(const char *) blade_webrequest_header_get(blade_webrequest_t *bwreq, const char *header);
+KS_DECLARE(ks_status_t) blade_webrequest_content_json_append(blade_webrequest_t *bwreq, cJSON *json);
+KS_DECLARE(ks_status_t) blade_webrequest_content_string_append(blade_webrequest_t *bwreq, const char *str);
+KS_DECLARE(ks_status_t) blade_webrequest_send(blade_webrequest_t *bwreq, ks_bool_t secure, const char *host, ks_port_t port, blade_webresponse_t **bwresP);
+
+KS_DECLARE(ks_status_t) blade_webrequest_oauth2_token_by_credentials_send(ks_bool_t secure, const char *host, ks_port_t port, const char *path, const char *client_id, const char *client_secret, const char **token);
+KS_DECLARE(ks_status_t) blade_webrequest_oauth2_token_by_code_send(ks_bool_t secure, const char *host, ks_port_t port, const char *path, const char *client_id, const char *client_secret, const char *code, const char **token);
+
+
+KS_DECLARE(ks_status_t) blade_webresponse_create(blade_webresponse_t **bwresP, const char *status);
+KS_DECLARE(ks_status_t) blade_webresponse_load(blade_webresponse_t **bwresP, struct mg_connection *conn);
+KS_DECLARE(ks_status_t) blade_webresponse_destroy(blade_webresponse_t **bwresP);
+KS_DECLARE(ks_status_t) blade_webresponse_header_add(blade_webresponse_t *bwres, const char *header, const char *value);
+KS_DECLARE(const char *) blade_webresponse_header_get(blade_webresponse_t *bwres, const char *header);
+KS_DECLARE(ks_status_t) blade_webresponse_content_json_append(blade_webresponse_t *bwres, cJSON *json);
+KS_DECLARE(ks_status_t) blade_webresponse_content_string_append(blade_webresponse_t *bwres, const char *str);
+KS_DECLARE(ks_status_t) blade_webresponse_content_json_get(blade_webresponse_t *bwres, cJSON **json);
+KS_DECLARE(ks_status_t) blade_webresponse_send(blade_webresponse_t *bwres, struct mg_connection *conn);
+
+KS_END_EXTERN_C
+
+#endif
+
+/* For Emacs:
+ * Local Variables:
+ * mode:c
+ * indent-tabs-mode:t
+ * tab-width:4
+ * c-basic-offset:4
+ * End:
+ * For VIM:
+ * vim:set softtabstop=4 shiftwidth=4 tabstop=4 noet:
+ */
diff --git a/libs/libblade/switchblade/switchblade.c b/libs/libblade/switchblade/switchblade.c
index 5c4b89c70e..9df78d46b0 100644
--- a/libs/libblade/switchblade/switchblade.c
+++ b/libs/libblade/switchblade/switchblade.c
@@ -139,37 +139,31 @@ void command_quit(blade_handle_t *bh, char *args)
 
 int rest_service_test(blade_restmgr_t *brestmgr, struct mg_connection *conn, const char **captures)
 {
-	const struct mg_request_info *info = NULL;
+	blade_webrequest_t *request = NULL;
+	blade_webresponse_t *response = NULL;
 	cJSON *json = NULL;
 	cJSON *json_captures = NULL;
-	ks_sb_t *sb = NULL;
+	const char *token = NULL;
 
+	blade_webrequest_load(&request, conn);
+
+	// make a json object to send back
 	json = cJSON_CreateObject();
-
-	ks_sb_create(&sb, NULL, 0);
-
-	info = mg_get_request_info(conn);
-
-	cJSON_AddStringToObject(json, "method", info->request_method);
-
+	cJSON_AddStringToObject(json, "method", blade_webrequest_action_get(request));
 	cJSON_AddItemToObject(json, "captures", (json_captures = cJSON_CreateArray()));
-
 	for (int i = 0; captures[i]; ++i) cJSON_AddItemToArray(json_captures, cJSON_CreateString(captures[i]));
 
-	ks_sb_json(sb, json);
-
-	mg_printf(conn,
-		"HTTP/1.1 200 OK\r\n"
-		"Content-Length: %lu\r\n"
-		"Content-Type: text/plain\r\n"
-		"Connection: close\r\n\r\n",
-		ks_sb_length(sb));
-	
-	mg_write(conn, ks_sb_cstr(sb), ks_sb_length(sb));
-
-	ks_sb_destroy(&sb);
+	blade_webresponse_create(&response, "200");
+	blade_webresponse_content_json_append(response, json);
+	blade_webresponse_send(response, conn);
+	blade_webresponse_destroy(&response);
 
 	cJSON_Delete(json);
+	blade_webrequest_destroy(&request);
+
+	//blade_webrequest_oauth2_token_by_credentials_send(KS_FALSE, "192.168.1.99", 80, "/oauth2/token.php", "testclient", "testpass", &token);
+	//
+	//ks_pool_free(&token);
 
 	return 200;
 }
diff --git a/libs/libks/src/ks_sb.c b/libs/libks/src/ks_sb.c
index e2ebb7c8e5..4f9beacee5 100644
--- a/libs/libks/src/ks_sb.c
+++ b/libs/libks/src/ks_sb.c
@@ -88,6 +88,7 @@ KS_DECLARE(ks_status_t) ks_sb_destroy(ks_sb_t **sbP)
 	ks_assert(*sbP);
 
 	sb = *sbP;
+	*sbP = NULL;
 
 	if (sb->pool_owner) {
 		ks_pool_t *pool = ks_pool_get(sb);