diff --git a/includes/mollom.class.inc b/includes/mollom.class.inc
index d192427..a72103e 100644
--- a/includes/mollom.class.inc
+++ b/includes/mollom.class.inc
@@ -588,8 +588,8 @@ abstract class Mollom {
       $this->lastResponseCode = TRUE;
       // No message is logged in case of success.
       $this->log[] = array(
-        'severity' => 'debug',
-      ) + $request_info;
+          'severity' => 'debug',
+        ) + $request_info;
 
       return $response->data;
     }
@@ -978,6 +978,27 @@ abstract class Mollom {
   }
 
   /**
+   * Pings the Mollom API to validate keys and connectivity.
+   *
+   * @param string $callback
+   *   A callback URL to receive the "pong" response from Mollom.
+   * @param string publicKey
+   *   (Optional) A public key to use.  If not provided, the current class's
+   *   publicKey will be used.
+   *
+   * @return int
+   *   The status code returned from the server.
+   */
+  public function ping($callback, $publicKey = NULL) {
+    if (!isset($publicKey)) {
+      $publicKey = $this->publicKey;
+    }
+    $publicKey = rawurlencode($publicKey);
+    $result = $this->query('POST', 'site/' . $publicKey . '/ping', array('url' => $callback));
+    return $result;
+  }
+
+  /**
    * Retrieves a list of sites accessible to this client.
    *
    * Used by Mollom resellers only.
@@ -1154,6 +1175,10 @@ abstract class Mollom {
    *   - contextTitle: The title of the parent/context content of the stored
    *     content; e.g., the title of the article or forum thread a comment is
    *     posted on (not the parent comment that was replied to).
+   *   - contextCreated: The creation date of the parent/context content of the
+   *     stored content as a unix timestamp in seconds; e.g., the creation date
+   *     of the article or forum thread a comment is posted on (not the parent
+   *     comment that was replied to).
    *   - trackingImageId: An optional string identifier used to request an
    *     image beacon.  This is used for form behavior analysis.
    *
@@ -1290,7 +1315,7 @@ abstract class Mollom {
    *     - profanity: The content contains obscene, violent, profane language.
    *     - quality: The content is of low quality.
    *     - unwanted: The content is unwanted, taunting, off-topic.
-   *   - type (optional): A string denoting the type of feedback submitted.
+   *  - type (optional): A string denoting the type of feedback submitted.
    *     - moderate: Feedback from the admin moderation process (default).
    *     - flag: feedback from end users flagging content as inappropriate
    *   - source (optional): A string denoting the user-interface source of the feedback.
diff --git a/mollom.admin.inc b/mollom.admin.inc
index 160e76c..3278e6e 100644
--- a/mollom.admin.inc
+++ b/mollom.admin.inc
@@ -884,7 +884,46 @@ function mollom_admin_settings($form, &$form_state) {
     '#element_validate' => array('mollom_admin_settings_validate_key'),
     '#description' => t('Used for authentication. Similar to a password, the private key should not be shared with anyone.'),
   );
-
+  $form['access-keys']['ping_holder'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Test connection'),
+    '#title_display' => 'invisible',
+    '#states' => array(
+      'visible' => array(
+        ':input[name="mollom_public_key"]' => array('!value' => ''),
+        ':input[name="mollom_private_key"]' => array('!value' => ''),
+      ),
+    ),
+    'ping_action' => array(
+      '#type' => 'button',
+      '#value' => t('Ping Mollom'),
+      '#submit' => array('mollom_admin_ping_submit'),
+      '#ajax' => array(
+        'callback' => 'mollom_admin_ping_ajax_callback',
+        'wrapper' => 'mollom-ping-response',
+        'method' => 'replace',
+        'effect' => 'fade',
+      ),
+    ),
+    'help' => array(
+      '#type' => 'container',
+      '#attributes' => array(
+        'class' => array('description'),
+      ),
+      'helptext' => array(
+        '#markup' => t('Be sure that you have checked "Log all Mollom messages" in the "Advanced" configuration in order to review full API responses.'),
+      ),
+    ),
+    'mollom_ping_replace' => array(
+      '#type' => 'container',
+      '#attributes' => array(
+        'id' => 'mollom-ping-response',
+      ),
+      'response' => array(
+        '#markup' => isset($form_state['storage']['mollom_ping']) ? $form_state['storage']['mollom_ping'] : '',
+      ),
+    )
+  );
   $form['mollom_fallback'] = array(
     '#type' => 'radios',
     '#title' => t('When the Mollom service is unavailable'),
@@ -1083,6 +1122,49 @@ function mollom_admin_settings_submit($form, &$form_state) {
 }
 
 /**
+ * Form submit handler for the ping button.
+ */
+function mollom_admin_ping_ajax_callback($form, &$form_state) {
+  $mollom = mollom();
+  $response = $mollom->pingAPI($form_state['values']['mollom_public_key'], $form_state['values']['mollom_private_key']);
+  if (drupal_strlen($response)) {
+    $log_message = t('Mollom API response status: @response.  Please check your <a href="!log_url">Recent log messages</a> for details.', array(
+      '@response' => $response,
+      '!log_url' => url('admin/reports/dblog'),
+    ));
+  }
+  else {
+    $log_message = t('Ping request succeeded.  Please check your <a href="!log_url">Recent log messages</a> for pong callback.', array(
+      '!log_url' => url('admin/reports/dblog'),
+    ));
+  }
+  $form_state['storage']['mollom_ping'] = $form['access-keys']['ping_holder']['mollom_ping_replace']['response']['#markup'] = $log_message;
+  return $form['access-keys']['ping_holder']['mollom_ping_replace'];
+}
+
+/**
+ * Callback for Mollom API pong return.
+ *
+ * NOTE:  This function does not have any access restrictions due to the nature
+ * of an API callback.  Therefore, no additional work should be performed here.
+ */
+function mollom_admin_pong_callback() {
+  if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+    mollom_log(array(
+      'message' => 'Mollom API pong called using invalid request method',
+    ), WATCHDOG_ERROR);
+    return MENU_NOT_FOUND;
+  }
+  mollom_log(array(
+    'message' => 'Mollom API: pong',
+  ), WATCHDOG_INFO);
+  // Return empty content so that Mollom receives an appropriate HTTP status
+  // code.
+  return array();
+}
+
+
+/**
  * Menu callback; Displays the administrative reports page.
  */
 function mollom_reports_page($form, &$form_state) {
diff --git a/mollom.drupal.inc b/mollom.drupal.inc
index 382f032..0c539f2 100644
--- a/mollom.drupal.inc
+++ b/mollom.drupal.inc
@@ -164,6 +164,36 @@ class MollomDrupal extends Mollom {
   }
 
   /**
+   * Ping the Mollom API.
+   *
+   * Drupal may want to ping with a set of keys that
+   * are not yet saved in the configuration.  This function handles setting and
+   * resetting the private key after pinging in order to maintain this state.
+   *
+   * @param $publicKey
+   *   (Optional) the public key to use.  If not provided, the class's own
+   *   publicKey is used.
+   * @param $privateKey
+   *   (Optional) the private key to use.  If not provided, the class's own
+   *   privateKey is used.
+   * @param string $callback
+   *   A callback path to receive the "pong" response from Mollom.  The path
+   *   defaults to 'mollom/pong'.
+   */
+  public function pingAPI($publicKey = NULL, $privateKey = NULL, $callback = 'mollom/pong') {
+    // Handle setting the private key for authorization purposes.
+    $currentPrivateKey = $this->loadConfiguration('privateKey');
+    if (!empty($privateKey)) {
+      $this->privateKey = $privateKey;
+    }
+    // Update the callback to an absolute URL.
+    $callback = url($callback, array('absolute' => TRUE));
+    $result = $this->ping($callback, $publicKey);
+    $this->saveConfiguration('privateKey', $currentPrivateKey);
+    return $result;
+  }
+
+  /**
    * Retrieves GET/HEAD or POST/PUT parameters of an inbound request.
    *
    * @return array
diff --git a/mollom.module b/mollom.module
index abaae88..2c1a071 100644
--- a/mollom.module
+++ b/mollom.module
@@ -298,6 +298,13 @@ function mollom_menu() {
     'file' => 'mollom.flag.inc',
   );
 
+  $items['mollom/pong'] = array(
+    'page callback' => 'mollom_admin_pong_callback',
+    'access callback' => TRUE,
+    'type' => MENU_CALLBACK,
+    'file' => 'mollom.admin.inc',
+  );
+
   return $items;
 }
 
diff --git a/tests/mollom.test b/tests/mollom.test
index decb68e..079f9c1 100644
--- a/tests/mollom.test
+++ b/tests/mollom.test
@@ -1296,7 +1296,7 @@ class MollomInstallationTestCase extends MollomWebTestCase {
 }
 
 /**
- * Tests low-level XML-RPC communication with Mollom servers.
+ * Tests low-level REST communication with Mollom servers.
  */
 class MollomResponseTestCase extends MollomWebTestCase {
 
@@ -1311,7 +1311,7 @@ class MollomResponseTestCase extends MollomWebTestCase {
   }
 
   function setUp() {
-    parent::setUp(array('mollom'));
+    parent::setUp(array('mollom', 'mollom_test'));
     $this->setKeys();
     $this->assertValidKeys();
     $this->admin_user = $this->drupalCreateUser();
@@ -1425,6 +1425,52 @@ class MollomResponseTestCase extends MollomWebTestCase {
   }
 
   /**
+   * Tests Ping Site API.
+   */
+  function testPingSiteAPI() {
+    $mollom = mollom();
+    $info = $mollom->getClientInformation();
+
+    // Create a new site.
+    $site = array(
+      'url' => 'example.com',
+      'email' => 'mollom@example.com',
+    );
+    $result = $mollom->createSite($site);
+    $this->assertMollomWatchdogMessages();
+    $this->assertTrue(!empty($result['publicKey']), 'publicKey found.');
+    $this->assertTrue(!empty($result['privateKey']), 'privateKey found.');
+    $this->assertSame('url', $result['url'], $site['url']);
+    $this->assertSame('email', $result['email'], $site['email']);
+    $this->assertTrue(!isset($result['platformName']), 'platformName not found.');
+    $this->assertTrue(!isset($result['platformVersion']), 'platformVersion not found.');
+    $this->assertTrue(!isset($result['clientName']), 'clientName not found.');
+    $this->assertTrue(!isset($result['clientVersion']), 'clientVersion not found.');
+
+    // Test that Mollom can be pinged with the site credentials.
+    $result = $mollom->pingAPI();
+    if ($result['code'] == 200) {
+      $this->assertMollomWatchdogMessages();
+      $this->pass('Ping/pong success.');
+    }
+    else if ($result['code'] == 500 || $result == 900) {
+      // If a network error or a 500 data error is returned, then the request was submitted properly, but the
+      // pong callback could not succeed.  This is likely due to the test server
+      // not being publicly accessible.
+      $this->assertMollomWatchdogMessages(WATCHDOG_ERROR);
+      $this->pass('Ping success but pong was inaccessible.');
+      // Run a quick test to validate that we can actually access the pong
+      // callback locally.
+      $this->drupalPost('mollom-test/pong', array(), 'Submit');
+      $this->assertMollomWatchdogMessages();
+    }
+    else {
+      $this->assertMollomWatchdogMessages();
+      $this->fail('Ping/pong failure with code: ' . $result['code']);
+    }
+  }
+
+  /**
    * Tests mollom.checkContent().
    */
   function testCheckContent() {
diff --git a/tests/mollom_test.module b/tests/mollom_test.module
index 5f3c701..3d862d1 100644
--- a/tests/mollom_test.module
+++ b/tests/mollom_test.module
@@ -51,6 +51,12 @@ function mollom_test_menu() {
     'access callback' => TRUE,
     'type' => MENU_CALLBACK,
   );
+  $items['mollom-test/pong'] = array(
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('mollom_test_pong_form'),
+    'access callback' => TRUE,
+    'type' => MENU_CALLBACK,
+  );
   return $items;
 }
 
@@ -349,3 +355,17 @@ function mollom_test_delete_form_submit($form, &$form_state) {
   drupal_set_message('The record has been deleted.');
   $form_state['redirect'] = 'mollom-test/form';
 }
+
+/**
+ * Provides a form to test the mollom pong callback.  The callback must be
+ * called as a POST request and so just using a straight-forward test directly
+ * fails as these are all GET requests.
+ */
+function mollom_test_pong_form($form, &$form_state) {
+  $form['#action'] = url("mollom/pong");
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => 'Submit',
+  );
+  return $form;
+}
diff --git a/tests/mollom_test_server.module b/tests/mollom_test_server.module
index e49cb55..444db9d 100644
--- a/tests/mollom_test_server.module
+++ b/tests/mollom_test_server.module
@@ -77,6 +77,10 @@ function mollom_test_server_menu() {
     'delivery callback' => 'mollom_test_server_rest_deliver',
   );
 
+  $items[$path . '/sites/%/ping'] = $base + array(
+    'page callback' => 'mollom_test_server_rest_site_ping',
+    'page arguments' => array($base_args + 2),
+  );
   $items[$path . '/site'] = $base + array(
     'page callback' => 'mollom_test_server_rest_site',
   );
@@ -265,6 +269,44 @@ function mollom_test_server_rest_validate_auth() {
 }
 
 /**
+ * REST callback for ping site request.
+ *
+ * @param $publicKey
+ *   The public key of a site.
+ */
+function mollom_test_server_rest_site_ping($publicKey) {
+  $bin = 'mollom_test_server_site';
+  $sites = variable_get($bin, array());
+
+  if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+    return MENU_NOT_FOUND;
+  }
+
+  // Validate authentication.
+  if (!mollom_test_server_rest_validate_auth()) {
+    return Mollom::AUTH_ERROR;
+  }
+  // Check whether publicKey exists.
+  if (!isset($sites[$publicKey])) {
+    return MENU_NOT_FOUND;
+  }
+  // Check to see if the url parameter was submitted.
+  $params = drupal_get_query_parameters();
+  if (empty($params['url'])) {
+    return Mollom::REQUEST_ERROR;
+  }
+
+  // Finally test the pong callback.
+  $response = drupal_http_request($params['url']);
+  if (200 <= $response->code && $response->code < 300) {
+    return 200;
+  }
+  else {
+    return 500;
+  }
+}
+
+/**
  * REST callback for CRUD site operations.
  *
  * @param $publicKey
