diff --git a/config/install/sms.gateway.log.yml b/config/install/sms.gateway.log.yml
new file mode 100644
index 0000000..7de765f
--- /dev/null
+++ b/config/install/sms.gateway.log.yml
@@ -0,0 +1,3 @@
+# Default configuration for log gateway.
+enabled: true
+plugin_id: log
diff --git a/config/schema/sms.schema.yml b/config/schema/sms.schema.yml
index e0b1a68..9979935 100644
--- a/config/schema/sms.schema.yml
+++ b/config/schema/sms.schema.yml
@@ -22,9 +22,22 @@ sms.settings:
 # Configuration for sms gateways.
 sms.gateway.*:
   type: config_object
-  label: 'SMS Gateway settings'
+  label: 'SMS Gateway configuration'
   mapping:
-    settings:
+    label:
+      type: label
+      label: 'Human readable gateway label'
+    name:
+      type: string
+      label: 'Gateway machine name'
+    plugin_id:
+      type: string
+      label: 'Gateway plugin type'
+    enabled:
+      type: boolean
+      label: 'Gateway enabled status'
+    custom:
       type: sequence
+      label: 'Additional gateway-specific configuration'
       sequence:
         - type: string
diff --git a/modules/sms_blast/src/Tests/SmsBlastWebTest.php b/modules/sms_blast/src/Tests/SmsBlastWebTest.php
index b4df3b1..ee48f26 100644
--- a/modules/sms_blast/src/Tests/SmsBlastWebTest.php
+++ b/modules/sms_blast/src/Tests/SmsBlastWebTest.php
@@ -7,23 +7,23 @@
 
 namespace Drupal\sms_blast\Tests;
 
-use \Drupal\simpletest\WebTestBase;
+use Drupal\sms\Tests\SmsFrameworkWebTestBase;
 
 /**
  * Integration tests for the sms_blast module.
  *
  * @group SMS Framework
  */
-class SmsBlastWebTest extends WebTestBase {
+class SmsBlastWebTest extends SmsFrameworkWebTestBase {
 
-  public static $modules = ['sms', 'sms_test_gateway', 'sms_user', 'sms_blast'];
+  public static $modules = ['sms_user', 'sms_blast'];
 
   /**
    * Tests sending sms blast.
    */
   function testSendBlast() {
     // Set up test default gateway and test user.
-    $this->container->get('config.factory')->getEditable('sms.settings')->set('default_gateway', 'test')->save();
+    $this->setDefaultGateway('test');
     $user = $this->drupalCreateUser(array('receive sms', 'Send SMS Blast'));
     $this->drupalLogin($user);
     $data = array(
diff --git a/modules/sms_devel/src/Tests/SmsDevelWebTest.php b/modules/sms_devel/src/Tests/SmsDevelWebTest.php
index acd6567..081b64b 100644
--- a/modules/sms_devel/src/Tests/SmsDevelWebTest.php
+++ b/modules/sms_devel/src/Tests/SmsDevelWebTest.php
@@ -7,16 +7,16 @@
 
 namespace Drupal\sms_devel\Tests;
 
-use Drupal\simpletest\WebTestBase;
+use Drupal\sms\Tests\SmsFrameworkWebTestBase;
 
 /**
  * Tests the send/receive form provided by SMS Devel.
  *
  * @group SMS Framework
  */
-class SmsDevelWebTest extends WebTestBase {
+class SmsDevelWebTest extends SmsFrameworkWebTestBase {
 
-  public static $modules = ['sms', 'sms_test_gateway', 'sms_devel'];
+  public static $modules = ['sms_devel'];
 
   /**
    * Tests if messages sent using the test send form are stored properly.
@@ -28,7 +28,7 @@ class SmsDevelWebTest extends WebTestBase {
     $this->drupalLogin($user);
 
     // Set up test default gateway.
-    $this->config('sms.settings')->set('default_gateway', 'test')->save();
+    $this->setDefaultGateway('test');
 
     $test_message1 = array(
       'number' => '1234567890',
diff --git a/modules/sms_sendtophone/sms_sendtophone.module b/modules/sms_sendtophone/sms_sendtophone.module
index d4a01e5..f071090 100644
--- a/modules/sms_sendtophone/sms_sendtophone.module
+++ b/modules/sms_sendtophone/sms_sendtophone.module
@@ -429,7 +429,7 @@ function sms_sendtophone_form_validate($form, &$form_state) {
  * @see http://drupal.org/node/1354
  */
 function sms_sendtophone_form_submit($form, &$form_state) {
-  if (sms_send($form_state['values']['number'], $form_state['values']['message'], $form_state['values']['gateway'])) {
+  if (sms_send($form_state['values']['number'], $form_state['values']['message'], (array) $form_state['values']['gateway'])) {
     drupal_set_message(t('The message "@message" sent to @number.', array('@message' => $form_state['values']['message'], '@number' => $form_state['values']['number'])));
   }
 }
diff --git a/modules/sms_track/sms_track.module b/modules/sms_track/sms_track.module
index 29cc6ca..6afe938 100644
--- a/modules/sms_track/sms_track.module
+++ b/modules/sms_track/sms_track.module
@@ -8,6 +8,7 @@
  */
 
 use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\sms\Gateway\GatewayInterface;
 use Drupal\sms\Message\SmsMessageInterface;
 use Drupal\views\Plugin\views\query\QueryPluginBase;
 use \Drupal\views\ViewExecutable;
@@ -43,7 +44,7 @@ function sms_track_cron() {
 /**
  * Implements hook_sms_send().
  */
-function sms_track_sms_send(SmsMessageInterface $sms, array $options, $gateway) {
+function sms_track_sms_send(SmsMessageInterface $sms, array $options, GatewayInterface $gateway) {
   // Tracking.
   // Place a tracking reference on a sent message if we need to.
   if (!empty($options) && !array_key_exists('reference', $options)) {
@@ -60,17 +61,17 @@ function sms_track_sms_send(SmsMessageInterface $sms, array $options, $gateway)
  *   The SMS message object.
  * @param array $options
  *   Additional options array including sender.
- * @param array $gateway
+ * @param \Drupal\sms\Gateway\GatewayInterface $gateway
  *   Gateway array for the active gateway.
- * @param array $result
- *   Result array from the gateway response handler.
+ * @param bool|array $result
+ *   The result from the gateway response handler.
  */
-function sms_track_sms_send_process($op, SmsMessageInterface $sms, array $options, $gateway, $result) {
+function sms_track_sms_send_process($op, SmsMessageInterface $sms, array $options, GatewayInterface $gateway, $result) {
   if ($op == 'post process') {
     // Archiving (outgoing == 0)
     $dir = 0;
     $options = (isset($options) && is_array($options))? $options : array();
-    $options['gateway_id'] = $gateway['identifier'];
+    $options['gateway_id'] = $gateway->getIdentifier();
     $options['result']  = $result;
     sms_track_archive_write($dir, implode(',', $sms->getRecipients()), $sms->getMessage(), $options);
   }
@@ -112,7 +113,7 @@ function sms_track_sms_incoming($op, SmsMessageInterface $sms, array $options) {
  * @param array $options
  *   Additional options array.
  */
-function sms_track_sms_receipt($op, $number, $reference, $status, $options) {
+function sms_track_sms_receipt($op, $number, $reference, $status, array $options) {
   if ($op == 'pre process') {
     // Tracking.
     sms_track_update_message($reference, $status);
@@ -132,7 +133,7 @@ function sms_track_sms_receipt($op, $number, $reference, $status, $options) {
  * @param array $options
  *   (optional) an array of additional options.
  */
-function sms_track_archive_write($dir, $number, $message, $options = array()) {
+function sms_track_archive_write($dir, $number, $message, array $options = array()) {
   $user = \Drupal::currentUser();
   $archive_dir = \Drupal::config('sms_track.settings')->get('archive_dir');
   // Query the database for the uid of the user that owns the $number
@@ -155,10 +156,10 @@ function sms_track_archive_write($dir, $number, $message, $options = array()) {
       // Or render a status code from a simple true/false result
       if (! $status) {
         if ($options['result']) {
-          $status = SMS_GW_OK;
+          $status = GatewayInterface::STATUS_OK;
         }
         else {
-          $status = SMS_GW_ERR_OTHER;
+          $status = GatewayInterface::STATUS_ERR_OTHER;
         }
       }
       // Add the author and recipient
@@ -174,7 +175,7 @@ function sms_track_archive_write($dir, $number, $message, $options = array()) {
     if ($archive_dir == SMS_DIR_ALL || $archive_dir == SMS_DIR_IN) {
       $reference = NULL;
       // Inbound message status is always the same.
-      $status = SMS_MSG_STATUS_OK;
+      $status = SmsMessageInterface::STATUS_OK;
       // Add the author and recipient
       $recipient = $user->id();
       $author = ($remote_user) ? $remote_user : 0;
diff --git a/modules/sms_user/sms_user.module b/modules/sms_user/sms_user.module
index a80f886..e228dee 100644
--- a/modules/sms_user/sms_user.module
+++ b/modules/sms_user/sms_user.module
@@ -8,6 +8,7 @@
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Url;
+use Drupal\sms\Gateway\GatewayInterface;
 use Drupal\sms\Message\SmsMessageInterface;
 use Drupal\user\Entity\User;
 use Drupal\user\UserInterface;
@@ -98,8 +99,10 @@ function sms_user_get_uid($number, $status = NULL) {
 
 /**
  * Implements hook_sms_send_process().
+ *
+ * Aborts sending if the user has opted out or is in sleep mode.
  */
-function sms_user_sms_send_process($step, SmsMessageInterface $sms, array $options, $gateway) {
+function sms_user_sms_send_process($step, SmsMessageInterface $sms, array $options, GatewayInterface $gateway) {
   if ($step == 'pre process') {
     if (\Drupal::config('sms_user.settings')->get('enable_sleep', 1) && $uid = sms_user_get_uid($sms->getRecipients()[0])) {
       $account = User::load($uid);
diff --git a/modules/sms_user/src/Tests/SmsUserWebTest.php b/modules/sms_user/src/Tests/SmsUserWebTest.php
index 8725bee..380c54c 100644
--- a/modules/sms_user/src/Tests/SmsUserWebTest.php
+++ b/modules/sms_user/src/Tests/SmsUserWebTest.php
@@ -7,7 +7,7 @@
 
 namespace Drupal\sms_user\Tests;
 
-use Drupal\simpletest\WebTestBase;
+use Drupal\sms\Tests\SmsFrameworkWebTestBase;
 use Drupal\user\Entity\User;
 
 /**
@@ -18,16 +18,16 @@ use Drupal\user\Entity\User;
  * @todo Add tests for creation of users via sms.
  * @todo Add tests for integration with rules and actions modules.
  */
-class SmsUserWebTest extends WebTestBase {
+class SmsUserWebTest extends SmsFrameworkWebTestBase {
 
-  public static $modules = ['sms', 'sms_test_gateway', 'sms_user', 'syslog', 'sms_devel'];
+  public static $modules = ['sms_user', 'syslog', 'sms_devel'];
 
   /**
    * Tests user adding phone number.
    */
   public function testNumberConfirmationAndSmsUserSend() {
     // Set up test default gateway.
-    $this->config('sms.settings')->set('default_gateway', 'test')->save();
+    $this->setDefaultGateway('test');
     $user = $this->drupalCreateUser(array('receive sms', 'edit own sms number'));
     $this->drupalLogin($user);
 
@@ -158,8 +158,7 @@ class SmsUserWebTest extends WebTestBase {
     $this->drupalLogin($excluded_user);
 
     // Set up test default gateway.
-    $this->config('sms.settings')->set('default_gateway', 'test')->save();
-
+    $this->setDefaultGateway('test');
     $sms_user_settings = array(
       'registration_enabled' => TRUE,
       'allow_password' => TRUE,
diff --git a/sms.links.action.yml b/sms.links.action.yml
index eec7b10..f4f6527 100644
--- a/sms.links.action.yml
+++ b/sms.links.action.yml
@@ -4,3 +4,10 @@ sms.carrier_add:
   weight: 1
   appears_on:
     - sms.carrier_admin
+
+sms.gateway_add:
+  title: 'Add Configurable Gateway'
+  route_name: sms.gateway_add
+  weight: 1
+  appears_on:
+    - sms.gateway_admin
diff --git a/sms.module b/sms.module
index 00c18d6..eaeb317 100644
--- a/sms.module
+++ b/sms.module
@@ -6,7 +6,9 @@
  * sending and receiving SMS messages.
  */
 
+use Drupal\Core\Form\FormState;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\sms\Gateway\GatewayInterface;
 use Drupal\sms\Message\SmsMessage;
 use Drupal\sms\Message\SmsMessageInterface;
 
@@ -26,20 +28,19 @@ define('SMS_MSG_STATUS_NOCREDIT',   SmsMessageInterface::STATUS_NO_CREDIT);
 define('SMS_MSG_STATUS_EXPIRED',    SmsMessageInterface::STATUS_EXPIRED);
 
 // Gateway response codes.
-// 0=Unknown, 2xx=Positive, 4xx=Negative(likely client err), 5xx=Negative(likely gateway err)
-define('SMS_GW_UNKNOWN_STATUS',      0);
-define('SMS_GW_OK',                200);
-define('SMS_GW_ERR_AUTH',          401);
-define('SMS_GW_ERR_INVALID_CALL',  400);
-define('SMS_GW_ERR_NOT_FOUND',     404);
-define('SMS_GW_ERR_MSG_LIMITS',    413);
-define('SMS_GW_ERR_MSG_ROUTING',   502);
-define('SMS_GW_ERR_MSG_QUEUING',   408);
-define('SMS_GW_ERR_MSG_OTHER',     409);
-define('SMS_GW_ERR_SRC_NUMBER',    415);
-define('SMS_GW_ERR_DEST_NUMBER',   416);
-define('SMS_GW_ERR_CREDIT',        402);
-define('SMS_GW_ERR_OTHER',         500);
+define('SMS_GW_UNKNOWN_STATUS',    GatewayInterface::STATUS_UNKNOWN);
+define('SMS_GW_OK',                GatewayInterface::STATUS_OK);
+define('SMS_GW_ERR_AUTH',          GatewayInterface::STATUS_ERR_AUTH);
+define('SMS_GW_ERR_INVALID_CALL',  GatewayInterface::STATUS_ERR_INVALID_CALL);
+define('SMS_GW_ERR_NOT_FOUND',     GatewayInterface::STATUS_ERR_NOT_FOUND);
+define('SMS_GW_ERR_MSG_LIMITS',    GatewayInterface::STATUS_ERR_MSG_LIMITS);
+define('SMS_GW_ERR_MSG_ROUTING',   GatewayInterface::STATUS_ERR_MSG_ROUTING);
+define('SMS_GW_ERR_MSG_QUEUING',   GatewayInterface::STATUS_ERR_MSG_QUEUING);
+define('SMS_GW_ERR_MSG_OTHER',     GatewayInterface::STATUS_ERR_MSG_OTHER);
+define('SMS_GW_ERR_SRC_NUMBER',    GatewayInterface::STATUS_ERR_SRC_NUMBER);
+define('SMS_GW_ERR_DEST_NUMBER',   GatewayInterface::STATUS_ERR_DEST_NUMBER);
+define('SMS_GW_ERR_CREDIT',        GatewayInterface::STATUS_ERR_CREDIT);
+define('SMS_GW_ERR_OTHER',         GatewayInterface::STATUS_ERR_OTHER);
 
 // Carrier status.
 define('SMS_CARRIER_DEFAULT', 0);
@@ -128,148 +129,78 @@ function sms_incoming($number, $message, array $options = array()) {
 /**
  * Handles responses to message transactions.
  *
- * Allows gateways modules to pass message receipts and other responses to
- * messages in a standard format for processing, and provides a basic set of
- * status codes for common code handling.
- *
- * Allowed message status codes are defined as constants in this module.
- *
- * The gateway status code and message should be provided in the $options array
- * as 'gateway_message_status' and 'gateway_message_status_text'.
- *
- * @param string $number
- *   The sender's mobile number.
- * @param string $reference
- *   Unique message reference code, as provided when message is sent.
- * @param int $message_status
- *   (optional) An SMS Framework message status code, per the defined constants.
- *    Defaults to SMS_GW_UNKNOWN_STATUS.
- * @param array $options
- *   (optional) Extended options passed by the receipt receiver.
+ * @see \Drupal\sms\Gateway\SmsProviderInterface::receipt
  *
  * @deprecated use \Drupal\sms\Provider\SmsProviderInterface::receipt() instead.
  */
-function sms_receipt($number, $reference, $message_status = SMS_GW_UNKNOWN_STATUS, $options = array()) {
+function sms_receipt($number, $reference, $message_status = GatewayInterface::STATUS_UNKNOWN, $options = array()) {
   \Drupal::service('sms_provider.default')->receipt($number, $reference, $message_status, $options);
 }
 
 /**
  * Returns the current default gateway.
+ *
+ * @return \Drupal\sms\Gateway\GatewayInterface
  */
 function sms_default_gateway() {
-  return sms_gateways('gateway', sms_default_gateway_id());
+  return \Drupal::service('plugin.manager.sms_gateway')->getDefaultGateway();
 }
 
 /**
- * Returns the current default gateway machine name.
+ * Returns the identifier for the current default gateway.
+ *
+ * @return string
  */
 function sms_default_gateway_id() {
   return \Drupal::config('sms.settings')->get('default_gateway');
 }
 
 /**
- * Implements hook_gateway_info().
- */
-function sms_gateway_info() {
-  return array(
-    'log' => array(
-      'name' => t('Log only'),
-      'send' => 'sms_send_log',
-    ),
-  );
-}
-
-/**
- * Logs sms message.
- *
- * This is a rudimentary implementation of an sms gateway by simply logging the
- * message to watchdog.
- *
- * @param \Drupal\sms\Message\SmsMessageInterface $sms
- *   The message that was sent.
- * @param array $options
- *   An associative array of options passed to gateway.
- *
- * @return array
- *   An array containing one key:
- *   - status: true.
- */
-function sms_send_log(SmsMessageInterface $sms, $options) {
-  \Drupal::logger('sms')->info('SMS message sent to %number with the text: @message', array('%number' => implode(',', $sms->getRecipients()), '@message' => $sms->getMessage()));
-  return array('status' => TRUE);
-}
-
-
-/**
- * SMS gateway menutitle callback.
- */
-function sms_admin_gateway_title($gateway_id) {
-  $gateway = sms_gateways('gateway', $gateway_id);
-  return sprintf('%s gateway', $gateway['name']);
-}
-
-/**
  * Gets a list of all gateways.
  *
  * @param string $op
- *   (optional) The format in which to return the list. When set to 'gateway' or 'name',
- *   only the specified gateway is returned. When set to 'gateways' or 'names',
- *   all gateways are returned. Defaults to 'gateways'.
+ *   (optional) The format in which to return the list. When set to 'gateway' or
+ *   'name', only the specified gateway is returned. When set to 'gateways' or
+ *   'names', all gateways are returned. Defaults to 'gateways'.
+ *
  * @param string $gateway
  *   (optional) A gateway identifier string that indicates the gateway to return.
  *   Leave at default value (NULL) to return all gateways.
  *
- * @return array|string|NULL
+ * @return array|\Drupal\sms\Gateway\GatewayInterface
  *   Either an array of all gateways or a single gateway, in a variable format.
+ *
+ * @deprecated as of 8.x-1.x. Use @code GatewayManager @endcode methods instead.
+ * @code
+ *   Drupal::service('plugin.manager.sms_gateway')->getAvailableGateways();
+ *   Drupal::service('plugin.manager.sms_gateway')->getEnabledGateways();
+ *   Drupal::service('plugin.manager.sms_gateway')->getDefaultGateway();
+ *   Drupal::service('plugin.manager.sms_gateway')->getGateway($name);
+ * @endcode
  */
-function sms_gateways($op = 'gateways', $gateway = NULL) {
-  list($_gateways, $_names) = _gateways_build();
+function sms_gateways($op = 'gateways', $gateway_id = NULL) {
+  /** @var \Drupal\sms\Gateway\GatewayManagerInterface $gateway_manager */
+  $gateway_manager = \Drupal::service('plugin.manager.sms_gateway');
 
   switch ($op) {
     case 'gateway':
-      if (isset($_gateways[$gateway])) {
-        $return = $_gateways[$gateway];
-        $return['identifier'] = $gateway;
-        return $return;
-      }
-      else {
-        return NULL;
-      }
+      return $gateway_manager->getGateway($gateway_id) ? : array();
     case 'names':
+      $_names = array();
+      foreach ($gateway_manager->getAvailableGateways() as $id => $gateway) {
+        $_names[$id] = $gateway->getLabel();
+      }
       return $_names;
     case 'name':
-      return isset($_names[$gateway]) ? $_names[$gateway] : NULL;
+      $gateway = $gateway_manager->getGateway($gateway_id);
+      return ($gateway) ? $gateway->getLabel() : '';
     case 'gateways':
     default:
-      return $_gateways;
+      return $gateway_manager->getAvailableGateways();
   }
 }
 
 /**
- * Helper function to get gateway definitions in hook_gateway_info.
- *
- * @return array
- *   A array of gateway definitions from hook_gateway_info().
- */
-function _gateways_build() {
-  // @todo Implement caching here
-  $_gateways = array();
-  $_names = array();
-
-  $gateway_array = \Drupal::moduleHandler()->invokeAll('gateway_info');
-  foreach ($gateway_array as $identifier => $info) {
-    $settings = \Drupal::config('sms.gateway.' . $identifier)->get('settings');
-    $info['configuration'] = isset($settings) ? $settings : array();
-    $_gateways[$identifier] = $info;
-    $_names[$identifier] = $info['name'];
-  }
-
-  asort($_names);
-
-  return array($_gateways, $_names);
-}
-
-/**
  * Form builder for send sms form.
  *
  * Generates a SMS sending form and adds gateway defined elements. The form
@@ -293,10 +224,9 @@ function sms_send_form($required = FALSE) {
   );
 
   // Add gateway defined fields
-  if (!empty($gateway['send form']) && function_exists($gateway['send form'])) {
-    $form['gateway']['#tree'] = TRUE;
-    $form['gateway'] = array_merge($gateway['send form']($required), $form['gateway']);
-  }
+  $form_state = new FormState();
+  $form['gateway']['#tree'] = TRUE;
+  $form['gateway'] = array_merge($gateway->sendForm($form, $form_state), $form['gateway']);
 
   return $form;
 }
@@ -560,10 +490,8 @@ function sms_validate_number(&$number, array $options = array()) {
   else {
     $gateway = sms_default_gateway();
   }
-  if (isset($gateway['validate number']) && function_exists($gateway['validate number'])) {
-    if ($error = $gateway['validate number']($number, $options)) {
-      $errors += (array) $error;
-    }
+  if ($error = $gateway->validateNumbers([$number], $options)) {
+    $errors += (array) $error;
   }
 
   return $errors;
diff --git a/sms.routing.yml b/sms.routing.yml
index bd2ef6c..099d5ff 100644
--- a/sms.routing.yml
+++ b/sms.routing.yml
@@ -14,6 +14,14 @@ sms.gateway_admin:
   requirements:
     _permission: 'administer smsframework'
 
+sms.gateway_add:
+  path: '/admin/config/smsframework/gateways/add'
+  defaults:
+    _form: '\Drupal\sms\Form\GatewayAddForm'
+    _title: 'Add new gateway configuration'
+  requirements:
+    _permission: 'administer smsframework'
+
 sms.gateway_config:
   path: '/admin/config/smsframework/gateways/{gateway_id}'
   defaults:
@@ -62,3 +70,10 @@ sms.bootstrap_admin:
     _title: 'Incoming SMS bootstrap'
   requirements:
     _permission: 'administer smsframework'
+
+sms.delivery_report:
+  path: '/sms/deliveryreport/{gateway_id}'
+  defaults:
+    _controller: '\Drupal\sms\DeliveryReportController::acknowledgeDelivery'
+  requirements:
+    _access: 'TRUE'
diff --git a/sms.services.yml b/sms.services.yml
index 7f49ff4..39f9257 100644
--- a/sms.services.yml
+++ b/sms.services.yml
@@ -1,4 +1,8 @@
 services:
   sms_provider.default:
     class: Drupal\sms\Provider\DefaultSmsProvider
-    arguments: ['@module_handler']
+    arguments: ['@plugin.manager.sms_gateway', '@module_handler']
+
+  plugin.manager.sms_gateway:
+    class: Drupal\sms\Gateway\GatewayManager
+    arguments: ['@container.namespaces', '@config.factory', '@cache.discovery', '@module_handler']
diff --git a/src/Annotation/SmsGateway.php b/src/Annotation/SmsGateway.php
new file mode 100644
index 0000000..a2293f7
--- /dev/null
+++ b/src/Annotation/SmsGateway.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\sms\Annotation\SmsGateway
+ */
+
+namespace Drupal\sms\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines SmsGateway Annotation object.
+ *
+ * @Annotation
+ */
+class SmsGateway extends Plugin {
+
+  /**
+   * The machine name of the sms gateway.
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * Translated user-readable label.
+   * @var string
+   */
+  protected $label;
+
+  /**
+   * A boolean flag to determine if the gateway is configurable.
+   *
+   * @var bool
+   */
+  protected $configurable;
+
+}
diff --git a/src/DeliveryReportController.php b/src/DeliveryReportController.php
new file mode 100644
index 0000000..a90d0bd
--- /dev/null
+++ b/src/DeliveryReportController.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\sms\DeliveryReportController
+ */
+
+namespace Drupal\sms;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\sms\Gateway\GatewayManagerInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Provides delivery reports acknowledgement and passes to the correct gateway.
+ */
+class DeliveryReportController implements ContainerInjectionInterface {
+
+  /**
+   * The gateway manager.
+   *
+   * @var \Drupal\sms\Gateway\GatewayManagerInterface
+   */
+  protected $gatewayManager;
+
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * Creates an new delivery report controller.
+   *
+   * @param \Drupal\sms\Gateway\GatewayManagerInterface $gateway_manager
+   *   The gateway manager service.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   */
+  public function __construct(GatewayManagerInterface $gateway_manager, RequestStack $request_stack) {
+    $this->gatewayManager = $gateway_manager;
+    $this->requestStack = $request_stack;
+  }
+
+  /**
+   * Acknowledges delivery reports and passes them to the correct gateway.
+   *
+   * @param string $gateway_id
+   *   The gateway id of the gateway that is to handle the delivery report.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   */
+  public function acknowledgeDelivery($gateway_id) {
+    $acknowledgement = $this->gatewayManager->getGateway($gateway_id)->deliveryReport($this->requestStack->getCurrentRequest());
+    return new Response($acknowledgement);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.sms_gateway'),
+      $container->get('request_stack')
+    );
+  }
+
+}
diff --git a/src/Form/GatewayAddForm.php b/src/Form/GatewayAddForm.php
new file mode 100644
index 0000000..850483b
--- /dev/null
+++ b/src/Form/GatewayAddForm.php
@@ -0,0 +1,106 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\sms\Form\GatewayCreateForm
+ */
+
+namespace Drupal\sms\Form;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\sms\Gateway\GatewayManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class GatewayAddForm extends FormBase {
+
+  /**
+   * The gateway manager.
+   *
+   * @var \Drupal\sms\Gateway\GatewayManagerInterface
+   */
+  protected $gatewayManager;
+
+  /**
+   * Constructor.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The configuration factory.
+   * @param \Drupal\sms\Gateway\GatewayManagerInterface $gateway_manager
+   *   The gateway manager
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, GatewayManagerInterface $gateway_manager) {
+    $this->setConfigFactory($config_factory);
+    $this->gatewayManager = $gateway_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('config.factory'),
+      $container->get('plugin.manager.sms_gateway')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'gateway_create_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['label'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Gateway name'),
+    ];
+    $form['name'] = [
+      '#type' => 'machine_name',
+      '#title' => '',
+      '#machine_name' => [
+        'exists' => [$this, 'gatewayExists'],
+        'source' => ['label'],
+      ],
+    ];
+    $form['plugin_id'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Gateway type'),
+      // List plugins that are configurable so user can choose.
+      '#options' => array_filter(array_map(function ($definition) {
+        return $definition['configurable'] ? $definition['label'] : FALSE;
+      }, $this->gatewayManager->getGatewayPlugins())),
+      '#required' => TRUE,
+    ];
+
+    $form['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Create'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    // Create the gateway and redirect to the configure form.
+    $configuration = $form_state->cleanValues()->getValues();
+    $this->gatewayManager->addGateway($configuration['plugin_id'], $configuration);
+    $form_state->setRedirect('sms.gateway_config', ['gateway_id' => $configuration['name']]);
+  }
+
+  /**
+   * Helper function for machine_name form element.
+   */
+  public function gatewayExists($machine_name) {
+    return (bool) $this->gatewayManager->getGateway($machine_name);
+  }
+
+}
diff --git a/src/Form/GatewayConfigForm.php b/src/Form/GatewayConfigForm.php
index 3b74719..7e808a1 100644
--- a/src/Form/GatewayConfigForm.php
+++ b/src/Form/GatewayConfigForm.php
@@ -2,13 +2,16 @@
 
 /**
  * @file
- * Contains GatewayConfigForm class
+ * Contains \Drupal\sms\Form\GatewayConfigForm
  */
 
 namespace Drupal\sms\Form;
 
+use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Form\ConfigFormBase;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\sms\Gateway\GatewayManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 /**
@@ -18,6 +21,32 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  * form more streamlined
  */
 class GatewayConfigForm extends ConfigFormBase {
+
+  /**
+   * The gateway manager.
+   *
+   * @var \Drupal\sms\Gateway\GatewayManagerInterface
+   */
+  protected $gatewayManager;
+
+  /**
+   * Creates new Gateway configuration form.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, GatewayManagerInterface $gateway_manager) {
+    $this->setConfigFactory($config_factory);
+    $this->gatewayManager = $gateway_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('config.factory'),
+      $container->get('plugin.manager.sms_gateway')
+    );
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -29,18 +58,27 @@ class GatewayConfigForm extends ConfigFormBase {
    * {@inheritdoc}
    */
   public function buildForm(array $form, FormStateInterface $form_state, $gateway_id = NULL) {
-    $gateway = sms_gateways('gateway', $gateway_id);
-    if ($gateway && !empty($gateway['configure form']) && function_exists($gateway['configure form'])) {
-      $form = $gateway['configure form']($gateway['configuration']);
-      $form['#title'] = $this->t('@gateway configuration', array('@gateway' => $gateway['name']));
-
+    $gateway = $this->gatewayManager->getGateway($gateway_id);
+    if ($gateway && $gateway->isConfigurable()) {
+      $form['title'] = [
+        '#type' => 'item',
+        '#title' => $gateway->getLabel(),
+        '#markup' => '(' . $gateway->getName() . ')',
+      ];
+      $form['enabled'] = [
+        '#type' => 'checkbox',
+        '#title' => $this->t('Enable this gateway'),
+        '#default_value' => $gateway->isEnabled(),
+      ];
+      $form = $gateway->buildConfigurationForm($form, $form_state);
+      $form['#title'] = $this->t('@gateway configuration', array('@gateway' => $gateway->getLabel()));
       $form['submit'] = array(
         '#type' => 'submit',
-        '#value' => $this->t('Save'),
+        '#value' => $this->t('Save configuration'),
       );
-      $form['gateway'] = array(
+      $form['gateway_id'] = array(
         '#type' => 'value',
-        '#value' => $gateway,
+        '#value' => $gateway->getIdentifier(),
       );
   
       return $form;
@@ -55,24 +93,24 @@ class GatewayConfigForm extends ConfigFormBase {
    */
   public function validateForm(array &$form, FormStateInterface $form_state) {
     // Pass validation to gateway.
-    $function = $form_state->getValue(['gateway', 'configure form']) . '_validate';
-    if (function_exists($function)) {
-      $function($form, $form_state);
-    }
+    $gateway = $this->gatewayManager->getGateway($form_state->getValue('gateway_id'));
+    $gateway->validateConfigurationForm($form, $form_state);
   }
 
   /**
    * {@inheritdoc}
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
-    $gateway = $form_state->getValue('gateway');
-    // Remove unnecessary values.
-    $form_state->cleanValues();
-    $form_state->unsetValue('gateway');
-
-    $this->configFactory()->getEditable('sms.gateway.' . $gateway['identifier'])
-      ->set('settings' , $form_state->getValues())
-      ->save();
+    // Call the gateway submission callback before saving configuration.
+    $gateway = $this->gatewayManager->getGateway($form_state->getValue('gateway_id'));
+    $gateway->setEnabled($form_state->getValue('enabled'));
+    $form_state
+      ->unsetValue('title')
+      ->unsetValue('enabled')
+      ->unsetValue('gateway_id')
+      ->cleanValues();
+    $gateway->submitConfigurationForm($form, $form_state);
+    $this->gatewayManager->saveGateway($gateway);
     drupal_set_message($this->t('The gateway settings have been saved.'));
     $form_state->setRedirect('sms.gateway_admin');
   }
@@ -81,8 +119,7 @@ class GatewayConfigForm extends ConfigFormBase {
    * Title callback fo the menu
    */
   public function getTitle($gateway_id) {
-    $gateway = sms_gateways('gateway', $gateway_id);
-    return sprintf('%s gateway', $gateway['name']);
+    return $this->t('@name gateway', ['@name' => $this->gatewayManager->getGateway($gateway_id)->getLabel()]);
   }
 
   /**
diff --git a/src/Form/GatewayDefaultForm.php b/src/Form/GatewayDefaultForm.php
index 3856657..1f08232 100644
--- a/src/Form/GatewayDefaultForm.php
+++ b/src/Form/GatewayDefaultForm.php
@@ -2,20 +2,49 @@
 
 /**
  * @file
- * Contains GatewayDefaultForm class
+ * Contains \Drupal\sms\Form\GatewayDefaultForm
  */
 
 namespace Drupal\sms\Form;
 
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Form\ConfigFormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
+use Drupal\sms\Gateway\GatewayManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides a configuration form for setting the default gateway.
  */
 class GatewayDefaultForm extends ConfigFormBase {
+
+  /**
+   * The gateway manager.
+   *
+   * @var \Drupal\sms\Gateway\GatewayManagerInterface
+   */
+  protected $gatewayManager;
+
+  /**
+   * Creates new Gateway default selection form.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, GatewayManagerInterface $gateway_manager) {
+    $this->setConfigFactory($config_factory);
+    $this->gatewayManager = $gateway_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('config.factory'),
+      $container->get('plugin.manager.sms_gateway')
+    );
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -28,12 +57,14 @@ class GatewayDefaultForm extends ConfigFormBase {
    */
   public function buildForm(array $form, FormStateInterface $form_state) {
     $form = parent::buildForm($form, $form_state);
-    $gateways = sms_gateways();
-    $default = sms_default_gateway_id();
+    /** @var \Drupal\sms\Gateway\GatewayInterface[] $gateways */
+    $gateways = $this->gatewayManager->getAvailableGateways();
+    $default = $this->gatewayManager->getDefaultGateway();
 
     $form['gateways'] = array(
       '#type' => 'table',
       '#header' => array(
+        $this->t('Enabled'),
         $this->t('Default'),
         $this->t('Name'),
         array(
@@ -47,41 +78,79 @@ class GatewayDefaultForm extends ConfigFormBase {
     );
     foreach ($gateways as $identifier => $gateway) {
       $form['gateways'][$identifier] = array(
+        'enabled' => [
+          '#type' => 'checkbox',
+          '#default_value' => $gateway->isEnabled(),
+          // The checkbox should be disabled if this gateway is the default.
+          // @todo Needs tests.
+          '#states' => [
+            'disabled' => [
+              ':input[name="default"]' => ['value' => $identifier],
+            ],
+          ],
+        ],
         'default' => [
           '#name' => 'default',
           '#type' => 'radio',
-          '#default_value' => $default,
+          '#default_value' => ($default && $default->getName() ==  $identifier),
           '#return_value' => $identifier,
+          // The radio button should be disabled if this gateway not enabled.
+          // @todo Needs tests.
+          '#states' => [
+            'disabled' => [
+              ':input[name="gateways[' . $identifier . '][enabled]"]' => ['checked' => FALSE],
+            ],
+          ],
+        ],
+        'name' => [
+          '#markup' => SafeMarkup::checkPlain($gateway->getLabel()),
         ],
-        'name' => ['#markup' => SafeMarkup::checkPlain($gateway['name'])],
       );
-      if (isset($gateway['configure form']) && function_exists($gateway['configure form'] )) {
-        $form['gateways'][$identifier]['configure'] = ['#markup' => $this->l($this->t('configure'), Url::fromRoute('sms.gateway_config', ['gateway_id' => $identifier]))];
+      if ($gateway->isConfigurable()) {
+        $form['gateways'][$identifier]['configure'] = [
+          '#type' => 'link',
+          '#title' => $this->t('configure'),
+          '#url' => Url::fromRoute('sms.gateway_config', ['gateway_id' => $identifier]),
+        ];
       }
       else {
-        $form['gateways'][$identifier]['configure'] = array('#markup' => $this->t('No configuration options'));
+        $form['gateways'][$identifier]['configure'] = [
+          '#markup' => $this->t('No configuration options')
+        ];
       }
     }
-    $form['actions']['submit']['#value'] = $this->t('Set default gateway');
+    $form['actions']['submit']['#value'] = $this->t('Save settings');
     return $form;
   }
 
   public function validateForm(array &$form, FormStateInterface $form_state) {
-    $form_state->setValue('default', $form_state->getUserInput()['default']);
+    if (empty($form_state->getUserInput()['default'])) {
+      $form_state->setErrorByName('default', $this->t('Default gateway must be set.'));
+    }
+    else {
+      $form_state->setValue('default', $form_state->getUserInput()['default']);
+    }
   }
   
   /**
    * {@inheritdoc}
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
+    // Set the enabled status of the gateways.
+    $new_default = $form_state->getValue('default');
+    foreach ($form_state->getValue('gateways') as $identifier => $config) {
+      $gateway = $this->gatewayManager->getGateway($identifier);
+      // Selected default gateway is automatically enabled.
+      if ($gateway->getName() === $new_default) {
+        $config['enabled'] = TRUE;
+      }
+      $gateway->setEnabled($config['enabled']);
+      $this->gatewayManager->saveGateway($gateway);
+    }
     // Process form submission to set the default gateway.
-    $default = $form_state->getValue('default');
-    if (sms_gateways('name', $default)) {
+    if ($this->gatewayManager->getGateway($new_default)) {
       drupal_set_message($this->t('Default gateway updated.'));
-
-      $this->config('sms.settings')
-        ->set('default_gateway', $default)
-        ->save();
+      $this->gatewayManager->setDefaultGateway($new_default);
     }
   }
 
diff --git a/src/Gateway/GatewayBase.php b/src/Gateway/GatewayBase.php
new file mode 100644
index 0000000..2a64c4d
--- /dev/null
+++ b/src/Gateway/GatewayBase.php
@@ -0,0 +1,197 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\sms\Gateway\GatewayBase
+ */
+
+namespace Drupal\sms\Gateway;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\PluginBase;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Base class for sms gateway plugins.
+ */
+abstract class GatewayBase extends PluginBase implements GatewayInterface {
+
+  /**
+   * The watchdog logger for this gateway.
+   *
+   * @var \Psr\Log\LoggerInterface.
+   */
+  protected $logger;
+
+  /**
+   * Construct a new SmsGateway plugin
+   *
+   * @param array $configuration
+   *   The configuration to use and build the sms gateway.
+   * @param string $plugin_id
+   *   The gateway id.
+   * @param mixed $plugin_definition
+   *   The gateway plugin definition.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->configuration = array_merge($this->defaultConfiguration(), $this->configuration);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIdentifier() {
+    return $this->configuration['name'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getName() {
+    return $this->configuration['name'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLabel() {
+    return $this->configuration['label'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfiguration() {
+    return $this->configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setConfiguration(array $configuration) {
+    $this->configuration = $configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCustomConfiguration() {
+    return $this->configuration['custom'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setCustomConfiguration($configuration) {
+    $this->configuration['custom'] = $configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'plugin_id' => $this->pluginDefinition['id'],
+      'name' => $this->pluginDefinition['id'],
+      'label' => (string) $this->pluginDefinition['label'],
+      'enabled' => FALSE,
+      'custom' => [],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isConfigurable() {
+    return $this->pluginDefinition['configurable'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+    return;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    return;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function sendForm(array &$form, FormStateInterface $form_state) {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEnabled() {
+    return isset($this->configuration['enabled']) ? $this->configuration['enabled'] : FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setEnabled($enabled = TRUE) {
+    $this->configuration['enabled'] = (bool) $enabled;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateNumbers(array $numbers, array $options = []) {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function balance() {
+    return 0;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deliveryReport(Request $request) {
+    return;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getError() {
+    return [];
+  }
+
+  /**
+   * Gets the Psr logger for this gateway.
+   */
+  protected function logger() {
+    if (!isset($this->logger)) {
+      $this->logger = \Drupal::logger($this->getName());
+    }
+    return $this->logger;
+  }
+
+}
diff --git a/src/Gateway/GatewayInterface.php b/src/Gateway/GatewayInterface.php
new file mode 100644
index 0000000..48f0f85
--- /dev/null
+++ b/src/Gateway/GatewayInterface.php
@@ -0,0 +1,230 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\sms\Gateway\GatewayInterface
+ */
+
+namespace Drupal\sms\Gateway;
+
+use Drupal\Component\Plugin\ConfigurablePluginInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\sms\Message\SmsMessageInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Default implementation of sms gateway plugin
+ */
+interface GatewayInterface extends ConfigurablePluginInterface, PluginFormInterface {
+
+  /**
+   * Status Unknown.
+   *
+   * A gateway should return this status to indicate unknown status.
+   */
+  const STATUS_UNKNOWN = 0;
+
+  /**
+   * Status OK.
+   *
+   * A gateway should return this status to indicate message successfully sent.
+   */
+  const STATUS_OK = 200;
+
+  /**
+   * Authentication error.
+   *
+   * A gateway should return this status to indicate authentication error.
+   */
+  const STATUS_ERR_AUTH = 401;
+
+  /**
+   * Invalid Call.
+   *
+   * A gateway should return this status to indicate invalid call attempt.
+   */
+  const STATUS_ERR_INVALID_CALL = 400;
+
+  /**
+   * Gateway endpoint not found.
+   *
+   * A gateway should return this status to indicate gateway endpoint not found.
+   */
+  const STATUS_ERR_NOT_FOUND = 404;
+
+  /**
+   * Message limits exceeded.
+   *
+   * A gateway should return this status to indicate message limits exceeded.
+   */
+  const STATUS_ERR_MSG_LIMITS = 413;
+
+  /**
+   * Routing error.
+   *
+   * A gateway should return this status to indicate message routing error.
+   */
+  const STATUS_ERR_MSG_ROUTING = 502;
+
+  /**
+   * Message queuing error.
+   *
+   * A gateway should return this status to indicate message queuing error.
+   */
+  const STATUS_ERR_MSG_QUEUING = 408;
+
+  /**
+   * Other message error.
+   *
+   * A gateway should return this status to indicate other message error.
+   */
+  const STATUS_ERR_MSG_OTHER = 409;
+
+  /**
+   * Source number or id error.
+   *
+   * A gateway should return this status to indicate sender number or id error.
+   */
+  const STATUS_ERR_SRC_NUMBER = 415;
+
+  /**
+   * Destination number error.
+   *
+   * A gateway should return this status to indicate destination number error.
+   */
+  const STATUS_ERR_DEST_NUMBER = 416;
+
+  /**
+   * Credit error.
+   *
+   * A gateway should return this status to indicate insufficient credit error.
+   */
+  const STATUS_ERR_CREDIT = 402;
+
+  /**
+   * Other error.
+   *
+   * A gateway should return this status to indicate a known error condition but
+   * not mappable to any equivalent above.
+   */
+  const STATUS_ERR_OTHER = 500;
+
+  /**
+   * Sends an sms and invokes the corresponding sms receipt method.
+   *
+   * @param \Drupal\sms\Message\SmsMessageInterface $sms
+   *   The sms to be sent.
+   * @param array $options
+   *   Options to be applied while processing this sms.
+   *
+   * @return \Drupal\sms\Message\SmsMessageResultInterface
+   *   The result of the sms messaging operation.
+   */
+  public function send(SmsMessageInterface $sms, array $options);
+
+  /**
+   * Returns the credit balance available on this gateway.
+   *
+   * @return number
+   */
+  public function balance();
+
+  /**
+   * Handles delivery reports and invokes the corresponding sms_receipt method.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request
+   *   Request object containing the delivery report in raw format.
+   */
+  public function deliveryReport(Request $request);
+
+  /**
+   * Gets the last error message from the gateway.
+   *
+   * @return array
+   *   An array of values containing the following:
+   *   - code: the error code.
+   *   - message: the error message.
+   */
+  public function getError();
+
+  /**
+   * Gets the machine  name of the gateway.
+   *
+   * @return string
+   */
+  public function getIdentifier();
+
+  /**
+   * Gets the readable name of the gateway.
+   *
+   * @return string
+   */
+  public function getName();
+
+  /**
+   * Gets the user-readable translated name of the gateway.
+   *
+   * @return string
+   */
+  public function getLabel();
+
+  /**
+   * Returns a boolean to show if the gateway is configurable.
+   *
+   * @return bool
+   *   TRUE if the form is configurable, FALSE if not.
+   */
+  public function isConfigurable();
+
+  /**
+   * Returns a form to be appended to the send form.
+   *
+   * @param array $form
+   *   The parent form array.
+   * @param array $form_state
+   *   Form state.
+   * @returns array
+   *   The form for additional gateway-specific sending options.
+   */
+  public function sendForm(array &$form, FormStateInterface $form_state);
+
+  /**
+   * Gets the enabled status of the gateway.
+   *
+   * @return bool
+   */
+  public function isEnabled();
+
+  /**
+   * Sets the enabled status of the gateway.
+   *
+   * @param bool $status
+   *   The enabled status.
+   */
+  public function setEnabled($status = TRUE);
+
+  /**
+   * Gets the plugin-specific configuration for this gateway.
+   */
+  public function getCustomConfiguration();
+
+  /**
+   * Sets the plugin-specific configuration for this gateway.
+   */
+  public function setCustomConfiguration($configuration);
+
+  /**
+   * Carry out gateway-specific number validation.
+   *
+   * @param array $numbers
+   *   The list of phone numbers to be validated.
+   * @param array $options
+   *   Options to be considered for validation.
+   *
+   * @return array
+   *   An array containing an error message for each validation failure.
+   */
+  public function validateNumbers(array $numbers, array $options = array());
+
+}
diff --git a/src/Gateway/GatewayManager.php b/src/Gateway/GatewayManager.php
new file mode 100644
index 0000000..e865dc3
--- /dev/null
+++ b/src/Gateway/GatewayManager.php
@@ -0,0 +1,248 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\sms\Gateway\GatewayManager
+ */
+
+namespace Drupal\sms\Gateway;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Config\ConfigFactory;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\Core\StringTranslation\TranslationWrapper;
+
+/**
+ * Manages SMS gateways implemented using AnnotatedClassDiscovery
+ */
+class GatewayManager extends DefaultPluginManager implements GatewayManagerInterface {
+
+  /**
+   * Configuration factory for this gateway manager.
+   *
+   * @var \Drupal\Core\Config\ConfigFactory
+   */
+  protected $configFactory;
+
+  /**
+   * List of gateways managed by this gateway manager.
+   *
+   * @var \Drupal\sms\Gateway\GatewayInterface[]
+   */
+  protected $gateways;
+
+  /**
+   * List of human-readable names for the gateways.
+   *
+   * @var string[]
+   */
+  protected $names;
+
+  /**
+   * The default gateway.
+   *
+   * @var string
+   */
+  protected $defaultGateway;
+
+  /**
+   * Create new GatewayManager instance.
+   *
+   * @param \Traversable $namespaces
+   *   The namespaces to search for the gateway plugins.
+   * @param \Drupal\Core\Config\ConfigFactory $config_factory
+   *   Handles the instantiation of gateways based on stored configuration.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   Cache backend instance to use.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface
+   *   Module handler for calling module hooks.
+   */
+  public function __construct(\Traversable $namespaces, ConfigFactory $config_factory, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+    parent::__construct('Plugin/Gateway', $namespaces, $module_handler, 'Drupal\sms\Gateway\GatewayInterface', 'Drupal\sms\Annotation\SmsGateway');
+    $this->setCacheBackend($cache_backend, 'sms_gateways');
+    $this->alterInfo('sms_gateway_info');
+    $this->configFactory = $config_factory;
+    $this->defaultGateway = $this->configFactory->get('sms.settings')->get('default_gateway');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGatewayPlugins() {
+    return $this->getDefinitions();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGateway($gateway_id) {
+    // Ensure gateway list has been built.
+    $gateways = $this->getAvailableGateways();
+    if (!isset($gateways[$gateway_id])) {
+      return null;
+    }
+    else {
+      return $gateways[$gateway_id];
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultGateway() {
+    if (!isset($this->defaultGateway)) {
+      $this->defaultGateway = $this->configFactory->get('sms.settings')
+        ->get('default_gateway');
+    }
+    return $this->getGateway($this->defaultGateway);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setDefaultGateway($gateway_id) {
+    // Cannot make a disabled gateway default.
+    if ($this->getGateway($gateway_id)->isEnabled()) {
+      $this->defaultGateway = $gateway_id;
+      $this->configFactory->getEditable('sms.settings')
+        ->set('default_gateway', $this->defaultGateway)
+        ->save();
+    }
+    else {
+      throw new \LogicException('A disabled gateway cannot be made the default.');
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAvailableGateways() {
+    if (!isset($this->gateways)) {
+      $this->buildGateways();
+    }
+    return $this->gateways;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEnabledGateways() {
+    return array_filter($this->getAvailableGateways(), function(GatewayInterface $gateway) {
+      $gateway->isEnabled();
+    });
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setEnabledGateways(array $gateway_ids) {
+    $gateways = [];
+    foreach ($gateway_ids as $gateway_id) {
+      $gateway = $this->getGateway($gateway_id);
+      $gateway->setEnabled(TRUE);
+      $gateways[] = $gateway;
+    }
+    $this->saveGateways($gateways);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isGatewayEnabled($gateway_id) {
+    return $this->getGateway($gateway_id)->isEnabled();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function saveGateway(GatewayInterface $gateway) {
+    $this->saveGateways([$gateway]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function saveGateways(array $gateways) {
+    /** @var \Drupal\sms\Gateway\GatewayInterface $gateway */
+    foreach ($gateways as $gateway) {
+      $this->configFactory->getEditable('sms.gateway.' . $gateway->getName())
+        ->setData($gateway->getConfiguration())
+        ->save();
+    }
+    // @todo Implement more granular cache invalidations.
+    // Clear static caches.
+    $this->gateways = NULL;
+    $this->names = NULL;
+    // Clear dynamic caches.
+    $this->clearCachedDefinitions();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addGateway($plugin_id, array $configuration) {
+    // Initialize existing gateways first.
+    if (!isset($this->gateways)) {
+      $this->buildGateways();
+    }
+    $name = $configuration['name'];
+    if (!isset($configuration['name'])) {
+      throw new \InvalidArgumentException('A machine name is required to create an SMS gateway.');
+    }
+    $configuration['plugin_id'] = $plugin_id;
+    $this->gateways[$name] = $gateway = $this->createInstance($plugin_id, $configuration);
+    $this->names[$name] = $this->gateways[$name]->getLabel();
+    $this->saveGateway($gateway);
+  }
+
+  /**
+   * Builds the gateway plugin objects from the definitions.
+   *
+   * @return array
+   *   \Drupal\sms\Gateway\GatewayInterface[]
+   */
+  protected function buildGateways() {
+    // Get configured gateways.
+    $names = $this->configFactory->listAll('sms.gateway.');
+    foreach ($this->configFactory->loadMultiple($names) as $id => $instance_config) {
+      $id = substr($id, 12);
+      $settings = $instance_config->getRawData();
+      // Set some sane defaults.
+      $settings += ['name' => $id];
+
+      // Note that DefaultFactory::createInstance will get the right definitions.
+      if (!isset($settings['plugin_id'])) {
+        throw new \InvalidArgumentException(sprintf('Gateway "%s" configured without a plugin id.', $id));
+      }
+      $this->gateways[$id] = $this->createInstance($settings['plugin_id'], $settings);
+      $this->names[$id] = $this->gateways[$id]->getLabel();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @todo remove BC-shim when all gateways are converted to plugins.
+   */
+  public function findDefinitions() {
+    // Get discovered plugin definitions.
+    $definitions = parent::findDefinitions();
+
+    // Add hook_gateway_info definitions.
+    foreach ($this->moduleHandler->invokeAll('gateway_info') as $id => $hook_info) {
+      // @todo This allows overwriting of annotated plugins by hook plugins.
+      // @todo Is that acceptable?
+      $definitions[$id] = [
+        'id' => $id,
+        'label' => new TranslationWrapper($hook_info['name']),
+        'configurable' => is_callable($hook_info['configure form']),
+        'class' => '\Drupal\sms\Gateway\HookGateway',
+        'provider' => 'sms',
+        'hook_info' => $hook_info,
+      ];
+    }
+    return $definitions;
+  }
+
+}
diff --git a/src/Gateway/GatewayManagerInterface.php b/src/Gateway/GatewayManagerInterface.php
new file mode 100644
index 0000000..ca1443d
--- /dev/null
+++ b/src/Gateway/GatewayManagerInterface.php
@@ -0,0 +1,108 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\sms\GatewayInterface
+ */
+namespace Drupal\sms\Gateway;
+
+/**
+ * Manages SMS Gateways.
+ */
+interface GatewayManagerInterface {
+
+  /**
+   * Get the list of all discoverable SMS gateways.
+   *
+   * @return \Drupal\sms\Gateway\GatewayInterface[]
+   */
+  public function getAvailableGateways();
+
+  /**
+   * Get the list of all enabled SMS gateways.
+   *
+   * @return \Drupal\sms\Gateway\GatewayInterface[]
+   */
+  public function getEnabledGateways();
+
+  /**
+   * Get the default SMS gateway.
+   *
+   * @return \Drupal\sms\Gateway\GatewayInterface
+   */
+  public function getDefaultGateway();
+
+  /**
+   * Get the gateway whose id is specified
+   *
+   * @param string $gateway_id
+   *   The gateway id.
+   * @return \Drupal\sms\Gateway\GatewayInterface
+   */
+  public function getGateway($gateway_id);
+
+  /**
+   * Sets the list of gateways that are enabled.
+   *
+   * @param array $gateway_ids
+   *   An array of ids of the gateways to be enabled.
+   */
+  public function setEnabledGateways(array $gateway_ids);
+
+  /**
+   * Sets the default sms gateway for messaging.
+   *
+   * @param string $gateway_id
+   *   The gateway id.
+   */
+  public function setDefaultGateway($gateway_id);
+
+  /**
+   * Gets the status of the specified gateway.
+   *
+   * @param string $gateway_id
+   *   The id of the gateway to be checked.
+   * @return bool
+   *   TRUE if the gateway is enabled and FALSE otherwise.
+   */
+  public function isGatewayEnabled($gateway_id);
+
+  /**
+   * Creates a new gateway of a type from specified configuration.
+   *
+   * @param string $plugin_id
+   *   The plugin ID of an existing gateway plugin.
+   * @param array $configuration
+   *   The configuration to be saved for the gateway. At a minimum must contain:
+   *   - name: The machine name of the gateway
+   *   - label: The human-readable name of the gateway
+   *
+   * @return \Drupal\sms\Gateway\GatewayInterface
+   *   A newly created gateway instance.
+   */
+  public function addGateway($plugin_id, array $configuration);
+
+  /**
+   * Saves the configuration of a single gateway.
+   *
+   * @param \Drupal\sms\Gateway\GatewayInterface
+   *   The gateway object whose configuration is to be saved.
+   */
+  public function saveGateway(GatewayInterface $gateway);
+
+  /**
+   * Saves the configuration of multiple gateways.
+   *
+   * @param \Drupal\sms\Gateway\GatewayInterface[]
+   *   An array of gateway objects whose configuration is to be saved.
+   */
+  public function saveGateways(array $gateway);
+
+  /**
+   * Gets the list of gateway plugins discovered by this gateway manager.
+   *
+   * @return \Drupal\sms\Gateway\GatewayInterface[]
+   */
+  public function getGatewayPlugins();
+
+}
diff --git a/src/Gateway/HookGateway.php b/src/Gateway/HookGateway.php
new file mode 100644
index 0000000..ac0fbc3
--- /dev/null
+++ b/src/Gateway/HookGateway.php
@@ -0,0 +1,115 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\sms\Gateway\HookGateway
+ */
+
+namespace Drupal\sms\Gateway;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\sms\Message\SmsMessageInterface;
+use Drupal\sms\Message\SmsMessageResult;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * BC-shim class to allow hook_gateway_info() based gateways to still work.
+ */
+class HookGateway extends GatewayBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function send(SmsMessageInterface $sms, array $options) {
+    $hook = $this->pluginDefinition['hook_info']['send'];
+    if (is_callable($hook)) {
+      $result = $hook($sms, $options);
+      return new SmsMessageResult($result);
+    }
+    throw new \BadMethodCallException(sprintf('No send method defined for gateway %s', $this->configuration['name']));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function balance() {
+    $hook = $this->pluginDefinition['hook_info']['balance'];
+    if (is_callable($hook)) {
+      return $hook();
+    }
+    return 0;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deliveryReport(Request $request) {
+    $hook = $this->pluginDefinition['hook_info']['delivery report'];
+    if (is_callable($hook)) {
+      return $hook();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $hook = $this->pluginDefinition['hook_info']['configure form'];
+    if (is_callable($hook)) {
+      return array_merge($form, $hook($this->getCustomConfiguration()));
+    }
+    else {
+      throw new \RuntimeException($this->t('Configuration form callback not available.'));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $hook = $this->pluginDefinition['hook_info']['configure form'] . '_validate';
+    if (is_callable($hook)) {
+      $hook($form, $form_state);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $hook = $this->pluginDefinition['hook_info']['configure form'] . '_submit';
+    if (is_callable($hook)) {
+      // Call the hook gateway submit callback.
+      $hook($form, $form_state);
+    }
+    // Update the configuration.
+    $this->setCustomConfiguration($form_state->getValues());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function sendForm(array &$form, FormStateInterface $form_state) {
+    $hook = $this->pluginDefinition['hook_info']['send form'];
+    if (is_callable($hook)) {
+      return $hook($form, $form_state);
+    }
+    else {
+      return array();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateNumbers(array $numbers, array $options = array()) {
+    $hook = $this->pluginDefinition['hook_info']['validate number'];
+    if (is_callable($hook)) {
+      return $hook($numbers, $options);
+    }
+    else {
+      return array();
+    }
+  }
+
+}
diff --git a/src/Plugin/Gateway/LogGateway.php b/src/Plugin/Gateway/LogGateway.php
new file mode 100644
index 0000000..f7cabfe
--- /dev/null
+++ b/src/Plugin/Gateway/LogGateway.php
@@ -0,0 +1,32 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\sms\Plugin\Gateway\LogGateway
+ */
+
+namespace Drupal\sms\Plugin\Gateway;
+
+use Drupal\sms\Gateway\GatewayBase;
+use Drupal\sms\Message\SmsMessageInterface;
+use Drupal\sms\Message\SmsMessageResult;
+
+/**
+ * @SmsGateway(
+ *   id = "log",
+ *   label = @Translation("Log only"),
+ *   configurable = false,
+ * )
+ */
+class LogGateway extends GatewayBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function send(SmsMessageInterface $sms, array $options) {
+    // Log sms message to drupal logger.
+    $this->logger()->notice('SMS message sent to %number with the text: @message',
+      ['%number' => implode(', ', $sms->getRecipients()), '@message' => $sms->getMessage()]);
+    return new SmsMessageResult(['status' => TRUE]);
+  }
+
+}
diff --git a/src/Provider/DefaultSmsProvider.php b/src/Provider/DefaultSmsProvider.php
index 7cafe02..712cac0 100644
--- a/src/Provider/DefaultSmsProvider.php
+++ b/src/Provider/DefaultSmsProvider.php
@@ -8,8 +8,9 @@
 namespace Drupal\sms\Provider;
 
 use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\sms\Gateway\GatewayInterface;
+use Drupal\sms\Gateway\GatewayManagerInterface;
 use Drupal\sms\Message\SmsMessageInterface;
-use Drupal\sms\Message\SmsMessageResult;
 use Drupal\sms\Message\SmsMessageResultInterface;
 use Drupal\sms\SmsException;
 
@@ -19,19 +20,29 @@ use Drupal\sms\SmsException;
 class DefaultSmsProvider implements SmsProviderInterface {
 
   /**
-   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   * The SMS gateway manager.
    *
+   * @var \Drupal\sms\Gateway\GatewayManager
+   */
+  protected $gatewayManager;
+
+  /**
    * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
    */
   protected $moduleHandler;
 
   /**
    * Creates a new instance of the default SMS provider.
    *
+   * @param \Drupal\sms\Gateway\GatewayManagerInterface
+   *   The gateway manager.
    * @param \Drupal\Core\Extension\ModuleHandlerInterface
    *   The module handler.
    */
-  public function __construct(ModuleHandlerInterface $module_handler) {
+  public function __construct(GatewayManagerInterface $gateway_manager, ModuleHandlerInterface $module_handler) {
+    $this->gatewayManager = $gateway_manager;
     $this->moduleHandler = $module_handler;
   }
 
@@ -44,12 +55,11 @@ class DefaultSmsProvider implements SmsProviderInterface {
   public function send(SmsMessageInterface $sms, array $options = array()) {
     // Check if a preferred gateway is specified in the $options.
     if (isset($options['gateway'])) {
-      $gateway_id = $options['gateway'];
+      $gateway = $this->gatewayManager->getGateway($options['gateway']);
     }
-    if (empty($gateway_id)) {
-      $gateway_id = sms_default_gateway_id();
+    if (empty($gateway)) {
+      $gateway = $this->gatewayManager->getDefaultGateway();
     }
-    $gateway = sms_gateways('gateway', $gateway_id);
 
     if ($this->preProcess($sms, $options, $gateway)) {
       $this->moduleHandler->invokeAll('sms_send', [$sms, $options, $gateway]);
@@ -70,14 +80,14 @@ class DefaultSmsProvider implements SmsProviderInterface {
    *   The SMS to be sent.
    * @param array $options
    *   The gateway options.
-   * @param array $gateway
+   * @param \Drupal\sms\Gateway\GatewayInterface $gateway
    *   The default gateway for sending this message.
    *
    * @return bool
    *   TRUE if the message was successfully sent.
    */
-  protected function process(SmsMessageInterface $sms, array $options, array $gateway) {
-    $response = new SmsMessageResult($gateway['send']($sms, $options));
+  protected function process(SmsMessageInterface $sms, array $options, GatewayInterface $gateway) {
+    $response = $gateway->send($sms, $options);
     $result = $this->handleResult($response, $sms);
     return $result;
   }
@@ -89,17 +99,17 @@ class DefaultSmsProvider implements SmsProviderInterface {
    *   The SMS to be sent.
    * @param array $options
    *   Additional options to be passed to the SMS gateway.
-   * @param array $gateway
-       The default gateway for sending this message.
+   * @param \Drupal\sms\Gateway\GatewayInterface $gateway
+   *   The default gateway for sending this message.
    *
-   * @returns bool
+   * @return bool
    *   Whether to continue sending or not.
    */
-  protected function preProcess(SmsMessageInterface $sms, array $options, array $gateway) {
+  protected function preProcess(SmsMessageInterface $sms, array $options, GatewayInterface $gateway) {
     // Call the send pre process hooks.
     $return = $this->moduleHandler->invokeAll('sms_send_process', ['pre process', $sms, $options, $gateway, NULL]);
     // Return FALSE if any of the hooks returned FALSE.
-    return !in_array(FALSE, $return);
+    return !in_array(FALSE, $return, TRUE);
   }
 
   /**
@@ -109,12 +119,12 @@ class DefaultSmsProvider implements SmsProviderInterface {
    *   The SMS that was sent.
    * @param array $options
    *   Additional options that were passed to the SMS gateway.
-   * @param array $gateway
+   * @param \Drupal\sms\Gateway\GatewayInterface $gateway
    *   The default gateway for sending this message.
-   * @param bool $result
-   *   Whether the SMS sending was successful or not.
+   * @param bool|array $result
+   *   The result from the gateway response handler.
    */
-  protected function postProcess(SmsMessageInterface $sms, array $options, array $gateway, $result) {
+  protected function postProcess(SmsMessageInterface $sms, array $options, GatewayInterface $gateway, $result) {
     // Call the send post process hooks.
     $this->moduleHandler->invokeAll('sms_send_process', ['post process', $sms, $options, $gateway, $result]);
   }
@@ -123,7 +133,7 @@ class DefaultSmsProvider implements SmsProviderInterface {
    * Handles the response back from the SMS gateway.
    *
    * @param \Drupal\sms\Message\SmsMessageResultInterface $result
-   *   The result to be handled.
+   *   The message result to be handled.
    * @param \Drupal\sms\Message\SmsMessageInterface $sms
    *   The message that was sent.
    *
@@ -162,7 +172,7 @@ class DefaultSmsProvider implements SmsProviderInterface {
   /**
    * {@inheritdoc}
    */
-  public function receipt($number, $reference, $message_status = SMS_GW_UNKNOWN_STATUS, $options = array()) {
+  public function receipt($number, $reference, $message_status = GatewayInterface::STATUS_UNKNOWN, array $options = array()) {
     // @todo Implement rules event integration here for incoming SMS.
     // Execute three phases.
     $this->moduleHandler->invokeAll('sms_receipt', array('pre process', $number, $reference, $message_status, $options));
diff --git a/src/Provider/SmsProviderInterface.php b/src/Provider/SmsProviderInterface.php
index 6ad0ad8..bcaf66d 100644
--- a/src/Provider/SmsProviderInterface.php
+++ b/src/Provider/SmsProviderInterface.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\sms\Provider;
 
+use Drupal\sms\Gateway\GatewayInterface;
 use Drupal\sms\Message\SmsMessageInterface;
 
 /**
@@ -47,7 +48,8 @@ interface SmsProviderInterface {
    * messages in a standard format for processing, and provides a basic set of
    * status codes for common code handling.
    *
-   * Allowed message status codes are defined as constants in sms.module
+   * Allowed message status codes are defined as constants in
+   * @link \Drupal\sms\Gateway\GatewayInterface @endlink
    *
    * The original gateway code and string will often be provided in the $options
    * array as 'gateway_message_status' and 'gateway_message_status_text'.
@@ -63,6 +65,6 @@ interface SmsProviderInterface {
    * @param array $options
    *   (optional) Extended options passed by the receipt receiver.
    */
-  public function receipt($number, $reference, $message_status = SMS_GW_UNKNOWN_STATUS, $options = array());
+  public function receipt($number, $reference, $message_status = GatewayInterface::STATUS_UNKNOWN, array $options = array());
 
 }
diff --git a/src/Tests/SmsFrameworkWebTest.php b/src/Tests/SmsFrameworkWebTest.php
index 9f5e772..99e5f8d 100644
--- a/src/Tests/SmsFrameworkWebTest.php
+++ b/src/Tests/SmsFrameworkWebTest.php
@@ -7,22 +7,59 @@
 
 namespace Drupal\sms\Tests;
 
-use \Drupal\simpletest\WebTestBase;
-
 /**
  * Integration tests for the SMS Framework.
  *
  * @group SMS Framework
  */
-class SmsFrameworkWebTest extends WebTestBase {
+class SmsFrameworkWebTest extends SmsFrameworkWebTestBase {
 
-  public static $modules = ['sms', 'sms_test_gateway'];
+  /**
+   * Tests the HookGateway implementation.
+   */
+  public function testHookGatewayIntegration() {
+    // Test that hook gateway plugins are correctly discovered.
+    $gateway_plugins = $this->gatewayManager->getGatewayPlugins();
+    $this->assertEqual(array_keys($gateway_plugins), ['log', 'test'], 'Hook-based gateway discovered.');
+    $this->assertEqual($gateway_plugins['test']['hook_info'], sms_test_gateway_gateway_info()['test'], 'sms_test_gateway hooks correct.');
+
+    // Confirm the existence of the test gateway.
+    $test_gateway = $this->gatewayManager->getGateway('test');
+    $this->assertNotNull($test_gateway, 'Test gateway not null');
+
+    // Add an instance and confirm that it exists
+    $this->gatewayManager->addGateway('test', ['name' => 'test_instance', 'label' => 'Test gateway instance']);
+    $test_gateway = $this->gatewayManager->getGateway('test_instance');
+    $this->assertEqual(get_class($test_gateway), 'Drupal\sms\Gateway\HookGateway');
+    $this->assertEqual($test_gateway->getLabel(), 'Test gateway instance');
+  }
 
   /**
    * Tests that the correct gateways list is obtained.
    */
   public function testGatewaysList() {
-    $this->assertEqual(array('log' => t('Log only'), 'test' => t('For testing')), sms_gateways('names'));
+    $test_gateways = [
+      'log' => 'Log only',
+      'test' => 'For testing',
+    ];
+    $this->assertEqual($test_gateways, sms_gateways('names'));
+  }
+
+  /**
+   * Tests the add gateway functionality.
+   */
+  public function testAddGateways() {
+    $gateways = ['log', 'test'];
+    $this->assertEqual($gateways, array_keys($this->gatewayManager->getAvailableGateways()));
+    for ($i = 0; $i < 3; $i++) {
+      $name = $this->randomMachineName();
+      $this->gatewayManager->addGateway('test', ['name' => $name]);
+      // GatewayManagerInterface::getAvailableGateways() sorts by the names
+      // before adding, so we need to simulate in the expected result.
+      sort($gateways);
+      $gateways[] = $name;
+      $this->assertEqual($gateways, array_keys($this->gatewayManager->getAvailableGateways()));
+    }
   }
 
   /**
@@ -31,20 +68,21 @@ class SmsFrameworkWebTest extends WebTestBase {
   public function testDefaultGateway() {
     // Test initial default gateway.
     $gw = sms_default_gateway();
-    $this->assertEqual($gw['identifier'], 'log', 'Initial default gateway is "log".');
+    $this->assertEqual($gw->getIdentifier(), 'log', 'Initial default gateway is "log".');
 
     $this->drupalLogin($this->drupalCreateUser(['administer smsframework']));
     // Set up default log gateway.
-    $this->drupalPostForm('admin/config/smsframework/gateways', ['default' => 'log'], t('Set default gateway'));
+    $this->drupalPostForm('admin/config/smsframework/gateways', ['default' => 'log'], 'Save settings');
     $this->assertResponse(200);
     $gw = sms_default_gateway();
-    $this->assertEqual($gw['identifier'], 'log', 'Default gateway set to log.');
+    $this->assertEqual($gw->getIdentifier(), 'log', 'Default gateway set to log.');
 
     // Set up default test gateway.
-    $this->drupalPostForm('admin/config/smsframework/gateways', ['default' => 'test'], t('Set default gateway'));
+    $this->drupalPostForm('admin/config/smsframework/gateways', ['default' => 'test'], 'Save settings');
     $this->assertResponse(200);
+    $this->resetAll();
     $gw = sms_default_gateway();
-    $this->assertEqual($gw['identifier'], 'test', 'Default gateway set to test.');
+    $this->assertEqual($gw->getIdentifier(), 'test', 'Default gateway set to test.');
   }
 
   /**
@@ -59,10 +97,10 @@ class SmsFrameworkWebTest extends WebTestBase {
       'method' => 0,
       'ssl' => false,
     );
-    $this->drupalPostForm('admin/config/smsframework/gateways/test', $edit, t('Save'));
+    $this->drupalPostForm('admin/config/smsframework/gateways/test', $edit, 'Save configuration');
     $this->assertResponse(200);
-    $gateway = sms_gateways('gateway', 'test');
-    $this->assertEqual($edit, $gateway['configuration'], 'SMS Test gateway successfully configured.');
+    $gateway = $this->gatewayManager->getGateway('test');
+    $this->assertEqual($edit, $gateway->getCustomConfiguration(), 'SMS Test gateway successfully configured.');
   }
 
   /**
diff --git a/src/Tests/SmsFrameworkWebTestBase.php b/src/Tests/SmsFrameworkWebTestBase.php
new file mode 100644
index 0000000..e718097
--- /dev/null
+++ b/src/Tests/SmsFrameworkWebTestBase.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\sms\Tests\SmsFrameworkWebTestBase.
+ */
+
+namespace Drupal\sms\Tests;
+
+use \Drupal\simpletest\WebTestBase;
+
+/**
+ * Provides commonly used functionality for tests.
+ */
+abstract class SmsFrameworkWebTestBase extends WebTestBase {
+
+  public static $modules = ['sms', 'sms_test_gateway'];
+
+  /**
+   * The gateway manager.
+   *
+   * @var \Drupal\sms\Gateway\GatewayManagerInterface
+   */
+  protected $gatewayManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->gatewayManager = $this->container->get('plugin.manager.sms_gateway');
+    // Add an instance of test gateway.
+    $this->gatewayManager->addGateway('test', ['name' => 'test']);
+  }
+
+  /**
+   * Sets the specified gateway as the default.
+   *
+   * @param string $gateway_id
+   *   The ID of the gateway to be set as default.
+   */
+  public function setDefaultGateway($gateway_id) {
+    // Ensure gateway is enabled first.
+    $this->gatewayManager->setEnabledGateways([$gateway_id]);
+    $this->gatewayManager->setDefaultGateway($gateway_id);
+  }
+
+}
diff --git a/tests/modules/sms_test_gateway/sms_test_gateway.module b/tests/modules/sms_test_gateway/sms_test_gateway.module
index 68d1b4f..c0eb25b 100644
--- a/tests/modules/sms_test_gateway/sms_test_gateway.module
+++ b/tests/modules/sms_test_gateway/sms_test_gateway.module
@@ -164,17 +164,19 @@ function sms_test_gateway_config_form($configuration) {
  * @return array
  *   List of errors.
  */
-function sms_test_gateway_validate_number($number) {
-  $code = substr($number, 0, 3);
+function sms_test_gateway_validate_number($numbers) {
   $errors = array();
-  if (preg_match('/[^0-9]/', $number)) {
-    $errors[] = t('Non-numeric character found in number.');
-  }
-  if (strlen($number) > 15 || strlen($number) < 10) {
-    $errors[] = t('Number longer than 15 digits or shorter than 10 digits.');
-  }
-  if ($code == '990' || $code == '997' || $code == '999') {
-    $errors[] = t('Country code not allowed');
+  foreach ($numbers as $number) {
+    $code = substr($number, 0, 3);
+    if (preg_match('/[^0-9]/', $number)) {
+      $errors[] = t('Non-numeric character found in number.');
+    }
+    if (strlen($number) > 15 || strlen($number) < 10) {
+      $errors[] = t('Number longer than 15 digits or shorter than 10 digits.');
+    }
+    if ($code == '990' || $code == '997' || $code == '999') {
+      $errors[] = t('Country code not allowed');
+    }
   }
   return $errors;
 }
