diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 9f93cb3..fa62cd8 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -9,11 +9,11 @@ jobs:
     runs-on: ubuntu-18.04
 
     steps:
-    - uses: actions/checkout@master
-    - name: Set up Python 3.9
-      uses: actions/setup-python@v1
+    - uses: actions/checkout@v3
+    - name: Set up Python 3.11
+      uses: actions/setup-python@v3
       with:
-        python-version: 3.9
+        python-version: "3.11"
     - name: Install pypa/build
       run: >-
         python -m
@@ -31,4 +31,4 @@ jobs:
     - name: Publish distribution 📦 to PyPI
       uses: pypa/gh-action-pypi-publish@master
       with:
-        password: ${{ secrets.PYPI_API_TOKEN }}
\ No newline at end of file
+        password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml
index 37956f9..5a097aa 100644
--- a/.github/workflows/python-app.yml
+++ b/.github/workflows/python-app.yml
@@ -19,14 +19,14 @@ jobs:
 
     steps:
     - uses: actions/checkout@v3
-    - name: Set up Python 3.10
+    - name: Set up Python 3.11
       uses: actions/setup-python@v3
       with:
-        python-version: "3.10"
+        python-version: "3.11"
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
-        pip install requests flake8 pytest
+        pip install requests flake8 pytest pytest-asyncio
         if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
     - name: Lint with flake8
       run: |
diff --git a/.gitignore b/.gitignore
index 6b70983..f004a8b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -103,8 +103,12 @@ venv.bak/
 .mypy_cache/
 
 .DS_Store
+.idea
 
 # Pipenv
 Pipfile
 Pipfile.lock
 .vscode/launch.json
+
+# Unasync
+knockapi/sync_client
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..2d970d2
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1 @@
+recursive-include knockapi/sync_client *
diff --git a/README.md b/README.md
index 45e5955..7391c02 100644
--- a/README.md
+++ b/README.md
@@ -118,7 +118,22 @@ client.users.set_preferences(
 client.users.get_preferences(user_id="jhammond")
 ```
 
-### Signing JWTs
+## AsyncIO Usage
+The library utilises the [unasync](https://github.com/python-trio/unasync) library to provide full AsyncIO support, you just need to import the AsyncKnock client.
+```python
+import asyncio
+from knockapi import AsyncKnock
+
+client = AsyncKnock(api_key="sk_12345")
+
+async def example_coroutine():
+    messages = await client.messages.list()
+    print(messages)
+
+asyncio.run(example_coroutine())
+```
+
+## Signing JWTs
 
 You can use the `pyjwt` package to [sign JWTs easily](https://pyjwt.readthedocs.io/en/stable/usage.html#encoding-decoding-tokens-with-rs256-rsa).
 You will need to generate an environment specific signing key, which you can find in the Knock dashboard.
diff --git a/knockapi/__init__.py b/knockapi/__init__.py
index 220cf65..5793456 100644
--- a/knockapi/__init__.py
+++ b/knockapi/__init__.py
@@ -1,2 +1,14 @@
-from knockapi.client import Knock
-from knockapi.resources import *
+__version__ = '0.6.0'
+
+from knockapi.async_client import Knock as AsyncKnock
+
+try:
+    from knockapi.sync_client import Knock
+except ImportError:
+    Knock = None
+
+__all__ = [
+    "AsyncKnock",
+    "Knock",
+    "__version__"
+]
diff --git a/knockapi/client.py b/knockapi/async_client/Knock.py
similarity index 50%
rename from knockapi/client.py
rename to knockapi/async_client/Knock.py
index 0f432cd..5318fbb 100644
--- a/knockapi/client.py
+++ b/knockapi/async_client/Knock.py
@@ -1,94 +1,62 @@
-import requests
-from json.decoder import JSONDecodeError
-
-__version__ = '0.5.2'
-
-
-class Connection(object):
-    def __init__(self, api_key, host='https://api.knock.app'):
-        self.api_key = api_key
-        self.host = host
-        self.client_version = __version__
-        self.headers = {
-            'Authorization': 'Bearer {}'.format(self.api_key),
-            'User-Agent': 'Knock Python - {}'.format(self.client_version)
-        }
-
-    def request(self, method, endpoint, payload=None):
-        url = '{}/v1{}'.format(self.host, endpoint)
-
-        r = requests.request(
-            method,
-            url,
-            params=payload if method == 'get' else None,
-            json=payload if method != 'get' else None,
-            headers=self.headers,
-        )
-
-        # If we got a successful response, then attempt to deserialize as JSON
-        if r.ok:
-            try:
-                return r.json()
-            except JSONDecodeError:
-                return None
+from knockapi import __version__
+from knockapi.core import AsyncConnection
+from knockapi.async_client.services import User, Workflows, Preferences, Objects, Tenants, BulkOperations, Messages
 
-        return r.raise_for_status()
 
-
-class Knock(Connection):
+class Knock(object):
     """Client to access the Knock features."""
-    @property
-    def _auth(self):
-        return self.api_key
 
-    @property
-    def _version(self):
-        return __version__
+    def __init__(
+        self,
+        api_key: str,
+        api_host: str = 'https://api.knock.app',
+        read_timeout: int = 300
+    ):
+        self.connection = AsyncConnection(
+            api_key,
+            api_host,
+            __version__,
+            read_timeout
+        )
 
     @property
     def users(self):
-        from .resources import User
         return User(self)
 
     @property
     def workflows(self):
-        from .resources import Workflows
         return Workflows(self)
 
     @property
     def preferences(self):
-        from .resources import Preferences
         return Preferences(self)
 
     @property
     def objects(self):
-        from .resources import Objects
         return Objects(self)
 
     @property
     def tenants(self):
-        from .resources import Tenants
         return Tenants(self)
 
     @property
     def bulk_operations(self):
-        from .resources import BulkOperations
         return BulkOperations(self)
 
     @property
     def messages(self):
-        from .resources import Messages
         return Messages(self)
 
     # Defined at the top level here for convenience
-    def notify(
-            self,
-            key,
-            recipients,
-            data={},
-            actor=None,
-            cancellation_key=None,
-            tenant=None):
+    async def notify(
+        self,
+        key,
+        recipients,
+        data={},
+        actor=None,
+        cancellation_key=None,
+        tenant=None
+    ):
         """
         Triggers a workflow.
 
@@ -112,10 +80,11 @@ def notify(
             dict: Response from Knock.
         """
         # Note: this is essentially a delegated method
-        return self.workflows.trigger(
+        return await self.workflows.trigger(
             key=key,
             recipients=recipients,
             data=data,
             actor=actor,
             cancellation_key=cancellation_key,
-            tenant=tenant)
+            tenant=tenant
+        )
diff --git a/knockapi/async_client/__init__.py b/knockapi/async_client/__init__.py
new file mode 100644
index 0000000..ca6d96e
--- /dev/null
+++ b/knockapi/async_client/__init__.py
@@ -0,0 +1 @@
+from .Knock import Knock
diff --git a/knockapi/resources/__init__.py b/knockapi/async_client/services/__init__.py
similarity index 100%
rename from knockapi/resources/__init__.py
rename to knockapi/async_client/services/__init__.py
diff --git a/knockapi/async_client/services/bulk_operations.py b/knockapi/async_client/services/bulk_operations.py
new file mode 100644
index 0000000..81a6604
--- /dev/null
+++ b/knockapi/async_client/services/bulk_operations.py
@@ -0,0 +1,19 @@
+from knockapi.core.Service import Service
+
+default_set_id = "default"
+
+
+class BulkOperations(Service):
+
+    async def get(self, bulk_operation_id):
+        """
+        Returns a bulk operation.
+
+        Args:
+            bulk_operation_id (str): The id of the bulk operation
+
+        Returns:
+            dict: A Knock BulkOperation
+        """
+        endpoint = f'/bulk_operations/{bulk_operation_id}'
+        return await self.client.connection.request('get', endpoint)
diff --git a/knockapi/resources/messages.py b/knockapi/async_client/services/messages.py
similarity index 53%
rename from knockapi/resources/messages.py
rename to knockapi/async_client/services/messages.py
index 01fd294..c287d16 100644
--- a/knockapi/resources/messages.py
+++ b/knockapi/async_client/services/messages.py
@@ -1,8 +1,11 @@
 import json
-from .service import Service
+
+from knockapi.core.Service import Service
+
 
 class Messages(Service):
-    def list(self, options=None):
+
+    async def list(self, options=None):
         """
         Gets a paginated list of Message records
 
@@ -17,60 +20,60 @@ def list(self, options=None):
         if options and options['trigger_data']:
             options['trigger_data'] = json.dumps(options['trigger_data'])
 
-        return self.client.request('get', endpoint, payload=options)
+        return await self.client.connection.request('get', endpoint, payload=options)
 
-    def get(self, id):
+    async def get(self, message_id):
         """
         Get a message by its id
 
         Args:
-            id: The message ID
+            message_id: The message ID
 
         Returns:
             dict: Message response from Knock.
         """
-        endpoint = '/messages/{}'.format(id)
-        return self.client.request('get', endpoint)
+        endpoint = f'/messages/{message_id}'
+        return await self.client.connection.request('get', endpoint)
 
-    def get_content(self, id):
+    async def get_content(self, message_id):
         """
         Get a message's content by its id
 
         Args:
-            id: The message ID
+            message_id: The message ID
 
         Returns:
             dict: MessageContent response from Knock.
         """
-        endpoint = '/messages/{}/content'.format(id)
-        return self.client.request('get', endpoint)
+        endpoint = f'/messages/{message_id}/content'
+        return await self.client.connection.request('get', endpoint)
 
-    def get_activities(self, id, options=None):
+    async def get_activities(self, message_id, options=None):
         """
         Get a message's activities by its id
 
         Args:
-            id: The message ID
+            message_id: The message ID
 
         Returns:
             dict: paginated Activity response from Knock.
         """
-        endpoint = '/messages/{}/activities'.format(id)
+        endpoint = f'/messages/{message_id}/activities'
 
         if options and options['trigger_data']:
             options['trigger_data'] = json.dumps(options['trigger_data'])
 
-        return self.client.request('get', endpoint, options)
+        return await self.client.connection.request('get', endpoint, options)
 
-    def get_events(self, id, options=None):
+    async def get_events(self, message_id, options=None):
         """
         Get a message's events by its id
 
         Args:
-            id: The message ID
+            message_id: The message ID
 
         Returns:
             dict: paginated Event response from Knock.
         """
-        endpoint = '/messages/{}/events'.format(id)
-        return self.client.request('get', endpoint, options)
+        endpoint = f'/messages/{message_id}/events'
+        return await self.client.connection.request('get', endpoint, options)
diff --git a/knockapi/resources/objects.py b/knockapi/async_client/services/objects.py
similarity index 60%
rename from knockapi/resources/objects.py
rename to knockapi/async_client/services/objects.py
index c9bf486..4072d52 100644
--- a/knockapi/resources/objects.py
+++ b/knockapi/async_client/services/objects.py
@@ -1,41 +1,43 @@
 import json
-from .service import Service
+
+from knockapi.core.Service import Service
 
 default_set_id = "default"
 
 
 class Objects(Service):
-    def get(self, collection, id):
+
+    async def get(self, collection, object_id):
         """
         Returns an object in a collection with the id given.
 
         Args:
             collection (str): The collection the object belongs to
-            id (str): The id of the object in the collection
+            object_id (str): The id of the object in the collection
 
         Returns:
             dict: A Knock Object
         """
-        endpoint = '/objects/{}/{}'.format(collection, id)
-        return self.client.request('get', endpoint)
+        endpoint = f'/objects/{collection}/{object_id}'
+        return await self.client.connection.request('get', endpoint)
 
     # NOTE: This is `set_object` as `set` is a reserved keyword
-    def set_object(self, collection, id, data={}):
+    async def set_object(self, collection, object_id, data={}):
         """
         Returns an object in a collection with the id given.
 
         Args:
             collection (str): The collection the object belongs to
-            id (str): The id of the object in the collection
+            object_id (str): The id of the object in the collection
             data (dict): The data to set on the object
 
         Returns:
             dict: A Knock Object
         """
-        endpoint = '/objects/{}/{}'.format(collection, id)
-        return self.client.request('put', endpoint, payload=data)
+        endpoint = f'/objects/{collection}/{object_id}'
+        return await self.client.connection.request('put', endpoint, payload=data)
 
-    def bulk_set(self, collection, objects):
+    async def bulk_set(self, collection, objects):
         """
         Bulk sets up to 100 objects in a collection.
 
@@ -47,24 +49,24 @@ def bulk_set(self, collection, objects):
             dict: BulkOperation from Knock
         """
         data = {'objects': objects}
-        endpoint = '/objects/{}/bulk/set'.format(collection)
-        return self.client.request('post', endpoint, payload=data)
+        endpoint = f'/objects/{collection}/bulk/set'
+        return await self.client.connection.request('post', endpoint, payload=data)
 
-    def delete(self, collection, id):
+    async def delete(self, collection, object_id):
         """
         Deletes the given object.
 
         Args:
             collection (str): The collection the object belongs to
-            id (str): The id of the object in the collection
+            object_id (str): The id of the object in the collection
 
         Returns:
             None: No response
         """
-        endpoint = '/objects/{}/{}'.format(collection, id)
-        return self.client.request('delete', endpoint)
+        endpoint = f'/objects/{collection}/{object_id}'
+        return await self.client.connection.request('delete', endpoint)
 
-    def bulk_delete(self, collection, object_ids):
+    async def bulk_delete(self, collection, object_ids):
         """
         Bulk deletes up to 100 objects in a collection.
 
@@ -76,112 +78,109 @@ def bulk_delete(self, collection, object_ids):
             dict: BulkOperation from Knock
         """
         data = {'object_ids': object_ids}
-        endpoint = '/objects/{}/bulk/delete'.format(collection)
-        return self.client.request('post', endpoint, payload=data)
+        endpoint = f'/objects/{collection}/bulk/delete'
+        return await self.client.connection.request('post', endpoint, payload=data)
 
     ##
     # Channel data
     ##
 
-    def get_channel_data(self, collection, id, channel_id):
+    async def get_channel_data(self, collection, object_id, channel_id):
         """
         Get object's channel data for the given channel id.
 
         Args:
             collection (str): The collection the object belongs to
-            id (str): The id of the object in the collection
+            object_id (str): The id of the object in the collection
             channel_id (str): Target channel ID
 
         Returns:
             dict: Channel data from Knock.
         """
-        endpoint = '/objects/{}/{}/channel_data/{}'.format(
-            collection, id, channel_id)
-        return self.client.request('get', endpoint)
+        endpoint = f'/objects/{collection}/{object_id}/channel_data/{channel_id}'
+        return await self.client.connection.request('get', endpoint)
 
-    def set_channel_data(self, collection, id, channel_id, channel_data):
+    async def set_channel_data(self, collection, object_id, channel_id, channel_data):
         """
         Upserts object's channel data for the given channel id.
 
         Args:
             collection (str): The collection the object belongs to
-            id (str): The id of the object in the collection
+            object_id (str): The id of the object in the collection
             channel_id (str): Target channel ID
             channel_data (dict): Channel data
 
         Returns:
             dict: Channel data from Knock.
         """
-        endpoint = '/objects/{}/{}/channel_data/{}'.format(
-            collection, id, channel_id)
-        return self.client.request(
+        endpoint = f'/objects/{collection}/{object_id}/channel_data/{channel_id}'
+        return await self.client.connection.request(
             'put', endpoint, payload={
                 'data': channel_data})
 
-    def unset_channel_data(self, collection, id, channel_id):
+    async def unset_channel_data(self, collection, object_id, channel_id):
         """
         Unsets the object's channel data for the given channel id.
 
         Args:
             collection (str): The collection the object belongs to
-            id (str): The id of the object in the collection
+            object_id (str): The id of the object in the collection
             channel_id (str): Target channel ID
 
         Returns:
             None: No response
         """
-        endpoint = '/objects/{}/{}/channel_data/{}'.format(
-            collection, id, channel_id)
-        return self.client.request('delete', endpoint)
+        endpoint = f'/objects/{collection}/{object_id}/channel_data/{channel_id}'
+        return await self.client.connection.request('delete', endpoint)
 
     ##
     # Messages
     ##
 
-    def get_messages(self, collection, id, options=None):
+    async def get_messages(self, collection, object_id, options=None):
         """
         Get object's messages
 
         Args:
             collection (str): The collection the object belongs to
-            id (str): The id of the object in the collection
+            object_id (str): The id of the object in the collection
             options (dict): An optional set of filtering options to pass to the query
 
         Returns:
             dict: Paginated Message response.
         """
-        endpoint = '/objects/{}/{}/messages'.format(collection, id)
+        endpoint = f'/objects/{collection}/{object_id}/messages'
 
         if options and options['trigger_data']:
             options['trigger_data'] = json.dumps(options['trigger_data'])
 
-        return self.client.request('get', endpoint, payload=options)
+        return await self.client.connection.request('get', endpoint, payload=options)
 
     ##
     # Preferences
     ##
 
-    def get_all_preferences(self, collection, id):
+    async def get_all_preferences(self, collection, object_id):
         """
         Get an objects full set of preferences
 
         Args:
             collection (str): The collection the object belongs to
-            id (str): The id of the object in the collection
+            object_id (str): The id of the object in the collection
 
         Returns:
             dict: PreferenceSet response from Knock.
         """
-        endpoint = '/objects/{}/{}/preferences'.format(collection, id)
-        return self.client.request('get', endpoint)
+        endpoint = f'/objects/{collection}/{object_id}/preferences'
+        return await self.client.connection.request('get', endpoint)
 
-    def get_preferences(self, collection, id, options={}):
+    async def get_preferences(self, collection, object_id, options={}):
         """
         Get a preference set
 
         Args:
             collection (str): The collection the object belongs to
-            id (str): The id of the object in the collection
+            object_id (str): The id of the object in the collection
             options (dict):
                 preference_set (str): The preference set to retrieve (defaults to "default")
 
@@ -189,15 +188,14 @@ def get_preferences(self, collection, id, options={}):
             dict: PreferenceSet response from Knock.
         """
         preference_set_id = options.get('preference_set', default_set_id)
-        endpoint = '/objects/{}/{}/preferences/{}'.format(
-            collection, id, preference_set_id)
+        endpoint = f'/objects/{collection}/{object_id}/preferences/{preference_set_id}'
 
-        return self.client.request('get', endpoint)
+        return await self.client.connection.request('get', endpoint)
 
-    def set_preferences(
+    async def set_preferences(
             self,
             collection,
-            id,
+            object_id,
             channel_types=None,
             categories=None,
             workflows=None,
@@ -207,7 +205,7 @@ def set_preferences(
 
         Args:
             collection (str): The collection the object belongs to
-            id (str): The id of the object in the collection
+            object_id (str): The id of the object in the collection
             channel_types (dict): A dictionary of channel type preferences
             categories (dict): A dictionary of category preferences
             workflows (dict): A dictionary of workflow preferences
@@ -218,8 +216,7 @@ def set_preferences(
         """
         preference_set_id = options.get('preference_set', default_set_id)
 
-        endpoint = '/objects/{}/{}/preferences/{}'.format(
-            collection, id, preference_set_id)
+        endpoint = f'/objects/{collection}/{object_id}/preferences/{preference_set_id}'
 
         params = {
             'channel_types': channel_types,
@@ -227,16 +224,16 @@ def set_preferences(
             'workflows': workflows
         }
 
-        return self.client.request('put', endpoint, payload=params)
+        return await self.client.connection.request('put', endpoint, payload=params)
 
-    def set_channel_types_preferences(
-            self, collection, id, preferences, options={}):
+    async def set_channel_types_preferences(
+            self, collection, object_id, preferences, options={}):
         """
         Sets the channel type preferences
 
         Args:
             collection (str): The collection the object belongs to
-            id (str): The id of the object in the collection
+            object_id (str): The id of the object in the collection
             preferences (dict): A dictionary of channel type preferences
             options (dict): A dictionary of options
 
@@ -245,15 +242,14 @@ def set_channel_types_preferences(
         """
         preference_set_id = options.get('preference_set', default_set_id)
 
-        endpoint = '/objects/{}/{}/preferences/{}/channel_types'.format(
-            collection, id, preference_set_id)
+        endpoint = f'/objects/{collection}/{object_id}/preferences/{preference_set_id}/channel_types'
 
-        return self.client.request('put', endpoint, payload=preferences)
+        return await self.client.connection.request('put', endpoint, payload=preferences)
 
-    def set_channel_type_preferences(
+    async def set_channel_type_preferences(
             self,
             collection,
-            id,
+            object_id,
             channel_type,
             setting,
             options={}):
@@ -262,7 +258,7 @@ def set_channel_type_preferences(
 
         Args:
             collection (str): The collection the object belongs to
-            id (str): The id of the object in the collection
+            object_id (str): The id of the object in the collection
             channel_type (str): The channel_type to set
             setting (boolean): The preference setting
             options (dict): A dictionary of options
@@ -272,17 +268,16 @@ def set_channel_type_preferences(
         """
         preference_set_id = options.get('preference_set', default_set_id)
 
-        endpoint = '/objects/{}/{}/preferences/{}/channel_types/{}'.format(
-            collection, id, preference_set_id, channel_type)
+        endpoint = f'/objects/{collection}/{object_id}/preferences/{preference_set_id}/channel_types/{channel_type}'
 
-        return self.client.request(
+        return await self.client.connection.request(
             'put', endpoint, payload={
                 'subscribed': setting})
 
-    def set_workflows_preferences(
+    async def set_workflows_preferences(
             self,
             collection,
-            id,
+            object_id,
             preferences,
             options={}):
         """
@@ -290,7 +285,7 @@ def set_workflows_preferences(
 
         Args:
             collection (str): The collection the object belongs to
-            id (str): The id of the object in the collection
+            object_id (str): The id of the object in the collection
             preferences (dict): A dictionary of workflow preferences
             options (dict): A dictionary of options
 
@@ -299,15 +294,14 @@ def set_workflows_preferences(
         """
         preference_set_id = options.get('preference_set', default_set_id)
 
-        endpoint = '/objects/{}/{}/preferences/{}/workflows'.format(
-            collection, id, preference_set_id)
+        endpoint = f'/objects/{collection}/{object_id}/preferences/{preference_set_id}/workflows'
 
-        return self.client.request('put', endpoint, payload=preferences)
+        return await self.client.connection.request('put', endpoint, payload=preferences)
 
-    def set_workflow_preferences(
+    async def set_workflow_preferences(
             self,
             collection,
-            id,
+            object_id,
             key,
             setting,
             options={}):
@@ -316,7 +310,7 @@ def set_workflow_preferences(
 
         Args:
             collection (str): The collection the object belongs to
-            id (str): The id of the object in the collection
+            object_id (str): The id of the object in the collection
             key (str): The workflow key
             setting (boolean or dict): The preference setting
             options (dict): A dictionary of options
@@ -326,18 +320,17 @@ def set_workflow_preferences(
         """
         preference_set_id = options.get('preference_set', default_set_id)
 
-        endpoint = '/objects/{}/{}/preferences/{}/workflows/{}'.format(
-            collection, id, preference_set_id, key)
+        endpoint = f'/objects/{collection}/{object_id}/preferences/{preference_set_id}/workflows/{key}'
 
         params = setting if isinstance(setting, dict) else {
             'subscribed': setting}
 
-        return self.client.request('put', endpoint, payload=params)
+        return await self.client.connection.request('put', endpoint, payload=params)
 
-    def set_categories_preferences(
+    async def set_categories_preferences(
             self,
             collection,
-            id,
+            object_id,
             preferences,
             options={}):
         """
@@ -345,7 +338,7 @@ def set_categories_preferences(
 
         Args:
             collection (str): The collection the object belongs to
-            id (str): The id of the object in the collection
+            object_id (str): The id of the object in the collection
             preferences (dict): A dictionary of category preferences
             options (dict): A dictionary of options
 
@@ -354,15 +347,14 @@ def set_categories_preferences(
         """
         preference_set_id = options.get('preference_set', default_set_id)
 
-        endpoint = '/objects/{}/{}/preferences/{}/categories'.format(
-            collection, id, preference_set_id)
+        endpoint = f'/objects/{collection}/{object_id}/preferences/{preference_set_id}/categories'
 
-        return self.client.request('put', endpoint, payload=preferences)
+        return await self.client.connection.request('put', endpoint, payload=preferences)
 
-    def set_category_preferences(
+    async def set_category_preferences(
             self,
             collection,
-            id,
+            object_id,
             key,
             setting,
             options={}):
@@ -371,7 +363,7 @@ def set_category_preferences(
 
         Args:
             collection (str): The collection the object belongs to
-            id (str): The id of the object in the collection
+            object_id (str): The id of the object in the collection
             key (str): The category key
             setting (boolean or dict): The preference setting
             options (dict): A dictionary of options
@@ -381,10 +373,9 @@ def set_category_preferences(
         """
         preference_set_id = options.get('preference_set', default_set_id)
 
-        endpoint = '/objects/{}/{}/preferences/{}/categories/{}'.format(
-            collection, id, preference_set_id, key)
+        endpoint = f'/objects/{collection}/{object_id}/preferences/{preference_set_id}/categories/{key}'
 
         params = setting if isinstance(setting, dict) else {
             'subscribed': setting}
 
-        return self.client.request('put', endpoint, payload=params)
+        return await self.client.connection.request('put', endpoint, payload=params)
diff --git a/knockapi/async_client/services/preferences.py b/knockapi/async_client/services/preferences.py
new file mode 100644
index 0000000..b987749
--- /dev/null
+++ b/knockapi/async_client/services/preferences.py
@@ -0,0 +1,71 @@
+from knockapi.core.Service import Service
+from warnings import warn
+
+
+class Preferences(Service):
+
+    async def get_all(self, user_id):
+        warn("This method is deprecated. Use users.get_all_preferences instead.", DeprecationWarning, stacklevel=2)
+        return await self.client.users.get_all_preferences(user_id)
+
+    async def get(self, user_id, options={}):
+        warn("This method is deprecated. Use users.get_preferences instead.", DeprecationWarning, stacklevel=2)
+        return await self.client.users.get_preferences(user_id, options)
+
+    async def update(self, user_id, channel_types=None, categories=None, workflows=None, options={}):
+        warn("This method is deprecated. Use users.set_preferences instead.", DeprecationWarning, stacklevel=2)
+        return await self.client.users.set_preferences(
+            user_id,
+            channel_types=channel_types,
+            categories=categories,
+            workflows=workflows,
+            options=options
+        )
+
+    async def set_channel_types(self, user_id, preferences, options={}):
+        warn(
+            "This method is deprecated. Use users.set_channel_types_preferences instead.",
+            DeprecationWarning,
+            stacklevel=2
+        )
+        return await self.client.users.set_channel_types_preferences(user_id, preferences, options=options)
+
+    async def set_channel_type(self, user_id, channel_type, setting, options={}):
+        warn(
+            "This method is deprecated. Use users.set_channel_type_preferences instead.",
+            DeprecationWarning,
+            stacklevel=2
+        )
+        return await self.client.users.set_channel_types_preferences(user_id, channel_type, setting, options=options)
+
+    async def set_workflows(self, user_id, preferences, options={}):
+        warn(
+            "This method is deprecated. Use users.set_workflows_preferences instead.",
+            DeprecationWarning,
+            stacklevel=2
+        )
+        return await self.client.users.set_workflows_preferences(user_id, preferences, options=options)
+
+    async def set_workflow(self, user_id, key, setting, options={}):
+        warn(
+            "This method is deprecated. Use users.set_workflow_preferences instead.",
+            DeprecationWarning,
+            stacklevel=2
+        )
+        return await self.client.users.set_workflow_preferences(user_id, key, setting, setting, options=options)
+
+    async def set_categories(self, user_id, preferences, options={}):
+        warn(
+            "This method is deprecated. Use users.set_categories_preferences instead.",
+            DeprecationWarning,
+            stacklevel=2
+        )
+        return await self.client.users.set_categories_preferences(user_id, preferences, options=options)
+
+    async def set_category(self, user_id, key, setting, options={}):
+        warn(
+            "This method is deprecated. Use users.set_category_preferences instead.",
+            DeprecationWarning,
+            stacklevel=2
+        )
+        return await self.client.users.set_category_preferences(user_id, key, setting, setting, options=options)
diff --git a/knockapi/async_client/services/tenants.py b/knockapi/async_client/services/tenants.py
new file mode 100644
index 0000000..10789e6
--- /dev/null
+++ b/knockapi/async_client/services/tenants.py
@@ -0,0 +1,58 @@
+from knockapi.core.Service import Service
+
+
+class Tenants(Service):
+
+    async def list(self):
+        """
+        Returns all tenants in the environment
+
+        Args:
+            None
+
+        Returns:
+            dict: A Knock Tenant
+        """
+        endpoint = '/tenants'
+        return await self.client.connection.request('get', endpoint)
+
+    async def get(self, tenant_id):
+        """
+        Returns a tenant with the id given.
+
+        Args:
+            tenant_id (str): The id of the tenant
+
+        Returns:
+            dict: A Knock Tenant
+        """
+        endpoint = f'/tenants/{tenant_id}'
+        return await self.client.connection.request('get', endpoint)
+
+    # NOTE: This is `set_tenant` as `set` is a reserved keyword
+    async def set_tenant(self, tenant_id, tenant_data={}):
+        """
+        Returns a tenant with the id given and updated settings.
+
+        Args:
+            tenant_id (str): The id of the tenant
+            tenant_data (dict): The data to set on the tenant
+
+        Returns:
+            dict: A Knock Tenant
+        """
+        endpoint = f'/tenants/{tenant_id}'
+        return await self.client.connection.request('put', endpoint, payload=tenant_data)
+
+    async def delete(self, tenant_id):
+        """
+        Deletes the given tenant.
+
+        Args:
+            tenant_id (str): The id of the tenant
+
+        Returns:
+            None: No response
+        """
+        endpoint = f'/tenants/{tenant_id}'
+        return await self.client.connection.request('delete', endpoint)
diff --git a/knockapi/resources/users.py b/knockapi/async_client/services/users.py
similarity index 60%
rename from knockapi/resources/users.py
rename to knockapi/async_client/services/users.py
index 7d381c3..0c70ab3 100644
--- a/knockapi/resources/users.py
+++ b/knockapi/async_client/services/users.py
@@ -1,44 +1,44 @@
 import json
-from .service import Service
+from knockapi.core.Service import Service
 from warnings import warn
 
 default_set_id = "default"
 
 
 class User(Service):
-    def get_user(self, id):
-        warn("This method is deprecated. Use User.get instead.",
-             DeprecationWarning, stacklevel=2)
-        return self.get(id)
 
-    def get(self, id):
+    async def get_user(self, user_id):
+        warn("This method is deprecated. Use User.get instead.", DeprecationWarning, stacklevel=2)
+        return await self.get(user_id)
+
+    async def get(self, user_id):
         """
         Get a user by their id
 
         Args:
-            id: The user ID
+            user_id: The user ID
 
         Returns:
             dict: User response from Knock.
         """
-        endpoint = '/users/{}'.format(id)
-        return self.client.request('get', endpoint)
+        endpoint = f'/users/{user_id}'
+        return await self.client.connection.request('get', endpoint)
 
-    def identify(self, id, data={}):
+    async def identify(self, user_id, data={}):
         """
         Identify a user, upserting them
 
         Args:
-            id (str): The user ID
+            user_id (str): The user ID
             data (dict): Other properties to put on the user
 
         Returns:
             dict: User response from Knock.
         """
-        endpoint = '/users/{}'.format(id)
-        return self.client.request('put', endpoint, payload=data)
+        endpoint = f'/users/{user_id}'
+        return await self.client.connection.request('put', endpoint, payload=data)
 
-    def bulk_identify(self, users):
+    async def bulk_identify(self, users):
         """
         Bulk identifies up to 100 users
 
@@ -50,22 +50,22 @@ def bulk_identify(self, users):
         """
         endpoint = '/users/bulk/identify'
         data = {'users': users}
-        return self.client.request('post', endpoint, payload=data)
+        return await self.client.connection.request('post', endpoint, payload=data)
 
-    def delete(self, id):
+    async def delete(self, user_id):
         """
         Deletes the given user.
 
         Args:
-            id (str): The user ID
+            user_id (str): The user ID
 
         Returns:
             None: No response
         """
-        endpoint = '/users/{}'.format(id)
-        return self.client.request('delete', endpoint)
+        endpoint = f'/users/{user_id}'
+        return await self.client.connection.request('delete', endpoint)
 
-    def bulk_delete(self, user_ids):
+    async def bulk_delete(self, user_ids):
         """
         Bulk deletes up to 100 users.
 
@@ -77,9 +77,9 @@ def bulk_delete(self, user_ids):
         """
         endpoint = '/users/bulk/delete'
         data = {'user_ids': user_ids}
-        return self.client.request('post', endpoint, payload=data)
+        return await self.client.connection.request('post', endpoint, payload=data)
 
-    def get_feed(self, user_id, channel_id, options=None):
+    async def get_feed(self, user_id, channel_id, options=None):
         """
         Gets a feed for the given user
 
@@ -91,14 +91,14 @@ def get_feed(self, user_id, channel_id, options=None):
         Returns
             dict: A Knock feed response
         """
-        endpoint = '/users/{}/feeds/{}'.format(user_id, channel_id)
+        endpoint = f'/users/{user_id}/feeds/{channel_id}'
 
         if options and options['trigger_data']:
             options['trigger_data'] = json.dumps(options['trigger_data'])
 
-        return self.client.request('get', endpoint, payload=options)
+        return await self.client.connection.request('get', endpoint, payload=options)
 
-    def merge(self, user_id, from_user_id):
+    async def merge(self, user_id, from_user_id):
         """
         Merges the user specified with `from_user_id` into the user specified with `user_id`.
 
@@ -109,64 +109,62 @@ def merge(self, user_id, from_user_id):
         Returns
             dict: A Knock User response
         """
-        endpoint = '/users/{}/merge'.format(user_id)
+        endpoint = f'/users/{user_id}/merge'
         data = {'from_user_id': from_user_id}
-        return self.client.request('post', endpoint, payload=data)
+        return await self.client.connection.request('post', endpoint, payload=data)
 
     ##
     # Channel data
     ##
 
-    def get_channel_data(self, id, channel_id):
+    async def get_channel_data(self, user_id, channel_id):
         """
         Get user's channel data for the given channel id.
 
         Args:
-            id (str): The user ID
+            user_id (str): The user ID
             channel_id (str): Target channel ID
 
         Returns:
             dict: Channel data from Knock.
         """
-        endpoint = '/users/{}/channel_data/{}'.format(id, channel_id)
-        return self.client.request('get', endpoint)
+        endpoint = f'/users/{user_id}/channel_data/{channel_id}'
+        return await self.client.connection.request('get', endpoint)
 
-    def set_channel_data(self, id, channel_id, channel_data):
+    async def set_channel_data(self, user_id, channel_id, channel_data):
         """
         Upserts user's channel data for the given channel id.
 
         Args:
-            id (str): The user ID
+            user_id (str): The user ID
             channel_id (str): Target channel ID
             channel_data (dict): Channel data
 
         Returns:
             dict: Channel data from Knock.
         """
-        endpoint = '/users/{}/channel_data/{}'.format(id, channel_id)
-        return self.client.request(
-            'put', endpoint, payload={
-                'data': channel_data})
+        endpoint = f'/users/{user_id}/channel_data/{channel_id}'
+        return await self.client.connection.request('put', endpoint, payload={'data': channel_data})
 
-    def unset_channel_data(self, id, channel_id):
+    async def unset_channel_data(self, user_id, channel_id):
         """
         Unsets the user's channel data for the given channel id.
 
         Args:
-            id (str): The user ID
+            user_id (str): The user ID
             channel_id (str): Target channel ID
 
         Returns:
             None: no response
         """
-        endpoint = '/users/{}/channel_data/{}'.format(id, channel_id)
-        return self.client.request('delete', endpoint)
+        endpoint = f'/users/{user_id}/channel_data/{channel_id}'
+        return await self.client.connection.request('delete', endpoint)
 
     ##
     # Preferences
     ##
 
-    def get_all_preferences(self, user_id):
+    async def get_all_preferences(self, user_id):
         """
         Get a users full set of preferences
 
@@ -176,10 +174,10 @@ def get_all_preferences(self, user_id):
         Returns:
             dict: PreferenceSet response from Knock.
         """
-        endpoint = '/users/{}/preferences'.format(user_id)
-        return self.client.request('get', endpoint)
+        endpoint = f'/users/{user_id}/preferences'
+        return await self.client.connection.request('get', endpoint)
 
-    def get_preferences(self, user_id, options={}):
+    async def get_preferences(self, user_id, options={}):
         """
         Get a users preference set
 
@@ -192,18 +190,11 @@ def get_preferences(self, user_id, options={}):
             dict: PreferenceSet response from Knock.
         """
         preference_set_id = options.get('preference_set', default_set_id)
-        endpoint = '/users/{}/preferences/{}'.format(
-            user_id, preference_set_id)
+        endpoint = f'/users/{user_id}/preferences/{preference_set_id}'
 
-        return self.client.request('get', endpoint)
+        return await self.client.connection.request('get', endpoint)
 
-    def set_preferences(
-            self,
-            user_id,
-            channel_types=None,
-            categories=None,
-            workflows=None,
-            options={}):
+    async def set_preferences(self, user_id, channel_types=None, categories=None, workflows=None, options={}):
         """
         Sets the preference set for the user
 
@@ -219,8 +210,7 @@ def set_preferences(
         """
         preference_set_id = options.get('preference_set', default_set_id)
 
-        endpoint = '/users/{}/preferences/{}'.format(
-            user_id, preference_set_id)
+        endpoint = f'/users/{user_id}/preferences/{preference_set_id}'
 
         params = {
             'channel_types': channel_types,
@@ -228,9 +218,9 @@ def set_preferences(
             'workflows': workflows
         }
 
-        return self.client.request('put', endpoint, payload=params)
+        return await self.client.connection.request('put', endpoint, payload=params)
 
-    def bulk_set_preferences(self, user_ids=[], preferences={}, options={}):
+    async def bulk_set_preferences(self, user_ids=[], preferences={}, options={}):
         """
         Bulk sets the preference set for the users given
 
@@ -254,9 +244,9 @@ def bulk_set_preferences(self, user_ids=[], preferences={}, options={}):
             'user_ids': user_ids,
         }
 
-        return self.client.request('put', endpoint, payload=params)
+        return await self.client.connection.request('put', endpoint, payload=params)
 
-    def set_channel_types_preferences(self, user_id, preferences, options={}):
+    async def set_channel_types_preferences(self, user_id, preferences, options={}):
         """
         Sets the channel type preferences for the user
 
@@ -270,17 +260,11 @@ def set_channel_types_preferences(self, user_id, preferences, options={}):
         """
         preference_set_id = options.get('preference_set', default_set_id)
 
-        endpoint = '/users/{}/preferences/{}/channel_types'.format(
-            user_id, preference_set_id)
+        endpoint = f'/users/{user_id}/preferences/{preference_set_id}/channel_types'
 
-        return self.client.request('put', endpoint, payload=preferences)
+        return await self.client.connection.request('put', endpoint, payload=preferences)
 
-    def set_channel_type_preferences(
-            self,
-            user_id,
-            channel_type,
-            setting,
-            options={}):
+    async def set_channel_type_preferences(self, user_id, channel_type, setting, options={}):
         """
         Sets the channel type preference for the user
 
@@ -295,14 +279,11 @@ def set_channel_type_preferences(
         """
         preference_set_id = options.get('preference_set', default_set_id)
 
-        endpoint = '/users/{}/preferences/{}/channel_types/{}'.format(
-            user_id, preference_set_id, channel_type)
+        endpoint = f'/users/{user_id}/preferences/{preference_set_id}/channel_types/{channel_type}'
 
-        return self.client.request(
-            'put', endpoint, payload={
-                'subscribed': setting})
+        return await self.client.connection.request('put', endpoint, payload={'subscribed': setting})
 
-    def set_workflows_preferences(self, user_id, preferences, options={}):
+    async def set_workflows_preferences(self, user_id, preferences, options={}):
         """
         Sets the workflow preferences for the user
 
@@ -316,12 +297,11 @@ def set_workflows_preferences(self, user_id, preferences, options={}):
         """
         preference_set_id = options.get('preference_set', default_set_id)
 
-        endpoint = '/users/{}/preferences/{}/workflows'.format(
-            user_id, preference_set_id)
+        endpoint = f'/users/{user_id}/preferences/{preference_set_id}/workflows'
 
-        return self.client.request('put', endpoint, payload=preferences)
+        return await self.client.connection.request('put', endpoint, payload=preferences)
 
-    def set_workflow_preferences(self, user_id, key, setting, options={}):
+    async def set_workflow_preferences(self, user_id, key, setting, options={}):
         """
         Sets the workflow preferences for the user
 
@@ -336,15 +316,13 @@ def set_workflow_preferences(self, user_id, key, setting, options={}):
         """
         preference_set_id = options.get('preference_set', default_set_id)
 
-        endpoint = '/users/{}/preferences/{}/workflows/{}'.format(
-            user_id, preference_set_id, key)
+        endpoint = f'/users/{user_id}/preferences/{preference_set_id}/workflows/{key}'
 
-        params = setting if isinstance(setting, dict) else {
-            'subscribed': setting}
+        params = setting if isinstance(setting, dict) else {'subscribed': setting}
 
-        return self.client.request('put', endpoint, payload=params)
+        return await self.client.connection.request('put', endpoint, payload=params)
 
-    def set_categories_preferences(self, user_id, preferences, options={}):
+    async def set_categories_preferences(self, user_id, preferences, options={}):
         """
         Sets the categories preferences for the user
 
@@ -358,12 +336,11 @@ def set_categories_preferences(self, user_id, preferences, options={}):
         """
         preference_set_id = options.get('preference_set', default_set_id)
 
-        endpoint = '/users/{}/preferences/{}/categories'.format(
-            user_id, preference_set_id)
+        endpoint = f'/users/{user_id}/preferences/{preference_set_id}/categories'
 
-        return self.client.request('put', endpoint, payload=preferences)
+        return await self.client.connection.request('put', endpoint, payload=preferences)
 
-    def set_category_preferences(self, user_id, key, setting, options={}):
+    async def set_category_preferences(self, user_id, key, setting, options={}):
         """
         Sets the category preferences for the user
 
@@ -378,28 +355,26 @@ def set_category_preferences(self, user_id, key, setting, options={}):
         """
         preference_set_id = options.get('preference_set', default_set_id)
 
-        endpoint = '/users/{}/preferences/{}/categories/{}'.format(
-            user_id, preference_set_id, key)
+        endpoint = f'/users/{user_id}/preferences/{preference_set_id}/categories/{key}'
 
-        params = setting if isinstance(setting, dict) else {
-            'subscribed': setting}
+        params = setting if isinstance(setting, dict) else {'subscribed': setting}
 
-        return self.client.request('put', endpoint, payload=params)
+        return await self.client.connection.request('put', endpoint, payload=params)
 
     ##
     # Messages
     ##
 
-    def get_messages(self, id, options=None):
+    async def get_messages(self, user_id, options=None):
         """
         Get user's messages
 
         Args:
-            id (str): The user ID
+            user_id (str): The user ID
             options (dict): An optional set of filtering options to pass to the query
 
         Returns:
             dict: Paginated Message response.
         """
-        endpoint = '/users/{}/messages'.format(id)
-        return self.client.request('get', endpoint, payload=options)
+        endpoint = f'/users/{user_id}/messages'
+        return await self.client.connection.request('get', endpoint, payload=options)
diff --git a/knockapi/resources/workflows.py b/knockapi/async_client/services/workflows.py
similarity index 75%
rename from knockapi/resources/workflows.py
rename to knockapi/async_client/services/workflows.py
index c352d56..0a64b28 100644
--- a/knockapi/resources/workflows.py
+++ b/knockapi/async_client/services/workflows.py
@@ -1,15 +1,9 @@
-from .service import Service
+from knockapi.core.Service import Service
 
 
 class Workflows(Service):
-    def trigger(
-            self,
-            key,
-            recipients,
-            data={},
-            actor=None,
-            cancellation_key=None,
-            tenant=None):
+
+    async def trigger(self, key, recipients, data={}, actor=None, cancellation_key=None, tenant=None):
         """
         Triggers a workflow.
 
@@ -32,7 +26,7 @@ def trigger(
         Returns:
             dict: Response from Knock.
         """
-        endpoint = '/workflows/{}/trigger'.format(key)
+        endpoint = f'/workflows/{key}/trigger'
 
         params = {
             'actor': actor,
@@ -42,9 +36,9 @@ def trigger(
             'tenant': tenant
         }
 
-        return self.client.request("post", endpoint, payload=params)
+        return await self.client.connection.request("post", endpoint, payload=params)
 
-    def cancel(self, key, cancellation_key, recipients=None):
+    async def cancel(self, key, cancellation_key, recipients=None):
         """
         Cancels an in-flight workflow.
 
@@ -56,11 +50,11 @@ def cancel(self, key, cancellation_key, recipients=None):
         Returns:
             dict: Response from Knock.
         """
-        endpoint = '/workflows/{}/cancel'.format(key)
+        endpoint = f'/workflows/{key}/cancel'
 
         params = {
             'recipients': recipients,
             'cancellation_key': cancellation_key
         }
 
-        return self.client.request("post", endpoint, payload=params)
+        return await self.client.connection.request("post", endpoint, payload=params)
diff --git a/knockapi/core/AsyncConnection.py b/knockapi/core/AsyncConnection.py
new file mode 100644
index 0000000..668ce2d
--- /dev/null
+++ b/knockapi/core/AsyncConnection.py
@@ -0,0 +1,45 @@
+import aiohttp
+
+from json.decoder import JSONDecodeError
+
+
+class AsyncConnection(object):
+
+    def __init__(
+        self,
+        api_key: str,
+        api_host: str,
+        api_version: str,
+        read_timeout: int
+    ):
+        self.api_key = api_key
+        self.host = api_host
+        self.client_version = api_version
+        self.headers = {
+            'Authorization': 'Bearer {}'.format(self.api_key),
+            'User-Agent': 'Knock Python - {}'.format(self.client_version)
+        }
+        self.session = aiohttp.ClientSession(
+            raise_for_status=True,
+            headers=self.headers,
+            read_timeout=read_timeout
+        )
+
+    async def request(self, method, endpoint, payload=None):
+        url = '{}/v1{}'.format(self.host, endpoint)
+
+        r = await self.session.request(
+            method,
+            url,
+            params=payload if method == 'get' else None,
+            json=payload if method != 'get' else None
+        )
+
+        async with r:
+
+            # If we got a successful response, then attempt to deserialize as JSON
+            if r.ok:
+                try:
+                    return await r.json()
+                except JSONDecodeError:
+                    return None
diff --git a/knockapi/core/Connection.py b/knockapi/core/Connection.py
new file mode 100644
index 0000000..c5b5bc6
--- /dev/null
+++ b/knockapi/core/Connection.py
@@ -0,0 +1,43 @@
+import requests
+
+from json.decoder import JSONDecodeError
+
+
+class Connection(object):
+    def __init__(
+        self,
+        api_key: str,
+        api_host: str,
+        api_version: str,
+        read_timeout: int
+    ):
+        self.api_key = api_key
+        self.host = api_host
+        self.client_version = api_version
+        self.timeout = read_timeout
+        self.headers = {
+            'Authorization': 'Bearer {}'.format(self.api_key),
+            'User-Agent': 'Knock Python - {}'.format(self.client_version)
+        }
+        self.session = requests.Session()
+
+    def request(self, method, endpoint, payload=None):
+        url = '{}/v1{}'.format(self.host, endpoint)
+
+        r = self.session.request(
+            method,
+            url,
+            params=payload if method == 'get' else None,
+            json=payload if method != 'get' else None,
+            headers=self.headers,
+            timeout=self.timeout
+        )
+
+        # If we got a successful response, then attempt to deserialize as JSON
+        if r.ok:
+            try:
+                return r.json()
+            except JSONDecodeError:
+                return None
+
+        return r.raise_for_status()
diff --git a/knockapi/resources/service.py b/knockapi/core/Service.py
similarity index 100%
rename from knockapi/resources/service.py
rename to knockapi/core/Service.py
diff --git a/knockapi/core/__init__.py b/knockapi/core/__init__.py
new file mode 100644
index 0000000..e1aafc1
--- /dev/null
+++ b/knockapi/core/__init__.py
@@ -0,0 +1,3 @@
+from .Service import Service
+from .AsyncConnection import AsyncConnection
+from .Connection import Connection
diff --git a/knockapi/resources/bulk_operations.py b/knockapi/resources/bulk_operations.py
deleted file mode 100644
index a04927f..0000000
--- a/knockapi/resources/bulk_operations.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from .service import Service
-
-default_set_id = "default"
-
-
-class BulkOperations(Service):
-    def get(self, id):
-        """
-        Returns an bulk operation.
-
-        Args:
-            id (str): The id of the bulk operation
-
-        Returns:
-            dict: A Knock BulkOperation
-        """
-        endpoint = '/bulk_operations/{}'.format(id)
-        return self.client.request('get', endpoint)
diff --git a/knockapi/resources/preferences.py b/knockapi/resources/preferences.py
deleted file mode 100644
index ed91203..0000000
--- a/knockapi/resources/preferences.py
+++ /dev/null
@@ -1,84 +0,0 @@
-from .service import Service
-from warnings import warn
-
-
-class Preferences(Service):
-    def get_all(self, user_id):
-        warn(
-            "This method is deprecated. Use users.get_all_preferences instead.",
-            DeprecationWarning,
-            stacklevel=2)
-        return self.client.users.get_all_preferences(user_id)
-
-    def get(self, user_id, options={}):
-        warn(
-            "This method is deprecated. Use users.get_preferences instead.",
-            DeprecationWarning,
-            stacklevel=2)
-        return self.client.users.get_preferences(user_id, options)
-
-    def update(
-            self,
-            user_id,
-            channel_types=None,
-            categories=None,
-            workflows=None,
-            options={}):
-        warn(
-            "This method is deprecated. Use users.set_preferences instead.",
-            DeprecationWarning,
-            stacklevel=2)
-        return self.client.users.set_preferences(
-            user_id,
-            channel_types=channel_types,
-            categories=categories,
-            workflows=workflows,
-            options=options)
-
-    def set_channel_types(self, user_id, preferences, options={}):
-        warn(
-            "This method is deprecated. Use users.set_channel_types_preferences instead.",
-            DeprecationWarning,
-            stacklevel=2)
-        return self.client.users.set_channel_types_preferences(
-            user_id, preferences, options=options)
-
-    def set_channel_type(self, user_id, channel_type, setting, options={}):
-        warn(
-            "This method is deprecated. Use users.set_channel_type_preferences instead.",
-            DeprecationWarning,
-            stacklevel=2)
-        return self.client.users.set_channel_types_preferences(
-            user_id, channel_type, setting, options=options)
-
-    def set_workflows(self, user_id, preferences, options={}):
-        warn(
-            "This method is deprecated. Use users.set_workflows_preferences instead.",
-            DeprecationWarning,
-            stacklevel=2)
-        return self.client.users.set_workflows_preferences(
-            user_id, preferences, options=options)
-
-    def set_workflow(self, user_id, key, setting, options={}):
-        warn(
-            "This method is deprecated. Use users.set_workflow_preferences instead.",
-            DeprecationWarning,
-            stacklevel=2)
-        return self.client.users.set_workflow_preferences(
-            user_id, key, setting, setting, options=options)
-
-    def set_categories(self, user_id, preferences, options={}):
-        warn(
-            "This method is deprecated. Use users.set_categories_preferences instead.",
-            DeprecationWarning,
-            stacklevel=2)
-        return self.client.users.set_categories_preferences(
-            user_id, preferences, options=options)
-
-    def set_category(self, user_id, key, setting, options={}):
-        warn(
-            "This method is deprecated. Use users.set_category_preferences instead.",
-            DeprecationWarning,
-            stacklevel=2)
-        return self.client.users.set_category_preferences(
-            user_id, key, setting, setting, options=options)
diff --git a/knockapi/resources/tenants.py b/knockapi/resources/tenants.py
deleted file mode 100644
index 24790a3..0000000
--- a/knockapi/resources/tenants.py
+++ /dev/null
@@ -1,57 +0,0 @@
-from .service import Service
-
-
-class Tenants(Service):
-    def list(self):
-        """
-        Returns all tenants in the environment
-
-        Args:
-            None
-
-        Returns:
-            dict: A Knock Tenant
-        """
-        endpoint = '/tenants'
-        return self.client.request('get', endpoint)
-
-    def get(self, id):
-        """
-        Returns a tenant with the id given.
-
-        Args:
-            id (str): The id of the tenant
-
-        Returns:
-            dict: A Knock Tenant
-        """
-        endpoint = '/tenants/{}'.format(id)
-        return self.client.request('get', endpoint)
-
-    # NOTE: This is `set_tenant` as `set` is a reserved keyword
-    def set_tenant(self, id, tenant_data={}):
-        """
-        Returns a tenant with the id given and updated settings.
-
-        Args:
-            id (str): The id of the tenant
-            tenant_data (dict): The data to set on the tenant
-
-        Returns:
-            dict: A Knock Tenant
-        """
-        endpoint = '/tenants/{}'.format(id)
-        return self.client.request('put', endpoint, payload=tenant_data)
-
-    def delete(self, id):
-        """
-        Deletes the given tenant.
-
-        Args:
-            id (str): The id of the tenant
-
-        Returns:
-            None: No response
-        """
-        endpoint = '/tenants/{}'.format(id)
-        return self.client.request('delete', endpoint)
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..50df01b
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["setuptools>=62.3.0", "wheel", "unasync"]
+build-backend = "setuptools.build_meta"
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..a635c5c
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,2 @@
+[pytest]
+pythonpath = .
diff --git a/setup.py b/setup.py
index 8b28bfc..b324293 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,7 @@
 import setuptools
+import unasync
 
-version = '0.5.2'
+version = '0.6.0'
 
 with open("README.md", "r") as f:
     long_description = f.read()
@@ -8,9 +9,11 @@
 setuptools.setup(
     name='knockapi',
     version=version,
-    python_requires='>=2.7.16, <4',
+    python_requires='>=3.6, <4',
     install_requires=[
-        'requests'
+        'requests',
+        'aiohttp',
+        'unasync'
     ],
     extras_require={
         'dev': [
@@ -38,5 +41,13 @@
     packages=setuptools.find_packages(),
     author='Knock Labs, Inc.',
     author_email='support@knock.app',
-    license='MIT'
+    license='MIT',
+    cmdclass={"build_py": unasync.cmdclass_build_py(
+        rules=[
+            unasync.Rule("knockapi/async_client", "knockapi/sync_client", additional_replacements={
+                "AsyncConnection": "Connection",
+                "async_client": "sync_client",
+            })
+        ]
+    )},
 )
diff --git a/tests/test_tenants_class.py b/tests/test_tenants_class.py
index 0c06f63..8a00d6b 100644
--- a/tests/test_tenants_class.py
+++ b/tests/test_tenants_class.py
@@ -1,34 +1,37 @@
-from unittest.mock import Mock
+import pytest
 
-from knockapi.resources.tenants import Tenants
+from unittest.mock import AsyncMock
+from knockapi.async_client.services.tenants import Tenants
 
-# Tests
 
-
-def test_list():
-    mocked_client = Mock()
+@pytest.mark.asyncio
+async def test_list():
+    mocked_client = AsyncMock()
     tenants = Tenants(mocked_client)
-    tenants.list()
-    mocked_client.request.assert_called_with("get", "/tenants")
+    await tenants.list()
+    mocked_client.connection.request.assert_awaited_with("get", "/tenants")
 
 
-def test_get():
-    mocked_client = Mock()
+@pytest.mark.asyncio
+async def test_get():
+    mocked_client = AsyncMock()
     tenants = Tenants(mocked_client)
-    tenants.get("tenant-123")
-    mocked_client.request.assert_called_with("get", "/tenants/tenant-123")
+    await tenants.get("tenant-123")
+    mocked_client.connection.request.assert_awaited_with("get", "/tenants/tenant-123")
 
 
-def test_delete():
-    mocked_client = Mock()
+@pytest.mark.asyncio
+async def test_delete():
+    mocked_client = AsyncMock()
     tenants = Tenants(mocked_client)
-    tenants.delete("tenant-123")
-    mocked_client.request.assert_called_with("delete", "/tenants/tenant-123")
+    await tenants.delete("tenant-123")
+    mocked_client.connection.request.assert_awaited_with("delete", "/tenants/tenant-123")
 
 
-def test_set():
-    mocked_client = Mock()
+@pytest.mark.asyncio
+async def test_set():
+    mocked_client = AsyncMock()
     tenants = Tenants(mocked_client)
-    tenants.set_tenant("tenant-123", {"name": "Tenant 1"})
-    mocked_client.request.assert_called_with(
+    await tenants.set_tenant("tenant-123", {"name": "Tenant 1"})
+    mocked_client.connection.request.assert_awaited_with(
         "put", "/tenants/tenant-123", payload={'name': 'Tenant 1'})