diff --git a/bots_api/bot_lib.py b/bots_api/bot_lib.py
index 6b124b0a..955afcc5 100644
--- a/bots_api/bot_lib.py
+++ b/bots_api/bot_lib.py
@@ -6,6 +6,8 @@ import signal
 import sys
 import time
 import re
+from zerver.lib.actions import internal_send_message
+from zerver.models import UserProfile
 
 from six.moves import configparser
 
@@ -50,6 +52,22 @@ class RateLimit(object):
         logging.error(self.error_message)
         sys.exit(1)
 
+def send_reply(message, response, email, send_message):
+    # type: (Dict[str, Any], str, str, Any) -> Dict[str, Any]
+    if message['type'] == 'private':
+        return send_message(dict(
+            type='private',
+            to=[x['email'] for x in message['display_recipient'] if email != x['email']],
+            content=response,
+        ))
+    else:
+        return send_message(dict(
+            type='stream',
+            to=message['display_recipient'],
+            subject=message['subject'],
+            content=response,
+        ))
+
 
 class ExternalBotHandler(object):
     def __init__(self, client):
@@ -73,6 +91,10 @@ class ExternalBotHandler(object):
         else:
             self._rate_limit.show_error_and_exit()
 
+    def send_reply(self, message, response):
+        # type: (Dict[str, Any], str) -> Dict[str, Any]
+        return send_reply(message, response, self.email, self.send_message)
+
     def update_message(self, message):
         # type: (Dict[str, Any]) -> Dict[str, Any]
         if self._rate_limit.is_legal():
@@ -80,21 +102,41 @@ class ExternalBotHandler(object):
         else:
             self._rate_limit.show_error_and_exit()
 
-    def send_reply(self, message, response):
-        # type: (Dict[str, Any], str) -> Dict[str, Any]
-        if message['type'] == 'private':
-            return self.send_message(dict(
-                type='private',
-                to=[x['email'] for x in message['display_recipient'] if self.email != x['email']],
-                content=response,
-            ))
+    def get_config_info(self, bot_name, section=None):
+        # type: (str, Optional[str]) -> Dict[str, Any]
+        conf_file_path = os.path.realpath(os.path.join(
+            our_dir, '..', 'bots', bot_name, bot_name + '.conf'))
+        section = section or bot_name
+        config = configparser.ConfigParser()
+        config.readfp(open(conf_file_path))  # type: ignore
+        return dict(config.items(section))
+
+class EmbeddedBotHandler(object):
+    def __init__(self, user_profile):
+        # type: (UserProfile) -> None
+        # Only expose a subset of our UserProfile's functionality
+        self.user_profile = user_profile
+        self._rate_limit = RateLimit(20, 5)
+        try:
+            self.full_name = user_profile['full_name']
+            self.email = user_profile['email']
+        except KeyError:
+            logging.error('Cannot fetch user profile, make sure you have set'
+                          ' up the zuliprc file correctly.')
+            sys.exit(1)
+
+    def send_message(self, message):
+        # type: (Dict[str, Any]) -> None
+        if self._rate_limit.is_legal():
+            internal_send_message(realm=self.user_profile.realm, sender_email=message['sender'],
+                                  recipient_type_name=message['type'], recipients=message['to'],
+                                  subject=message['subject'], content=message['content'])
         else:
-            return self.send_message(dict(
-                type='stream',
-                to=message['display_recipient'],
-                subject=message['subject'],
-                content=response,
-            ))
+            self._rate_limit.show_error_and_exit()
+
+    def send_reply(self, message, response):
+        # type: (Dict[str, Any], str) -> None
+        send_reply(message, response, self.email, self.send_message)
 
     def get_config_info(self, bot_name, section=None):
         # type: (str, Optional[str]) -> Dict[str, Any]