diff --git a/rbtools/api/client.py b/rbtools/api/client.py
new file mode 100644
index 0000000000000000000000000000000000000000..12c93712588ef8d6a65261bc90d6c58966f6efac
--- /dev/null
+++ b/rbtools/api/client.py
@@ -0,0 +1,15 @@
+from rbtools.api.transport.sync import SyncTransport
+
+
+class RBClient(object):
+    """Entry point for accessing RB resources through the web API.
+
+    By default the synchronous transport will be used. To use a
+    different transport, provide the transport class in the
+    'transport_cls' parameter.
+    """
+    def __init__(self, url, transport_cls=SyncTransport, *args, **kwargs):
+        self._transport = transport_cls(url, *args, **kwargs)
+
+    def get_root(self, *args, **kwargs):
+        return self._transport.get_root(*args, **kwargs)
diff --git a/rbtools/api/decode.py b/rbtools/api/decode.py
new file mode 100644
index 0000000000000000000000000000000000000000..d17ea5e5cf494363c4af729867d1c7d961f6727d
--- /dev/null
+++ b/rbtools/api/decode.py
@@ -0,0 +1,57 @@
+import json
+
+from rbtools.api.utils import parse_mimetype
+
+
+DECODER_MAP = {}
+
+
+def DefaultDecoder(payload):
+    """Default decoder for API payloads.
+
+    The default decoder is used when a decoder is not found in the
+    DECODER_MAP. This is a last resort which should only be used when
+    something has gone wrong.
+    """
+    return {
+        'resource': {
+            'data': payload,
+        },
+    }
+
+DEFAULT_DECODER = DefaultDecoder
+
+
+def JsonDecoder(payload):
+    return json.loads(payload)
+
+DECODER_MAP['application/json'] = JsonDecoder
+
+
+def PlainTextDecoder(payload):
+    return {
+        'text': payload,
+    }
+
+DECODER_MAP['text/plain'] = PlainTextDecoder
+
+
+def decode_response(payload, mime_type):
+    """Decode a Web API response.
+
+    The body of a Web API response will be decoded into a dictionary,
+    according to the provided mime_type.
+    """
+    if not payload:
+        return {}
+
+    mime = parse_mimetype(mime_type)
+
+    format = '%s/%s' % (mime['main_type'], mime['format'])
+
+    if format in DECODER_MAP:
+        decoder = DECODER_MAP[format]
+    else:
+        decoder = DEFAULT_DECODER
+
+    return decoder(payload)
diff --git a/rbtools/api/errors.py b/rbtools/api/errors.py
index 1a87d9155370355df130497321ff73966e567727..3e5639e4328215d147de4ea7e2ace00c4391e4b9 100644
--- a/rbtools/api/errors.py
+++ b/rbtools/api/errors.py
@@ -15,3 +15,57 @@ class APIError(Exception):
             return '%s (%s)' % (self.rsp['err']['msg'], code_str)
         else:
             return code_str
+
+
+class ResourceError(Exception):
+    def __init__(self, msg, *args, **kwargs):
+        Exception.__init__(self, *args, **kwargs)
+        self.msg = msg
+
+    def __str__(self):
+        return self.msg
+
+
+class ServerInterfaceError(Exception):
+    def __init__(self, msg, *args, **kwargs):
+        Exception.__init__(self, *args, **kwargs)
+        self.msg = msg
+
+    def __str__(self):
+        return self.msg
+
+
+class ChildResourceUncreatableError(ResourceError):
+    pass
+
+
+class InvalidChildResourceUrlError(ResourceError):
+    pass
+
+
+class InvalidResourceTypeError(ResourceError):
+    pass
+
+
+class InvalidKeyError(ResourceError):
+    pass
+
+
+class RequestFailedError(ResourceError):
+    pass
+
+
+class UnloadedResourceError(ResourceError):
+    pass
+
+
+class UnknownResourceTypeError(ResourceError):
+    pass
+
+
+class InvalidRequestMethodError(ServerInterfaceError):
+    pass
+
+
+class AuthenticationFailedError(ServerInterfaceError):
+    pass
diff --git a/rbtools/api/factory.py b/rbtools/api/factory.py
new file mode 100644
index 0000000000000000000000000000000000000000..db840d3a74cec5674bfea0b40f7fa0b307603f5d
--- /dev/null
+++ b/rbtools/api/factory.py
@@ -0,0 +1,47 @@
+from rbtools.api.resource import CountResource, \
+                                 ResourceItem, \
+                                 ResourceList, \
+                                 RESOURCE_MAP
+from rbtools.api.utils import rem_mime_format
+
+
+SPECIAL_KEYS = set(('links', 'total_results', 'stat', 'count'))
+
+
+def create_resource(payload, url, mime_type=None, item_mime_type=None,
+                    guess_token=True):
+    """Construct and return a resource object.
+
+    The mime type will be used to find a resource specific base class.
+    Alternatively, if no resource specific base class exists, one of
+    the generic base classes, Resource or ResourceList, will be used.
+
+    If an item mime type is provided, it will be used by list
+    resources to construct item resources from the list.
+
+    If 'guess_token' is True, we will try and guess what key the
+    resources body lives under. If False, we assume that the resource
+    body is the body of the payload itself. This is important for
+    constructing Item resources from a resource list.
+    """
+
+    # Determine the key for the resources data.
+    token = None
+
+    if guess_token:
+        other_keys = set(payload.keys()).difference(SPECIAL_KEYS)
+        if len(other_keys) == 1:
+            token = other_keys.pop()
+
+    # Select the base class for the resource.
+    if 'count' in payload:
+        resource_class = CountResource
+    elif mime_type and rem_mime_format(mime_type) in RESOURCE_MAP:
+        resource_class = RESOURCE_MAP[rem_mime_format(mime_type)]
+    elif token and isinstance(payload[token], list):
+        resource_class = ResourceList
+    else:
+        resource_class = ResourceItem
+
+    return resource_class(payload, url, token=token,
+                          item_mime_type=item_mime_type)
diff --git a/rbtools/api/request.py b/rbtools/api/request.py
new file mode 100644
index 0000000000000000000000000000000000000000..fe928bf4607eb73f6354f1e73b8dda94c11cfe4f
--- /dev/null
+++ b/rbtools/api/request.py
@@ -0,0 +1,317 @@
+import base64
+import cookielib
+import mimetools
+import mimetypes
+from StringIO import StringIO
+import urllib
+import urllib2
+from urlparse import urlparse, urlunparse
+
+from rbtools import get_package_version
+from rbtools.api.errors import APIError, ServerInterfaceError
+
+try:
+    # Specifically import json_loads, to work around some issues with
+    # installations containing incompatible modules named "json".
+    from json import loads as json_loads
+except ImportError:
+    from simplejson import loads as json_loads
+
+try:
+    # In python 2.6, parse_qsl was deprectated in cgi, and
+    # moved to urlparse.
+    from urlparse import parse_qsl
+except ImportError:
+    from cgi import parse_qsl
+
+
+class HttpRequest(object):
+    """High-level HTTP-request object."""
+    def __init__(self, url, method='GET', query_args={}):
+        self.method = method
+        self.headers = {}
+        self._fields = {}
+        self._files = {}
+
+        # Replace all underscores in each query argument
+        # key with dashes.
+        query_args = dict([
+            (key.replace('_', '-'), value)
+            for key, value in query_args.iteritems()
+        ])
+
+        # Add the query arguments to the url
+        # TODO: Make work with Python < 2.6. In 2.6
+        # parse_qsl was moved from cgi to urlparse.
+        url_parts = list(urlparse(url))
+        query = dict(parse_qsl(url_parts[4]))
+        query.update(query_args)
+        url_parts[4] = urllib.urlencode(query)
+        self.url = urlunparse(url_parts)
+
+    def add_field(self, name, value):
+        self._fields[name] = value
+
+    def add_file(self, name, filename, content):
+        self._files[name] = {
+            'filename': filename,
+            'content': content,
+        }
+
+    def del_field(self, name):
+        del self._fields[name]
+
+    def del_file(self, filename):
+        del self._files[filename]
+
+    def encode_multipart_formdata(self):
+        """ Encodes data for use in an HTTP request.
+
+        Parameters:
+            fields - the fields to be encoded.  This should be a dict in a
+                     key:value format
+            files  - the files to be encoded.  This should be a dict in a
+                     key:dict, filename:value and content:value format
+        """
+        if not (self._fields or self._files):
+            return None, None
+
+        NEWLINE = '\r\n'
+        BOUNDARY = mimetools.choose_boundary()
+        content = StringIO()
+
+        for key in self._fields:
+            content.write('--' + BOUNDARY + NEWLINE)
+            content.write('Content-Disposition: form-data; name="%s"' % key)
+            content.write(NEWLINE + NEWLINE)
+            content.write(str(self._fields[key]) + NEWLINE)
+
+        for key in self._files:
+            filename = self._files[key]['filename']
+            value = self._files[key]['content']
+            mime_type = (mimetypes.guess_type(filename)[0] or
+                         'application/octet-stream')
+            content.write('--' + BOUNDARY + NEWLINE)
+            content.write('Content-Disposition: form-data; name="%s"; ' % key)
+            content.write('filename="%s"' % filename + NEWLINE)
+            content.write('Content-Type: %s' % mime_type + NEWLINE)
+            content.write('Content-Transfer-Encoding: base64' + NEWLINE)
+            content.write(NEWLINE)
+            content.write(base64.b64encode(value).decode())
+            content.write(NEWLINE)
+
+        content.write('--' + BOUNDARY + '--' + NEWLINE + NEWLINE)
+        content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
+
+        return content_type, content.getvalue()
+
+
+class Request(urllib2.Request):
+    """A request which contains a method attribute."""
+    def __init__(self, url, body='', headers={}, method="PUT"):
+        urllib2.Request.__init__(self, url, body, headers)
+        self.method = method
+
+    def get_method(self):
+        return self.method
+
+
+class PresetHTTPAuthHandler(urllib2.BaseHandler):
+    """urllib2 handler that presets the use of HTTP Basic Auth."""
+    handler_order = 480  # After Basic auth
+
+    def __init__(self, url, password_mgr):
+        self.url = url
+        self.password_mgr = password_mgr
+        self.used = False
+
+    def reset(self, username, password):
+        self.password_mgr.rb_user = username
+        self.password_mgr.rb_pass = password
+        self.used = False
+
+    def http_request(self, request):
+        if not self.used:
+            # Note that we call password_mgr.find_user_password to get the
+            # username and password we're working with.
+            username, password = \
+                self.password_mgr.find_user_password('Web API', self.url)
+            raw = '%s:%s' % (username, password)
+            request.add_header(
+                urllib2.HTTPBasicAuthHandler.auth_header,
+                'Basic %s' % base64.b64encode(raw).strip())
+            self.used = True
+
+        return request
+
+    https_request = http_request
+
+
+class ReviewBoardHTTPErrorProcessor(urllib2.HTTPErrorProcessor):
+    """Processes HTTP error codes.
+
+    Python 2.6 gets HTTP error code processing right, but 2.4 and 2.5
+    only accepts HTTP 200 and 206 as success codes. This handler
+    ensures that anything in the 200 range is a success.
+    """
+    def http_response(self, request, response):
+        if not (200 <= response.code < 300):
+            response = self.parent.error('http', request, response,
+                                         response.code, response.msg,
+                                         response.info())
+
+        return response
+
+    https_response = http_response
+
+
+class ReviewBoardHTTPBasicAuthHandler(urllib2.HTTPBasicAuthHandler):
+    """Custom Basic Auth handler that doesn't retry excessively.
+
+    urllib2's HTTPBasicAuthHandler retries over and over, which is
+    useless. This subclass only retries once to make sure we've
+    attempted with a valid username and password. It will then fail so
+    we can use our own retry handler.
+    """
+    def __init__(self, *args, **kwargs):
+        urllib2.HTTPBasicAuthHandler.__init__(self, *args, **kwargs)
+        self._retried = False
+        self._lasturl = ""
+
+    def retry_http_basic_auth(self, *args, **kwargs):
+        if self._lasturl != args[0]:
+            self._retried = False
+
+        self._lasturl = args[0]
+
+        if not self._retried:
+            self._retried = True
+            self.retried = 0
+            response = urllib2.HTTPBasicAuthHandler.retry_http_basic_auth(
+                self, *args, **kwargs)
+
+            if response.code != 401:
+                self._retried = False
+
+            return response
+        else:
+            return None
+
+
+class ReviewBoardHTTPPasswordMgr(urllib2.HTTPPasswordMgr):
+    """Adds HTTP authentication support for URLs.
+
+    Python 2.4's password manager has a bug in http authentication
+    when the target server uses a non-standard port.  This works
+    around that bug on Python 2.4 installs.
+
+    See: http://bugs.python.org/issue974757
+    """
+    def __init__(self, reviewboard_url, rb_user=None, rb_pass=None):
+        self.passwd = {}
+        self.rb_url = reviewboard_url
+        self.rb_user = rb_user
+        self.rb_pass = rb_pass
+
+    def find_user_password(self, realm, uri):
+        if realm == 'Web API':
+            return self.rb_user, self.rb_pass
+        else:
+            # If this is an auth request for some other domain (since HTTP
+            # handlers are global), fall back to standard password management.
+            return urllib2.HTTPPasswordMgr.find_user_password(self, realm, uri)
+
+
+class ReviewBoardServer(object):
+    """Represents a Review Board server we are communicating with.
+
+    Provides methods for executing HTTP requests on a Review Board
+    server's Web API.
+    """
+
+    def __init__(self, url, cookie_file, username=None, password=None,
+                 agent=None):
+        self.url = url
+        if self.url[-1] != '/':
+            self.url += '/'
+        self.cookie_file = cookie_file
+        self.cookie_jar = cookielib.MozillaCookieJar(self.cookie_file)
+
+        if self.cookie_file:
+            try:
+                self.cookie_jar.load(self.cookie_file, ignore_expires=True)
+            except IOError:
+                pass
+
+        # Set up the HTTP libraries to support all of the features we need.
+        password_mgr = ReviewBoardHTTPPasswordMgr(self.url,
+                                                  username,
+                                                  password)
+        self.preset_auth_handler = PresetHTTPAuthHandler(self.url,
+                                                         password_mgr)
+
+        handlers = []
+
+        handlers += [
+            urllib2.HTTPCookieProcessor(self.cookie_jar),
+            ReviewBoardHTTPBasicAuthHandler(password_mgr),
+            urllib2.HTTPDigestAuthHandler(password_mgr),
+            self.preset_auth_handler,
+            ReviewBoardHTTPErrorProcessor(),
+        ]
+
+        if agent:
+            self.agent = agent
+        else:
+            self.agent = 'RBTools/' + get_package_version()
+
+        opener = urllib2.build_opener(*handlers)
+        opener.addheaders = [
+            ('User-agent', self.agent),
+        ]
+        urllib2.install_opener(opener)
+
+    def login(self, username, password):
+        """Reset the user information"""
+        self.preset_auth_handler.reset(username, password)
+
+    def process_error(self, http_status, data):
+        """Processes an error, raising an APIError with the information."""
+        try:
+            rsp = json_loads(data)
+
+            assert rsp['stat'] == 'fail'
+
+            raise APIError(http_status, rsp['err']['code'], rsp,
+                           rsp['err']['msg'])
+        except ValueError:
+            raise APIError(http_status, None, None, data)
+
+    def make_request(self, request):
+        """Perform an http request.
+
+        The request argument should be an instance of
+        'rbtools.api.request.HttpRequest'.
+        """
+        try:
+            content_type, body = request.encode_multipart_formdata()
+            headers = request.headers
+            if body:
+                headers.update({
+                    'Content-Type': content_type,
+                    'Content-Length': str(len(body)),
+                    })
+
+            r = Request(request.url, body, headers, request.method)
+            rsp = urllib2.urlopen(r)
+        except urllib2.HTTPError, e:
+            self.process_error(e.code, e.read())
+        except urllib2.URLError, e:
+            raise ServerInterfaceError("%s" % e.reason)
+
+        try:
+            self.cookie_jar.save(self.cookie_file)
+        except IOError:
+            pass
+
+        return rsp
diff --git a/rbtools/api/resource.py b/rbtools/api/resource.py
new file mode 100644
index 0000000000000000000000000000000000000000..2b957a7a794700201a55fb8b35c0d1bb31c98f08
--- /dev/null
+++ b/rbtools/api/resource.py
@@ -0,0 +1,257 @@
+import re
+
+from rbtools.api.request import HttpRequest
+
+
+RESOURCE_MAP = {}
+LINKS_TOK = 'links'
+_EXCLUDE_ATTRS = [LINKS_TOK, 'stat']
+
+
+def _create(resource, data={}, *args, **kwargs):
+    """Generate a POST request on a resource."""
+    request = HttpRequest(resource._links['create']['href'], method='POST',
+                          query_args=kwargs)
+
+    for name, value in data.iteritems():
+        request.add_field(name, value)
+
+    return request
+
+
+def _delete(resource, *args, **kwargs):
+    """Generate a DELETE request on a resource."""
+    return HttpRequest(resource._links['delete']['href'], method='DELETE',
+                       query_args=kwargs)
+
+
+def _get_self(resource, *args, **kwargs):
+    """Generate a request for a resource's 'self' link."""
+    return HttpRequest(resource._links['self']['href'], query_args=kwargs)
+
+
+def _update(resource, data={}, *args, **kwargs):
+    """Generate a PUT request on a resource."""
+    request = HttpRequest(resource._links['update']['href'], method='PUT',
+                          query_args=kwargs)
+
+    for name, value in data.iteritems():
+        request.add_field(name, value)
+
+    return request
+
+
+# This dictionary is a mapping of special keys in a resources links,
+# to a name and method used for generating a request for that link.
+# This is used to special case the REST operation links. Any link
+# included in this dictionary will be generated separately, and links
+# with a None for the method will be ignored.
+SPECIAL_LINKS = {
+    'create': ['create', _create],
+    'delete': ['delete', _delete],
+    'next': ['get_next', None],
+    'prev': ['get_prev', None],
+    'self': ['get_self', _get_self],
+    'update': ['update', _update],
+}
+
+
+class Resource(object):
+    """Defines common functionality for Item and List Resources.
+
+    Resources are able to make requests to the Web API by returning an
+    HttpRequest object. When an HttpRequest is returned from a method
+    call, the transport layer will execute this request and return the
+    result to the user.
+
+    Methods for constructing requests to perform each of the supported
+    REST operations will be generated automatically. These methods
+    will have names corresponding to the operation (e.g. 'update()').
+    An additional method for re-requesting the resource using the
+    'self' link will be generated with the name 'get_self'. Each
+    additional link will have a method generated which constructs a
+    request for retrieving the linked resource.
+    """
+    _excluded_attrs = []
+
+    def __init__(self, payload, url, token=None, **kwargs):
+        self.url = url
+        self._token = token
+        self._payload = payload
+        self._excluded_attrs = self._excluded_attrs + _EXCLUDE_ATTRS
+
+        # Determine where the links live in the payload. This
+        # can either be at the root, or inside the resources
+        # token.
+        if LINKS_TOK in self._payload:
+            self._links = self._payload[LINKS_TOK]
+        elif (token and isinstance(self._payload[token], dict) and
+              LINKS_TOK in self._payload[token]):
+            self._links = self._payload[token][LINKS_TOK]
+        else:
+            self._payload[LINKS_TOK] = {}
+            self._links = {}
+
+        # Add a method for each supported REST operation, and
+        # retrieving 'self'.
+        for link, method in SPECIAL_LINKS.iteritems():
+            if link in self._links and method[1]:
+                setattr(self, method[0],
+                    lambda resource=self, meth=method[1], **kwargs:
+                    meth(resource, **kwargs))
+
+        # Generate request methods for any additional links
+        # the resource has.
+        for link, body in self._links.iteritems():
+            if link not in SPECIAL_LINKS:
+                setattr(self, "get_%s" % (link),
+                        lambda url=body['href'], **kwargs: HttpRequest(
+                            url, query_args=kwargs))
+
+
+class ResourceItem(Resource):
+    """The base class for Item Resources.
+
+    Any resource specific base classes for Item Resources should
+    inherit from this class. If a resource specific base class does
+    not exist for an Item Resource payload, this class will be used to
+    create the resource.
+
+    The body of the resource is copied into the fields dictionary. The
+    Transport is responsible for providing access to this data,
+    preferably as attributes for the wrapping class.
+    """
+    _excluded_attrs = []
+
+    def __init__(self, payload, url, token=None, **kwargs):
+        super(ResourceItem, self).__init__(payload, url, token=token)
+        self.fields = {}
+
+        # Determine the body of the resource's data.
+        if token is not None:
+            data = self._payload[token]
+        else:
+            data = self._payload
+
+        for name, value in data.iteritems():
+            if name not in self._excluded_attrs:
+                self.fields[name] = value
+
+
+class CountResource(ResourceItem):
+    """Resource returned by a query with 'counts-only' true.
+
+    When a resource is requested using 'counts-only', the payload will
+    not contain the regular fields for the resource. In order to
+    special case all payloads of this form, this class is used for
+    resource construction.
+    """
+    def __init__(self, payload, url, **kwargs):
+        super(CountResource, self).__init__(payload, url, token=None)
+
+    def get_self(self, **kwargs):
+        """Generate an GET request for the resource list.
+
+        This will return an HttpRequest to retrieve the list resource
+        which this resource is a count for. Any query arguments used
+        in the request for the count will still be present, only the
+        'counts-only' argument will be removed
+        """
+        # TODO: Fix this. It is generating a new request
+        # for a URL with 'counts-only' set to False, but
+        # RB treats the  argument being set to any value
+        # as true.
+        kwargs.update({'counts_only': False})
+        return HttpRequest(self.url, query_args=kwargs)
+
+
+class ResourceList(Resource):
+    """The base class for List Resources.
+
+    Any resource specific base classes for List Resources should
+    inherit from this class. If a resource specific base class does
+    not exist for a List Resource payload, this class will be used to
+    create the resource.
+
+    Instances of this class will act as a sequence, providing access
+    to the payload for each Item resource in the list. Iteration is
+    over the page of item resources returned by a single request, and
+    not the entire list of resources. To iterate over all item
+    resources 'get_next()' or 'get_prev()' should be used to grab
+    additional pages of items.
+    """
+    def __init__(self, payload, url, token=None, item_mime_type=None):
+        super(ResourceList, self).__init__(payload, url, token=token)
+        self._item_mime_type = item_mime_type
+
+        if token:
+            self._item_list = payload[self._token]
+        else:
+            self._item_list = payload
+
+        self.num_items = len(self._item_list)
+        self.total_results = payload['total_results']
+
+    def __len__(self):
+        return self.num_items
+
+    def __nonzero__(self):
+        return True
+
+    def __getitem__(self, key):
+        return self._item_list[key]
+
+    def __iter__(self):
+        return self._item_list.__iter__()
+
+    def get_next(self, **kwargs):
+        if 'next' not in self._links:
+            raise StopIteration()
+
+        return HttpRequest(self._links['next']['href'], query_args=kwargs)
+
+    def get_prev(self, **kwargs):
+        if 'prev' not in self._links:
+            raise StopIteration()
+
+        return HttpRequest(self._links['prev']['href'], query_args=kwargs)
+
+
+class RootResource(ResourceItem):
+    """The Root resource specific base class.
+
+    Provides additional methods for fetching any resource directly
+    using the uri templates. A method of the form "get_<uri-template-name>"
+    is called to retrieve the HttpRequest corresponding to the
+    resource. Template replacement values should be passed in as a
+    dictionary to the values parameter.
+    """
+    _excluded_attrs = ['uri_templates']
+    _TEMPLATE_PARAM_RE = re.compile('\{(?P<key>.*)\}')
+
+    def __init__(self, payload, url, **kwargs):
+        super(RootResource, self).__init__(payload, url, token=None)
+        # Generate methods for accessing resources directly using
+        # the uri-templates.
+        for name, url in payload['uri_templates'].iteritems():
+            attr_name = "get_%s" % name
+            if not hasattr(self, attr_name):
+                setattr(self, attr_name,
+                        lambda url=url, **kwargs: self._get_template_request(
+                            url, **kwargs))
+
+    def _get_template_request(self, url_template, values, **kwargs):
+        url = self._TEMPLATE_PARAM_RE.sub(
+            lambda m: str(values[m.group('key')]),
+            url_template)
+        return HttpRequest(url, query_args=kwargs)
+
+RESOURCE_MAP['application/vnd.reviewboard.org.root'] = RootResource
+
+
+class DiffResource(ResourceItem):
+    def get_patch(self):
+        """ Returns unified diff content."""
+        pass
+
+RESOURCE_MAP['application/vnd.reviewboard.org.diff'] = DiffResource
diff --git a/rbtools/api/transport/__init__.py b/rbtools/api/transport/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..c6f7ed05a9f11d830da009beee9a92ca0949f819
--- /dev/null
+++ b/rbtools/api/transport/__init__.py
@@ -0,0 +1,16 @@
+class Transport(object):
+    """Base class for API Transport layers.
+
+    An API Transport layer acts as an intermediary between the API
+    user and the Resource objects. All access to a resource's data,
+    and all communication with the Review Board server are handled by
+    the Transport. This allows for Transport implementations with
+    unique interfaces which operate on the same underlying resource
+    classes. Specifically, this allows for both a synchronous, and an
+    asynchronous implementation of the transport.
+
+    TODO: Actually make this class useful by pulling out
+    common functionality.
+    """
+    def __init__(self, url):
+        self.url = url
diff --git a/rbtools/api/transport/sync.py b/rbtools/api/transport/sync.py
new file mode 100644
index 0000000000000000000000000000000000000000..cd0288ab15b89940febe0146ef4f71ec5f693ca8
--- /dev/null
+++ b/rbtools/api/transport/sync.py
@@ -0,0 +1,270 @@
+from rbtools.api.decode import decode_response
+from rbtools.api.factory import create_resource
+from rbtools.api.request import HttpRequest, ReviewBoardServer
+from rbtools.api.resource import ResourceItem, ResourceList
+from rbtools.api.transport import Transport
+
+
+LINK_KEYS = set(['href', 'method', 'title'])
+
+
+class SyncTransport(Transport):
+    """A synchronous transport layer for the API client.
+
+    the url, cookie_file, username, and password parameters are
+    mandatory when using this resource. The file provided in
+    cookie_file is used to store and retrieive the authentication
+    cookies for the API.
+
+    The optional agent parameter can be used to specify a custom
+    User-Agent string for the API. If not provided, the default
+    RBTools User-Agent will be used.
+    """
+    def __init__(self, url, cookie_file, username, password, agent=None):
+        super(SyncTransport, self).__init__(url)
+        self.server = ReviewBoardServer(self.url, cookie_file,
+                                        username=username, password=password)
+
+        self.get_root = SyncTransportMethod(self, self._root_request)
+
+    def _root_request(self):
+        return HttpRequest(self.server.url)
+
+    def wrap(self, value):
+        """Wrap any values returned to the user
+
+        All values returned from the transport should be wrapped with
+        this method, unless the specific type is known and handled as
+        a special case. This wrapping allows for nested dictionaries
+        and fields to be accessed as attributes, instead of using the
+        '[]' operation.
+
+        This wrapping is also necessary to have control over updates to
+        nested fields inside the resource.
+        """
+        if isinstance(value, ResourceItem):
+            return SyncTransportItemResource(self, value)
+        elif isinstance(value, ResourceList):
+            return SyncTransportListResource(self, value)
+        elif isinstance(value, list):
+            return ResourceListField(self, value)
+        elif isinstance(value, dict):
+            dict_keys = set(value.keys())
+            if ('href' in dict_keys and
+                len(dict_keys.difference(LINK_KEYS)) == 0):
+                return SyncTransportResourceLink(self, **value)
+            else:
+                return ResourceDictField(self, value)
+        else:
+            return value
+
+
+class ResourceDictField(object):
+    """Wrapper for dictionaries returned from a resource.
+
+    Any dictionary returned from a resource will be wrapped using this
+    class. Attribute access will correspond to accessing the
+    dictionary key with the name of the attribute.
+    """
+    def __init__(self, transport, fields_dict):
+        object.__setattr__(self, '_transport', transport)
+        object.__setattr__(self, '_fields_dict', fields_dict)
+
+    def __getattr__(self, name):
+        fields = object.__getattribute__(self, '_fields_dict')
+        transport = object.__getattribute__(self, '_transport')
+
+        if name in fields:
+            return transport.wrap(fields[name])
+        else:
+            raise AttributeError
+
+    def __setattr__(self, name, value):
+            object.__getattribute__(self, '_fields_dict')[name] = value
+
+
+class SyncTransportListIterator(object):
+    """Iterator for lists which uses __getitem__."""
+    def __init__(self, l):
+        self._list = l
+        self.index = 0
+
+    def __iter__(self):
+        return self
+
+    def next(self):
+        try:
+            item = self._list[self.index]
+        except IndexError:
+            raise StopIteration
+
+        self.index += 1
+        return item
+
+
+class ResourceListField(list):
+    """Wrapper for lists returned from a resource.
+
+    Acts as a normal list, but wraps any returned items using the
+    transport.
+    """
+    def __init__(self, transport, list_field):
+        super(ResourceListField, self).__init__(list_field)
+        self._transport = transport
+
+    def __getitem__(self, key):
+        item = super(ResourceListField, self).__getitem__(key)
+        return self._transport.wrap(item)
+
+    def __iter__(self):
+        return SyncTransportListIterator(self)
+
+
+class SyncTransportResourceLink(object):
+    """Wrapper for links returned from a resource.
+
+    In order to support operations on links found outside of a
+    resource's links dictionary, detected links are wrapped with this
+    class.
+
+    A links fields (href, method, and title) are accessed as
+    attributes, and link operations are supported through method
+    calls. Currently the only supported method is "GET", which can be
+    invoked using the 'get' method.
+    """
+    def __init__(self, transport, href, method="GET", title=None):
+        self.href = href
+        self.method = method
+        self.title = title
+        self.get = SyncTransportMethod(transport, self._get)
+        # TODO: Might want to add support for methods other than "GET".
+
+    def _get(self):
+        return HttpRequest(self.href)
+
+
+class SyncTransportItemResource(object):
+    """Wrapper for Item resources.
+
+    Provides access to an item resource's data, and methods. To
+    retrieve a field from the resource's dictionary, the attribute with
+    name equal to the key should be accessed.
+
+    Any attributes which correspond to a resource method will be
+    wrapped, and calling the method will correspond to executing the
+    returned request.
+    """
+    _initted = False
+
+    def __init__(self, transport, resource):
+        self._transport = transport
+        self._resource = resource
+
+        # Indicate initialization is complete so that future
+        # setting of attributes is done on the resource.
+        self._initted = True
+
+    def __getattr__(self, name):
+        resource = object.__getattribute__(self, '_resource')
+        if name in resource.fields:
+            resource_attr = resource.fields[name]
+        else:
+            resource_attr = resource.__getattribute__(name)
+
+        transport = object.__getattribute__(self, '_transport')
+
+        if callable(resource_attr):
+            return SyncTransportMethod(transport, resource_attr)
+
+        return transport.wrap(resource_attr)
+
+    def __setattr__(self, name, value):
+        if not object.__getattribute__(self, '_initted'):
+            object.__setattr__(self, name, value)
+        else:
+            resource = object.__getattribute__(self, '_resource')
+
+            if resource and name in resource.fields:
+                resource.fields[name] = value
+            else:
+                raise AttributeError
+
+
+class SyncTransportListResource(object):
+    """Wrapper for List resources.
+
+    Acts as a sequence, providing access to the page of items. When a
+    list item is accessed, the item resource will be constructed using
+    the payload for the item, and returned.
+
+    Iteration is over the page of item resources returned by a single
+    request, and not the entire list of resources. To iterate over all
+    item resources 'get_next()' or 'get_prev()' should be used to grab
+    additional pages of items.
+    """
+    def __init__(self, transport, resource):
+        self._transport = transport
+        self._resource = resource
+        self._item_cache = {}
+
+    def __getattr__(self, name):
+        resource = object.__getattribute__(self, '_resource')
+        resource_attr = resource.__getattribute__(name)
+
+        if callable(resource_attr):
+            return SyncTransportMethod(self._transport, resource_attr)
+
+        return self._transport.wrap(resource_attr)
+
+    def __getitem__(self, key):
+        if key in self._item_cache:
+            return self._item_cache[key]
+
+        payload = self._resource[key]
+
+        # TODO: A proper url for the resource should be passed
+        # in to the factory.
+        resource = create_resource(
+            payload,
+            '',
+            mime_type=self._resource._item_mime_type,
+            guess_token=False)
+
+        wrapped_resource = self._transport.wrap(resource)
+        self._item_cache[key] = wrapped_resource
+        return wrapped_resource
+
+    def __iter__(self):
+        return SyncTransportListIterator(self)
+
+
+class SyncTransportMethod(object):
+    """Wrapper for resource methods.
+
+    Intercepts method return values, and synchronously executes all
+    returned HttpRequests. If a method returns an HttpRequest the
+    resulting response will be constructed into a new resource and
+    returned.
+    """
+    def __init__(self, transport, method):
+        self._transport = transport
+        self._method = method
+
+    def __call__(self, *args, **kwargs):
+        """Executed when a resource's method is called."""
+        call_result = self._method(*args, **kwargs)
+        if not isinstance(call_result, HttpRequest):
+            return call_result
+
+        rsp = self._transport.server.make_request(call_result)
+        info = rsp.info()
+        mime_type = info['Content-Type']
+        item_content_type = info.get('Item-Content-Type', None)
+        payload = rsp.read()
+        payload = decode_response(payload, mime_type)
+
+        resource = create_resource(payload, call_result.url,
+                                   mime_type=mime_type,
+                                   item_mime_type=item_content_type)
+
+        return self._transport.wrap(resource)
diff --git a/rbtools/api/utils.py b/rbtools/api/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..8f3febf9afdc8ecfb49492d241703bb7f6377d5b
--- /dev/null
+++ b/rbtools/api/utils.py
@@ -0,0 +1,37 @@
+def parse_mimetype(mime_type):
+    """Parse the mime type in to it's component parts."""
+    types = mime_type.split('/')
+
+    ret_val = {
+        'type': mime_type,
+        'main_type': types[0],
+        'sub_type': types[1]
+    }
+
+    sub_type = types[1].split('+')
+    ret_val['vendor'] = ''
+    if len(sub_type) == 1:
+        ret_val['format'] = sub_type[0]
+    else:
+        ret_val['format'] = sub_type[1]
+        ret_val['vendor'] = sub_type[0]
+
+    vendor = ret_val['vendor'].split('.')
+    if len(vendor) > 1:
+        ret_val['resource'] = vendor[-1].replace('-', '_')
+    else:
+        ret_val['resource'] = ''
+
+    return ret_val
+
+
+def rem_mime_format(mime_type):
+    """Strip the subtype from a mimetype, leaving vendor specific information.
+
+    Removes the portion of the subtype after a +, or the entire
+    subtype if no vendor specific type information is present.
+    """
+    if mime_type.rfind('+') != 0:
+        return mime_type.rsplit('+', 1)[0]
+    else:
+        return mime_type.rsplit('/', 1)[0]
